资讯专栏INFORMATION COLUMN

Snackbar源码分析

why_rookie / 1791人阅读

摘要:分别对应于中的几个常量值。源码分析的方法源码分析创建需要使用静态的方法,并且其中的参数是一个查找父布局的起点这里可以看到,的布局是,假如我们需要自定义并且设置字体颜色,大小等属性。表示回调已在队列中。

目录介绍

1.最简单创造方法

1.1 Snackbar作用

1.2 最简单的创建

1.3 Snackbar消失的几种方式

2.源码分析

2.1 Snackbar的make方法源码分析

2.2 对Snackbar属性进行设置

2.3 Snackbar的show显示与点击消失

2.4 显示和隐藏中动画源码分析

3.经典总结

3.1 Snackbar和SnackbarManager类的设计

4.思考问题分析

4.1 Snackbar的设计思路

4.2 什么时候Snackbar显示会导致FloatingActionButton上移

4.3 Snackbar控件show时为何从下往上移出来

4.4 为什么Snackbar总是显示在最下面

4.5 Snackbar与吐司有何区别

5.Snackbar封装库

好消息

博客笔记大汇总【16年3月到至今】,包括Java基础及深入知识点,Android技术博客,Python学习笔记等等,还包括平时开发中遇到的bug汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是markdown格式的!同时也开源了生活博客,从12年起,积累共计47篇[近20万字],转载请注明出处,谢谢!

链接地址:https://github.com/yangchong2...

如果觉得好,可以star一下,谢谢!当然也欢迎提出建议,万事起于忽微,量变引起质变!

Snackbar封装库项目地址:https://github.com/yangchong2...

02.Toast源码深度分析

最简单的创建,简单改造避免重复创建,show()方法源码分析,scheduleTimeoutLocked吐司如何自动销毁的,TN类中的消息机制是如何执行的,普通应用的Toast显示数量是有限制的,用代码解释为何Activity销毁后Toast仍会显示,Toast偶尔报错Unable to add window是如何产生的,Toast运行在子线程问题,Toast如何添加系统窗口的权限等等

03.DialogFragment源码分析

最简单的使用方法,onCreate(@Nullable Bundle savedInstanceState)源码分析,重点分析弹窗展示和销毁源码,使用中show()方法遇到的IllegalStateException分析

05.PopupWindow源码分析

显示PopupWindow,注意问题宽和高属性,showAsDropDown()源码,dismiss()源码分析,PopupWindow和Dialog有什么区别?为何弹窗点击一下就dismiss呢?

06.Snackbar源码分析

最简单的创建,Snackbar的make方法源码分析,Snackbar的show显示与点击消失源码分析,显示和隐藏中动画源码分析,Snackbar的设计思路,为什么Snackbar总是显示在最下面

07.弹窗常见问题

DialogFragment使用中show()方法遇到的IllegalStateException,什么常见产生的?Toast偶尔报错Unable to add window,Toast运行在子线程导致崩溃如何解决?

1.最简单创造方法 1.1 Snackbar作用

Snackbar是Android支持库中用于显示简单消息并且提供和用户的一个简单操作的一种弹出式提醒。当使用Snackbar时,提示会出现在消息最底部,通常含有一段信息和一个可点击的按钮。

同样作为消息提示,Snackbar相比于Toast而言,增加了一个用户操作,并且在同时弹出多个消息时,Snackbar会停止前一个,直接显示后一个,也就是说同一时刻只会有一个Snackbar在显示;而Toast则不然,如果不做特殊处理,那么同时可以有多个Toast出现;Snackbar相比于Dialog,操作更少,因为只有一个用户操作的接口,而Dialog最多可以设置三个,另外Snackbar的出现并不影响用户的继续操作,而Dialog则必须需要用户做出响应,所以相比Dialog,Snackbar更轻量。

1.2 最简单的创建

如下所示

