Android Snackbar 源码分析

1.最简单创造方法

1.1 Snackbar作用

  • Snackbar是Android支持库中用于显示简单消息并且提供和用户的一个简单操作的一种弹出式提醒。当使用Snackbar时,提示会出现在消息最底部,通常含有一段信息和一个可点击的按钮。
  • 同样作为消息提示,Snackbar相比于Toast而言,增加了一个用户操作,并且在同时弹出多个消息时,Snackbar会停止前一个,直接显示后一个,也就是说同一时刻只会有一个Snackbar在显示;而Toast则不然,如果不做特殊处理,那么同时可以有多个Toast出现;Snackbar相比于Dialog,操作更少,因为只有一个用户操作的接口,而Dialog最多可以设置三个,另外Snackbar的出现并不影响用户的继续操作,而Dialog则必须需要用户做出响应,所以相比Dialog,Snackbar更轻量。

1.2 最简单的创建

  • 如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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<Snackbar>() {
@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等。
    • image
  • 其中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
    • image

2.2 对Snackbar属性进行设置

  • 2.2.1 setActionTextColor设置action颜色

    • 可以看到先是获取父布局contentLayout,然后在获取snackbar_action的mActionView
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @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,然后判断文本和监听器,设置可见性、文本、监听器。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    @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方法,则执行这个条件代码
    • 如果两条件都不成立,则需要创建一个新记录并对其进行排队。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    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。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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消失。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    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 显示和隐藏中动画源码分析

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

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

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

    • 可以看到在动画结束的最后都调用了onViewHidden方法,所以最终都是要调用onViewHidden方法的。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    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移除。
    • image

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类的定义:
    • image
  • 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时为何从下往上移出来,看下面这段代码就知道呢,如下所示
    • image

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

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

4.5 Snackbar与吐司有何区别

  • 与Toast进行比较,SnackBar有优势:
    • 1.SnackBar可以自动消失,也可以手动取消(侧滑取消,但是需要在特殊的布局中,后面会仔细说)
    • 2.SnackBar可以通过setAction()来与用户进行交互
    • 3.通过CallBack我们可以获取SnackBar的状态
    • image

5.Snackbar封装库

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    //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();