Snackbar sb = Snackbar.make(v,"潇湘剑雨",Snackbar.LENGTH_LONG)
        .setAction("删除吗?", new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //点击了"是吗?"字符串操作
                ToastUtils.showRoundRectToast("逗比");
            }
        })
        .setActionTextColor(Color.RED)
        .setText("杨充是个逗比")
        .addCallback(new BaseTransientBottomBar.BaseCallback() {
            @Override
            public void onDismissed(Snackbar transientBottomBar, int event) {
                super.onDismissed(transientBottomBar, event);
                switch (event) {
                    case Snackbar.Callback.DISMISS_EVENT_CONSECUTIVE:
                    case Snackbar.Callback.DISMISS_EVENT_MANUAL:
                    case Snackbar.Callback.DISMISS_EVENT_SWIPE:
                    case Snackbar.Callback.DISMISS_EVENT_TIMEOUT:
                        ToastUtils.showRoundRectToast("删除成功");
                        break;
                    case Snackbar.Callback.DISMISS_EVENT_ACTION:
                        ToastUtils.showRoundRectToast("撤销了删除操作");
                        break;
                }
                Log.d("MainActivity","onDismissed");
            }
            @Override
            public void onShown(Snackbar transientBottomBar) {
                super.onShown(transientBottomBar);
                Log.d("MainActivity","onShown");
            }
        });
sb.show();
1.3 Snackbar消失的几种方式

Snackbar显示只有一种方式,那就是调用show()方法,但是消失有几种方式:时间到了自动消失、点击了右侧按钮消失、新的Snackbar出现导致旧的Snackbar消失、滑动消失或者通过调用dismiss()消失。

分别对应于Snackbar.Callback中的几个常量值。

DISMISS_EVENT_ACTION:点击了右侧按钮导致消失

DISMISS_EVENT_CONSECUTIVE:新的Snackbar出现导致旧的消失

DISMISS_EVENT_MANUAL:调用了dismiss方法导致消失

DISMISS_EVENT_SWIPE:滑动导致消失

DISMISS_EVENT_TIMEOUT:设置的显示时间到了导致消失

Callback有两个方法

void onDismissed(B transientBottomBar, @DismissEvent int event)

void onShown(B transientBottomBar)

其中onShown在Snackbar可见时调用,onDismissed在Snackbar准备消失时调用。

2.源码分析 2.1 Snackbar的make方法源码分析

创建Snackbar需要使用静态的make方法,并且其中的view参数是一个查找父布局的起点

这里可以看到,snackBar的布局是design_layout_snackbar_include,假如我们需要自定义SnackBar并且设置字体颜色,大小等属性。则需要拿到这个布局的控件id等。关于封装库,可以查看:https://github.com/yangchong2...

其中findSuitableParent()方法为以view为起点寻找合适的父布局,下面看看findSuitableParent()如何做的?

看了下面源码可知:可以看到如果view是CoordinatorLayout,那么就直接作为父布局了;如果是FrameLayout,并且如果是android.R.id.content,也就是查找到了DecorView,即最顶部,那么就只用这个view;如果不是的话,先保存下来;接下来就是获取view的父布局,然后循环再次判断。这样导致的结果最终会有两个选择,要么是CoordinatorLayout,要么就是FrameLayout,并且是最顶层的那个布局。

如果从View往上搜寻,如果有CoordinatorLayout,那么就使用该CoordinatorLayout ;如果从View往上搜寻,没有CoordinatorLayout,那么就使用android.R.id.content的FrameLayout

2.2 对Snackbar属性进行设置

2.2.1 setActionTextColor设置action颜色

可以看到先是获取父布局contentLayout,然后在获取snackbar_action的mActionView

@NonNull
public Snackbar setActionTextColor(@ColorInt int color) {
    final SnackbarContentLayout contentLayout = (SnackbarContentLayout) mView.getChildAt(0);
    final TextView tv = contentLayout.getActionView();
    tv.setTextColor(color);
    return this;
}

//然后看SnackbarContentLayout类中getActionView方法
@Override
protected void onFinishInflate() {
    super.onFinishInflate();
    mMessageView = (TextView) findViewById(R.id.snackbar_text);
    mActionView = (Button) findViewById(R.id.snackbar_action);
}
public Button getActionView() {
    return mActionView;
}

2.2.2 看setAction()方法的实现

首先是获取父布局contentLayout,然后通过contentLayout调用getActionView()方法,返回的tv其实就是右边的Button,然后判断文本和监听器,设置可见性、文本、监听器。

@NonNull
public Snackbar setAction(CharSequence text, final View.OnClickListener listener) {
    final SnackbarContentLayout contentLayout = (SnackbarContentLayout) mView.getChildAt(0);
    final TextView tv = contentLayout.getActionView();

    if (TextUtils.isEmpty(text) || listener == null) {
        tv.setVisibility(View.GONE);
        tv.setOnClickListener(null);
    } else {
        tv.setVisibility(View.VISIBLE);
        tv.setText(text);
        tv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                listener.onClick(view);
                // Now dismiss the Snackbar
                dispatchDismiss(BaseCallback.DISMISS_EVENT_ACTION);
            }
        });
    }
    return this;
}

2.3 Snackbar的show显示与点击消失

2.3.1 show显示

可以看到,首先获取一个SnackbarManager对象,然后调用它的show方法。可以看到在这个方法中,先判断如果是当前正在显示的SnackBar对应的CallBack,则更新显示时长,然后从消息队列中移除,最后调用scheduleTimeoutLocked方法发送定时消息dismiss;如果是下一个要显示的,则更新显示时长;如果都不是,那么就创建一个SnackbarRecord对象。

isCurrentSnackbarLocked:如果当前已经有一个Snackbar显示了,又再调用了该对象的show方法,但是只是设置了不同时间,那么isCurrentSnackbarLocked就会是true,执行里面的方法。

isNextSnackbarLocked:如果当前已有一个Snackbar正在显示,又创建了一个新的Snackbar并调用show方法,则执行这个条件代码

如果两条件都不成立,则需要创建一个新记录并对其进行排队。

public void show() {
    SnackbarManager.getInstance().show(mDuration, mManagerCallback);
}

public void show(int duration, Callback callback) {
    synchronized (mLock) {
        if (isCurrentSnackbarLocked(callback)) {
            // 表示回调已在队列中。我们只需更新持续时间
            mCurrentSnackbar.duration = duration;

            // 如果这是当前正在显示的Snackbar,请调用重新调度它的
            // timeout
            mHandler.removeCallbacksAndMessages(mCurrentSnackbar);
            // 这个方法很重要,当执行时间结束后,就会自动dismiss。下面再详细分析
            scheduleTimeoutLocked(mCurrentSnackbar);
            return;
        } else if (isNextSnackbarLocked(callback)) {
            //我们只需更新持续时间
            mNextSnackbar.duration = duration;
        } else {
            //否则,我们需要创建一个新记录并对其进行排队。
            mNextSnackbar = new SnackbarRecord(duration, callback);
        }
        if (mCurrentSnackbar != null && cancelSnackbarLocked(mCurrentSnackbar,Snackbar.Callback.DISMISS_EVENT_CONSECUTIVE)) {
            // 如果我们目前有一个Snackbar,请尝试取消它并排队等待。
            return;
        } else {
            // 清除当前的快捷键
            mCurrentSnackbar = null;
            //很重要
            showNextSnackbarLocked();
        }
    }
}

//注意这个callback方法
final SnackbarManager.Callback mManagerCallback = new SnackbarManager.Callback() {
    @Override
    public void show() {
        sHandler.sendMessage(sHandler.obtainMessage(MSG_SHOW, BaseTransientBottomBar.this));
    }

    @Override
    public void dismiss(int event) {
        sHandler.sendMessage(sHandler.obtainMessage(MSG_DISMISS, event, 0,
                BaseTransientBottomBar.this));
    }
};

//处理sHandler发送的消息
static {
    sHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
        @Override
        public boolean handleMessage(Message message) {
            switch (message.what) {
                case MSG_SHOW:
                    ((BaseTransientBottomBar) message.obj).showView();
                    return true;
                case MSG_DISMISS:
                    ((BaseTransientBottomBar) message.obj).hideView(message.arg1);
                    return true;
            }
            return false;
        }
    });
}

然后看看showNextSnackbarLocked这个方法,注意:mCurrentSnackbar当前正在显示的,而mNextSnackbar是下一个要显示的。能看到会调用callback的show方法,而这个calllback对象就是我们在调用snackbar的show方法是传进去的那个。向Snackbar的Handler发送一个消息,最后显示Snackbar。

private void showNextSnackbarLocked() {
    if (mNextSnackbar != null) {
        mCurrentSnackbar = mNextSnackbar;
        mNextSnackbar = null;

        final Callback callback = mCurrentSnackbar.callback.get();
        if (callback != null) {
            callback.show();
        } else {
            // The callback doesn"t exist any more, clear out the Snackbar
            mCurrentSnackbar = null;
        }
    }
}

2.3.2 看看scheduleTimeoutLocked源码如何销毁snackBar

可以发现,如果我们设置为无限期,则不会设置超时,直接return函数。然后发送了一个叫做MSG_TIMEOUT的消息,继续追终,最后会到达cancelSnackbarLocked方法。在cancelSnackbarLocked这个方法中,首先移除SnackbarRecord发出的所有消息,然后调用Callback的dismiss方法,从上面我们知道最终是向Snackbar的sHandler发送了一条消息,最终是调用Snackbar的hideView消失。

private void scheduleTimeoutLocked(SnackbarRecord r) {
    if (r.duration == Snackbar.LENGTH_INDEFINITE) {
        // If we"re set to indefinite, we don"t want to set a timeout
        return;
    }

    int durationMs = LONG_DURATION_MS;
    if (r.duration > 0) {
        durationMs = r.duration;
    } else if (r.duration == Snackbar.LENGTH_SHORT) {
        durationMs = SHORT_DURATION_MS;
    }
    mHandler.removeCallbacksAndMessages(r);
    mHandler.sendMessageDelayed(Message.obtain(mHandler, MSG_TIMEOUT, r), durationMs);
}

//接受mHandler消息并且处理
mHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
    @Override
    public boolean handleMessage(Message message) {
        switch (message.what) {
            case MSG_TIMEOUT:
                handleTimeout((SnackbarRecord) message.obj);
                return true;
        }
        return false;
    }
});

//
void handleTimeout(SnackbarRecord record) {
    synchronized (mLock) {
        if (mCurrentSnackbar == record || mNextSnackbar == record) {
            cancelSnackbarLocked(record, Snackbar.Callback.DISMISS_EVENT_TIMEOUT);
        }
    }
}

//最终可以追踪到这个方法
private boolean cancelSnackbarLocked(SnackbarRecord record, int event) {
    final Callback callback = record.callback.get();
    if (callback != null) {
        // Make sure we remove any timeouts for the SnackbarRecord
        mHandler.removeCallbacksAndMessages(record);
        callback.dismiss(event);
        return true;
    }
    return false;
}

2.4 显示和隐藏中动画源码分析

在显示的时候是这样设置动画的,具体如下所示

在隐藏的时候是这样设置动画的,具体如下所示

最后具体看一下animateViewOut部分源码

可以看到在动画结束的最后都调用了onViewHidden方法,所以最终都是要调用onViewHidden方法的。

private void animateViewOut(final int event) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
        ViewCompat.animate(mView)
                .translationY(mView.getHeight())
                .setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR)
                .setDuration(ANIMATION_DURATION)
                .setListener(new ViewPropertyAnimatorListenerAdapter() {
                    @Override
                    public void onAnimationStart(View view) {
                        mContentViewCallback.animateContentOut(0, ANIMATION_FADE_DURATION);
                    }

                    @Override
                    public void onAnimationEnd(View view) {
                        onViewHidden(event);
                    }
                }).start();
    } else {
        Animation anim = AnimationUtils.loadAnimation(mView.getContext(),
                R.anim.design_snackbar_out);
        anim.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
        anim.setDuration(ANIMATION_DURATION);
        anim.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationEnd(Animation animation) {
                onViewHidden(event);
            }

            @Override
            public void onAnimationStart(Animation animation) {}

            @Override
            public void onAnimationRepeat(Animation animation) {}
        });
        mView.startAnimation(anim);
    }
}

onViewHidden提供具体的业务处理,具体如下所示

首先调用SnackbarManager的onDismissed方法,然后判断Snackbar.Callback是不是null,调用Snackbar.Callback的onDismissed方法,就是我们上面介绍的处理Snackbar消失的方法。最后就是将Snackbar的mView移除。

3.经典总结 3.1 Snackbar和SnackbarManager类的设计

Snackbar和SnackbarManager,SnackbarManager内部有两个SnackbarRecord,一个mCurrentSnackbar,一个mNextSnackbar,SnackbarManager通过这两个对象实现Snackbar的顺序显示,如果在一个Snackbar显示之前有Snackbar正在显示,那么使用mNextSnackbar保存第二个Snackbar,然后让第一个Snackbar消失,然后消失之后再调用SnackbarManager显示下一个Snackbar,如此循环,实现了Snackbar的顺序显示。

Snackbar负责显示和消失,具体来说其实就是添加和移除View的过程。Snackbar和SnackbarManager的设计很巧妙,利用一个SnackbarRecord对象保存Snackbar的显示时间以及SnackbarManager.Callback对象,前面说到每一个Snackbar都有一个叫做mManagerCallback的SnackbarManager.Callback对象,下面看一下SnackRecord类的定义:

Snackbar向SnackbarManager发送消息主要是调用SnackbarManager.getInstace()返回一个单例对象;而SnackManager向Snackbar发送消息就是通过show方法传入的Callback对象。SnackbarManager中的Handler只处理一个MSG_TIMEOUT事件,最后是调用Snackbar的hideView消失的;Snackbar的sHandler处理两个消息,showView和hideView,而消息的发送者是mManagerCallback,控制者是SnackbarManager。

4.思考问题分析 4.1 Snackbar的设计思路

具体可以看经典总结3.1

4.2 什么时候Snackbar显示会导致FloatingActionButton上移

为什么CoordinatorLayout + FloatingActionButton,当Snackbar显示的时候FloatingActionButton会上移呢,这个是怎么实现的?

把CoordinatorLayout替换成FrameLayout确不行。这个问题我们还没说。其实这个不是在Snackbar里面处理的,是通过CoordinatorLayout和Behavior来处理的。那具体的处理在哪里呢。FloatingActionButton类里面Behavior类。正是Behavior里面的两个函数layoutDependsOn()和onDependentViewChanged()函数作用的结果。直接进去看下FloatingActionButton内部类Behavior里面这两个函数的代码。

4.3 Snackbar控件show时为何从下往上移出来

至于说Snackbar控件show时为何从下往上移出来,看下面这段代码就知道呢,如下所示

4.4 为什么Snackbar总是显示在最下面

直接找到make方法中的填充布局,然后去看design_layout_snackbar_include的布局参数,结果如下:

4.5 Snackbar与吐司有何区别

与Toast进行比较,SnackBar有优势:

1.SnackBar可以自动消失,也可以手动取消(侧滑取消,但是需要在特殊的布局中,后面会仔细说)

2.SnackBar可以通过setAction()来与用户进行交互

3.通过CallBack我们可以获取SnackBar的状态

5.Snackbar封装库

可以一行代码调用,也可以自己使用链式编程调用。支持设置显示时长属性;可以设置背景色;可以设置文字大小,颜色;可以设置action内容,文字大小,颜色,还有点击事件;可以设置icon;代码如下所示,更多内容可以直接运行demo哦!

//1.只设置text
SnackBarUtils.showSnackBar(this,"滚犊子");

//2.设置text,action,和点击事件
SnackBarUtils.showSnackBar(this, "滚犊子", "ACTION", new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        ToastUtils.showRoundRectToast("滚犊子啦?");
    }
});

//3.设置text,action,和点击事件,和icon
SnackBarUtils.showSnackBar(this, "滚犊子", "ACTION",R.drawable.icon_cancel, new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        ToastUtils.showRoundRectToast("滚犊子啦?");
    }
});

//4.链式调用
SnackBarUtils.builder()
    .setBackgroundColor(this.getResources().getColor(R.color.color_7f000000))
    .setTextSize(14)
    .setTextColor(this.getResources().getColor(R.color.white))
    .setTextTypefaceStyle(Typeface.BOLD)
    .setText("滚犊子")
    .setMaxLines(4)
    .centerText()
    .setActionText("收到")
    .setActionTextColor(this.getResources().getColor(R.color.color_f25057))
    .setActionTextSize(16)
    .setActionTextTypefaceStyle(Typeface.BOLD)
    .setActionClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            ToastUtils.showRoundRectToast("滚犊子啦?");
        }
    })
    .setIcon(R.drawable.icon_cancel)
    .setActivity(MainActivity.this)
    .setDuration(SnackBarUtils.DurationType.LENGTH_INDEFINITE)
    .build()
    .show();

关于其他内容介绍 01.关于博客汇总链接

1.技术博客汇总

2.开源项目汇总

3.生活博客汇总

4.喜马拉雅音频汇总

5.其他汇总

02.关于我的博客

我的个人站点:www.yczbj.org,www.ycbjie.cn

github:https://github.com/yangchong211

知乎:https://www.zhihu.com/people/...

简书:http://www.jianshu.com/u/b7b2...

csdn:http://my.csdn.net/m0_37700275

喜马拉雅听书:http://www.ximalaya.com/zhubo...

开源中国:https://my.oschina.net/zbj161...

泡在网上的日子:http://www.jcodecraeer.com/me...

邮箱:yangchong211@163.com

阿里云博客:https://yq.aliyun.com/users/a... 239.headeruserinfo.3.dT4bcV

segmentfault头条:https://segmentfault.com/u/xi...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/71687.html

相关文章

  • Android之Window和弹窗问题

    摘要:指向的主要是实现和通信的。子不能单独存在,需附属特定的父。系统需申明权限才能创建。和类似,同样是通过来实现。将添加到中显示。方法完成的显示。执行的检查参数等设置检查将保存到中将保存到中。因为通过和的将无法获取到从而导致失败。 目录介绍 10.0.0.1 Window是什么?如何通过WindowManager添加Window(代码实现)?WindowManager的主要功能是什么? 1...

    Lorry_Lu 评论0 收藏0
  • Activity、Window、View三者关系

    摘要:在代码中的直接应用是或者是。就像一个控制器,统筹视图的添加与显示,以及通过其他回调方法,来与以及进行交互。创建需要通过创建,通过将加载其中,并将交给,进行视图绘制以及其他交互。创建机制分析实例的创建中执行,从而生成了的实例。 目录介绍 01.Window,View,子Window 02.什么是Activity 03.什么是Window 04.什么是DecorView 05.什么是Vi...

    Cristic 评论0 收藏0
  • Flutter Toast的使用,安卓开发知识点

    摘要:创建一个创建遵循准则的应用程序时,请为您的应用程序提供一致的外观结构。例如,如果用户不小心删除了一条消息,则他们可以在中使用可选的动作来恢复该消息。来自第三方库的依赖,是一个来自第三方库的给用户弹出提示的实现,它覆盖了安卓,,及等平台。 来自Material Design的官方Toast,Snackbars在执...

    番茄西红柿 评论0 收藏2637
  • [Android] Toast问题深度剖析(二)

    摘要:所以,从体验上考虑,这个情况并不属于问题。一般情况下,这个节点占据了除了通知栏的所有区域。通知给对象的消息,都会被这个内部对象进行处理通过执行处理消息在通知给对象显示的时候,对象将给对象发送一条消息,并在的函数中执行。 欢迎大家前往云+社区,获取更多腾讯海量技术实践干货哦~ 作者:QQ音乐技术团队 题记 Toast 作为 Android 系统中最常用的类之一,由于其方便的api设计和...

    cloud 评论0 收藏0
  • Android Material Design系列之FloatingActionButton和Sna

    摘要:今天主讲的系列的两个控件都不难,所以一起讲了,分别是和。之所以出现这么久了,不太火,不太常用,估计跟他悬浮有关,容易挡住其他内容。那我们现在就研究改如何在滑动列表时隐藏和显示这个悬浮按钮。其实也非常简单,和修改样式的过程差不多。 今天主讲的Material Design系列的两个控件都不难,所以一起讲了,分别是FloatingActionButton和Snackbar。这个系列都是主讲...

    iKcamp 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<