Snackbar 源码分析

Snackbar 是一个类似于 Toast 的新控件,它可以给用户一些提示等等,可以和 Toast、Dialog 等划分为一起,起到提示的作用,它的用法也比较简单,网上有许多文章介绍,今天主要介绍一些它的源码,我们使用它之前,要在gradle文件中导入依赖   implementation 'com.android.support:design:23.4.0' ,然后就可以使用了,它的弹出方式和Toast类型,也是通过静态方法创建的
    Snackbar.make(view, "This is a SnackBar ", Snackbar.LENGTH_LONG).show();
view 是我们Activity中的一个控件,任何一个都行,没有限制。我们来看看 Snackbar 的代码

    public static Snackbar make(@NonNull View view, @NonNull CharSequence text,
            @Duration int duration) {
        Snackbar snackbar = new Snackbar(findSuitableParent(view));
        snackbar.setText(text);
        snackbar.setDuration(duration);
        return snackbar;
    }

    private static ViewGroup findSuitableParent(View view) {
        ViewGroup fallback = null;
        do {
            if (view instanceof CoordinatorLayout) {
                return (ViewGroup) view;
            } else if (view instanceof FrameLayout) {
                if (view.getId() == android.R.id.content) {
                    return (ViewGroup) view;
                } else {
                    fallback = (ViewGroup) view;
                }
            }
            if (view != null) {
                final ViewParent parent = view.getParent();
                view = parent instanceof View ? (View) parent : null;
            }
        } while (view != null);
        return fallback;
    }

我们来看看 findSuitableParent() 方法,使用了 do ... while ,会先执行 do 里面的命令,然后才是 while 判断,根据判断的结果决定是否继续执行 do 里的语句,看看这个方法,意思就是判断传入的 view 的类型,如果是 CoordinatorLayout,则返回;如果属于 FrameLayout ,则先把 view 赋值给 fallback,留着做最坏的打算,如果是id是android.R.id.content,说明是系统的 FrameLayout 布局控件了,则返回;如果即不是 CoordinatorLayout 也不是 R.id.content,则找 view 的父容器,只要父容器不为null,就重新进行 CoordinatorLayout 和 R.id.content的判断。从这个方法中知道,返回的 ViewGroup fallback 对象,要么是自己layout中的 CoordinatorLayout 布局,要么是系统 R.id.content 的 FrameLayout 布局,其次是自己layout中的帧布局,最不济的情况,fallback 为 null,获取到适合的 ViewGroup 后,通过构造方法 new Snackbar(findSuitableParent(view)) 生成一个 Snackbar

    private Snackbar(ViewGroup parent) {
        mTargetParent = parent;
        mContext = parent.getContext();
        ThemeUtils.checkAppCompatTheme(mContext);
        LayoutInflater inflater = LayoutInflater.from(mContext);
        mView = (SnackbarLayout) inflater.inflate(
                R.layout.design_layout_snackbar, mTargetParent, false);

        mAccessibilityManager = (AccessibilityManager)
                mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
    }

构造方法中,或通过 parent 来获取上下文,所以如果 parent 为 null的话,就会崩溃。注意 mView = (SnackbarLayout) inflater.inflate(R.layout.design_layout_snackbar, mTargetParent, false) 方法, 看看 design_layout_snackbar 的布局

<view xmlns:android="http://schemas.android.com/apk/res/android"
      class="android.support.design.widget.Snackbar$SnackbarLayout"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:layout_gravity="bottom"
      style="@style/Widget.Design.Snackbar" />

注意 class="android.support.design.widget.Snackbar$SnackbarLayout" 这一行代码,它指定的 View 的路径,我们来看看 SnackbarLayout,它是 SnackBar 的内部类

public static class SnackbarLayout extends LinearLayout {
    private TextView mMessageView;
    private Button mActionView;

    interface OnAttachStateChangeListener {
        void onViewAttachedToWindow(View v);
        void onViewDetachedFromWindow(View v);
    }

    private OnAttachStateChangeListener mOnAttachStateChangeListener;

    public SnackbarLayout(Context context) {
        this(context, null);
    }

    public SnackbarLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        ...
        setClickable(true);
        LayoutInflater.from(context).inflate(R.layout.design_layout_snackbar_include, this);
        ...
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mMessageView = (TextView) findViewById(R.id.snackbar_text);
        mActionView = (Button) findViewById(R.id.snackbar_action);
    }

    ...
    
    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        if (mOnAttachStateChangeListener != null) {
            mOnAttachStateChangeListener.onViewAttachedToWindow(this);
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (mOnAttachStateChangeListener != null) {
            mOnAttachStateChangeListener.onViewDetachedFromWindow(this);
        }
    }

    void setOnAttachStateChangeListener(OnAttachStateChangeListener listener) {
        mOnAttachStateChangeListener = listener;
    }
    
   ...
}

LayoutInflater.from(context).inflate(R.layout.design_layout_snackbar_include, this) 找到layout布局,然后添加到当前LinearLayout 中,看看 design_layout_snackbar_include 的布局

<merge xmlns:android="http://schemas.android.com/apk/res/android">
    <TextView
            android:id="@+id/snackbar_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:paddingTop="@dimen/design_snackbar_padding_vertical"
            android:paddingBottom="@dimen/design_snackbar_padding_vertical"
            android:paddingLeft="@dimen/design_snackbar_padding_horizontal"
            android:paddingRight="@dimen/design_snackbar_padding_horizontal"
            android:textAppearance="@style/TextAppearance.Design.Snackbar.Message"
            android:maxLines="@integer/design_snackbar_text_max_lines"
            android:layout_gravity="center_vertical|left|start"
            android:ellipsize="end"
            android:textAlignment="viewStart"/>

    <Button
            android:id="@+id/snackbar_action"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="@dimen/design_snackbar_extra_spacing_horizontal"
            android:layout_marginStart="@dimen/design_snackbar_extra_spacing_horizontal"
            android:layout_gravity="center_vertical|right|end"
            android:paddingTop="@dimen/design_snackbar_padding_vertical"
            android:paddingBottom="@dimen/design_snackbar_padding_vertical"
            android:paddingLeft="@dimen/design_snackbar_padding_horizontal"
            android:paddingRight="@dimen/design_snackbar_padding_horizontal"
            android:visibility="gone"
            android:textColor="?attr/colorAccent"
            style="?attr/borderlessButtonStyle"/>
</merge>

根节点用的是 merge,这样 inflate(R.layout.design_layout_snackbar_include, this) 中,把 layout 添加到 LinearLayout 中,直接就使用了 LinearLayout 布局,这样比着根节点也适用 LinearLayout 布局,可以减少一层布局,这是 LayoutInflater 的使用技巧,可以记一下。

onAttachedToWindow() 和 onDetachedFromWindow() 是当View被添加到ViewGroup中,显示在UI上和被remove时从UI上消失时分别触发的,mOnAttachStateChangeListener 是监听回调,可以设置一些监听,mMessageView 和 mActionView 是布局中的两个控件,对外暴露的了方法,可以获取到,SnackBar 中的构造方法中 snackbar.setText(text) 这行代码,对应的是 mMessageView

    public Snackbar setText(@NonNull CharSequence message) {
        final TextView tv = mView.getMessageView();
        tv.setText(message);
        return this;
    }

UI上的显示文字就是这么来的,它还有个方法 setAction() 这个可以设置点击事件

    public Snackbar setAction(CharSequence text, final View.OnClickListener listener) {
        final TextView tv = mView.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);
                    dispatchDismiss(Callback.DISMISS_EVENT_ACTION);
                }
            });
        }
        return this;
    }

这个对应的是 mActionView,如果文案或点击事件任何一个为空,就隐藏该控件;否则就显示,并添加点击事件,同时在点击事件中增加 dispatchDismiss(Callback.DISMISS_EVENT_ACTION) 方法,这个方法一会再分析; setActionTextColor( ) 方法是改变 mActionView 的字体颜色; getView() 方法可以获取到 SnackBar 中的 layout 对应的view,然后通过 view.findViewById() 找到对应的控件,做自己需要的操作。下面重点来了 show() 方法
    public void show() {
        SnackbarManager.getInstance().show(mDuration, mManagerCallback);
    }
SnackbarManager 是个单例,意思是所有的 SnackBar 对应一个 SnackbarManager 对象,看看它的 show() 方法

    public void show(int duration, Callback callback) {
        synchronized (mLock) {
            if (isCurrentSnackbarLocked(callback)) {
                mCurrentSnackbar.duration = duration;
                mHandler.removeCallbacksAndMessages(mCurrentSnackbar);
                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)) {
                return;
            } else {
                // 步骤三
                mCurrentSnackbar = null;
                showNextSnackbarLocked();
            }
        }
    }

    private boolean isCurrentSnackbarLocked(Callback callback) {
        return mCurrentSnackbar != null && mCurrentSnackbar.isSnackbar(callback);
    }

    private boolean isNextSnackbarLocked(Callback callback) {
        return mNextSnackbar != null && mNextSnackbar.isSnackbar(callback);
    }

第一次调用 SanckBar 的 show() 方法时,SnackBarManager 中的 mCurrentSnackbar 和 mNextSnackbar 值都为 null,它们是 SnackbarRecord 类型,里面对 Callback 和 duration 做了一层封装,void show(int duration, Callback callback)  中的 callback 对应的是 SnackBar 中的回调,每个都是唯一的,第一次进来,前面两个判断都不符合条件,因此会到 步骤一,把两个形参封装到 SnackbarRecord 中,赋值给 mNextSnackbar;继续往下,此时 mCurrentSnackbar 为null,所以执行 步骤三, 先把 mCurrentSnackbar 置空,然后执行 showNextSnackbarLocked() 方法

    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;
            }
        }
    }
mNextSnackbar 由步骤一的地方赋值,然后它把值赋给了 mCurrentSnackbar,同时置空 mNextSnackbar, Callback callback = mCurrentSnackbar.callback.get() 中的 callback,就是SnackBar 中的 mManagerCallback 对象

    private final SnackbarManager.Callback mManagerCallback = new SnackbarManager.Callback() {
        @Override
        public void show() {
            sHandler.sendMessage(sHandler.obtainMessage(MSG_SHOW, Snackbar.this));
        }

        @Override
        public void dismiss(int event) {
            sHandler.sendMessage(sHandler.obtainMessage(MSG_DISMISS, event, 0, Snackbar.this));
        }
    };
接着,执行 callback.show() 方法,即 mManagerCallback 中的 sHandler.sendMessage(sHandler.obtainMessage(MSG_SHOW, Snackbar.this)) 方法,此时把自己作为对象,传入Handler中,

    static {
        sHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
            @Override
            public boolean handleMessage(Message message) {
                switch (message.what) {
                    case MSG_SHOW:
                        ((Snackbar) message.obj).showView();
                        return true;
                    case MSG_DISMISS:
                        ((Snackbar) message.obj).hideView(message.arg1);
                        return true;
                }
                return false;
            }
        });
    }

这里的 sHandler 是在 static 静态代码块中生成的,是静态变量,也就是说,所有的 SnackBar 对象,共用一个 sHandler 对象,我们看看 ((Snackbar) message.obj).showView() 方法,

    final void showView() {
        if (mView.getParent() == null) {
            // 省略一
            ...
            mTargetParent.addView(mView);
        }
        ...
        if (ViewCompat.isLaidOut(mView)) {
            ...
        } else {
            mView.setOnLayoutChangeListener(new SnackbarLayout.OnLayoutChangeListener() {
                @Override
                public void onLayoutChange(View view, int left, int top, int right, int bottom) {
                    mView.setOnLayoutChangeListener(null);
                    if (shouldAnimate()) {
                        animateViewIn();
                    } else {
                        onViewShown();
                    }
                }
            });
        }
    }

以上是省略的代码,mTargetParent 就是上文中的容器,在这里它会把 SnackBar 的 view 添加到UI上,最终会执行到 setOnLayoutChangeListener() 监听回调中,最终执行 animateViewIn() 方法

    private void animateViewIn() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
            ViewCompat.setTranslationY(mView, mView.getHeight());
            ViewCompat.animate(mView)
                    .translationY(0f)
                    .setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR)
                    .setDuration(ANIMATION_DURATION)
                    .setListener(new ViewPropertyAnimatorListenerAdapter() {
                        @Override
                        public void onAnimationStart(View view) {
                            mView.animateChildrenIn(ANIMATION_DURATION - ANIMATION_FADE_DURATION,
                                    ANIMATION_FADE_DURATION);
                        }

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

                @Override
                public void onAnimationStart(Animation animation) {}

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

这里面是个位移动画,首先做了版本判断,如果大于等于14,使用属性动画,否则使用补间动画。重点看一下属性动画,第一行,先对view进行位移,调用的是 translationY() 方法来进行位移,这个是瞬间位移,然后是位移动画,动画时间是 ANIMATION_DURATION 250毫秒,它有个动画开始和结束的监听,分别执行两个方法

    void animateChildrenIn(int delay, int duration) {
        ViewCompat.setAlpha(mMessageView, 0f);
        ViewCompat.animate(mMessageView).alpha(1f).setDuration(duration)
                .setStartDelay(delay).start();

        if (mActionView.getVisibility() == VISIBLE) {
            ViewCompat.setAlpha(mActionView, 0f);
            ViewCompat.animate(mActionView).alpha(1f).setDuration(duration)
                    .setStartDelay(delay).start();
        }
    }
这里面是透明动画,对动画不了解的可以网上搜一些相关资料。

    private void onViewShown() {
        SnackbarManager.getInstance().onShown(mManagerCallback);
        if (mCallback != null) {
            mCallback.onShown(this);
        }
    }
先看后面的 mCallback 回调,看看它是哪里赋值的

    public Snackbar setCallback(Callback callback) {
        mCallback = callback;
        return this;
    }
也就是说,在创建 SnackBar 时可以设置 setCallback() 回调,当 SnackBar 显示和消失时分别触发相应的回调,这个触发的是展示回调;看看 onViewShown() 方法中第一行代码,对应
SnackbarManager 中的 onShown() 方法

    public void onShown(Callback callback) {
        synchronized (mLock) {
            if (isCurrentSnackbarLocked(callback)) {
                scheduleTimeoutLocked(mCurrentSnackbar);
            }
        }
    }

    private void scheduleTimeoutLocked(SnackbarRecord r) {
        if (r.duration == Snackbar.LENGTH_INDEFINITE) {
            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);
    }
isCurrentSnackbarLocked() 中判断传入的 callback 是否是 mCurrentSnackbar 中封装的 callback,此时它们是同一个对象,执行 scheduleTimeoutLocked() 方法,r.duration 对应的是时间的标志,我们案例中传入的是 Snackbar.LENGTH_LONG = 0,SnackBar 中还有两个常量 int LENGTH_INDEFINITE = -2,LENGTH_SHORT = -1; 有什么区别呢?继续看,判断duration如果等于LENGTH_INDEFINITE,则 return了,这说明,如果不主动触发,它是会一直这样显示下去,即为无时间限制的显示;往下,默认时间是 int durationMs = LONG_DURATION_MS; LONG_DURATION_MS = 2750,下面的if判断是控制 durationMs 的大小,如果 duration 大于0,说明是我们传入了自己自定义的时间,如果等于了-1,则选择了较短的系统时间 1500,然后是 Handler 操作,重点看mHandler.sendMessageDelayed(Message.obtain(mHandler, MSG_TIMEOUT, r), durationMs); 延迟 durationMs 时间执行,对应的是 handleTimeout((SnackbarRecord) message.obj) 方法,

    private 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) {
            mHandler.removeCallbacksAndMessages(record);
            callback.dismiss(event);
            return true;
        }
        return false;
    }

最终会执行 callback.dismiss(event) 方法, event 的值为 int DISMISS_EVENT_TIMEOUT = 2,这个执行的是 SnackBar 中的 mManagerCallback 中的 dismiss(int event) 方法,对应 sHandler 中 ((Snackbar) message.obj).hideView(message.arg1);

    final void hideView(@Callback.DismissEvent final int event) {
        if (shouldAnimate() && mView.getVisibility() == View.VISIBLE) {
            animateViewOut(event);
        } else {
            // If anims are disabled or the view isn't visible, just call back now
            onViewHidden(event);
        }
    }
执行 animateViewOut() 方法,它里面也是个动画,动画结束时最终执行 onViewHidden(event)  方法,

    private void onViewHidden(int event) {
        SnackbarManager.getInstance().onDismissed(mManagerCallback);
        if (mCallback != null) {
            mCallback.onDismissed(this, event);
        }
        final ViewParent parent = mView.getParent();
        if (parent instanceof ViewGroup) {
            ((ViewGroup) parent).removeView(mView);
        }
    }

在这里,如果 mCallback 回调有设置,触发 onDismissed() 回调,然后把 mView 从它的父控件中移除,再看看方法中第一行代码,调用了 SnackbarManager 中的 onDismissed() 方法

    public void onDismissed(Callback callback) {
        synchronized (mLock) {
            if (isCurrentSnackbarLocked(callback)) {
                mCurrentSnackbar = null;
                if (mNextSnackbar != null) {
                    showNextSnackbarLocked();
                }
            }
        }
    }
这个方法中,会执行if语句,把 mCurrentSnackbar 值置空,由于 mNextSnackbar 本身就是null,所以就不执行了。我们再回想一下 setAction() 方法中的 dispatchDismiss(Callback.DISMISS_EVENT_ACTION)

    private void dispatchDismiss(@Callback.DismissEvent int event) {
        SnackbarManager.getInstance().dismiss(mManagerCallback, event);
    }

    public void dismiss(Callback callback, int event) {
        synchronized (mLock) {
            if (isCurrentSnackbarLocked(callback)) {
                cancelSnackbarLocked(mCurrentSnackbar, event);
            } else if (isNextSnackbarLocked(callback)) {
                cancelSnackbarLocked(mNextSnackbar, event);
            }
        }
    }

    private boolean cancelSnackbarLocked(SnackbarRecord record, int event) {
        final Callback callback = record.callback.get();
        if (callback != null) {
            mHandler.removeCallbacksAndMessages(record);
            callback.dismiss(event);
            return true;
        }
        return false;
    }

发现它对应的方法,最终走到 cancelSnackbarLocked() 中,执行了 callback.dismiss(event) 回调,此时 event 值是 int DISMISS_EVENT_ACTION = 1,到此,就与上面的接轨了。
SnackBar 的添加显示流程,简单的分析了一遍,现在看看 showView() 方法中 省略一 的地方,

    final void showView() {
        if (mView.getParent() == null) {
            final ViewGroup.LayoutParams lp = mView.getLayoutParams();
            if (lp instanceof CoordinatorLayout.LayoutParams) {
                // If our LayoutParams are from a CoordinatorLayout, we'll setup our Behavior
                final Behavior behavior = new Behavior();
                behavior.setStartAlphaSwipeDistance(0.1f);
                behavior.setEndAlphaSwipeDistance(0.6f);
                behavior.setSwipeDirection(SwipeDismissBehavior.SWIPE_DIRECTION_START_TO_END);
                behavior.setListener(new SwipeDismissBehavior.OnDismissListener() {
                    @Override
                    public void onDismiss(View view) {
                        view.setVisibility(View.GONE);
                        dispatchDismiss(Callback.DISMISS_EVENT_SWIPE);
                    }

                    @Override
                    public void onDragStateChanged(int state) {
                        switch (state) {
                            case SwipeDismissBehavior.STATE_DRAGGING:
                            case SwipeDismissBehavior.STATE_SETTLING:
                                SnackbarManager.getInstance().cancelTimeout(mManagerCallback);
                                break;
                            case SwipeDismissBehavior.STATE_IDLE:
                                SnackbarManager.getInstance().restoreTimeout(mManagerCallback);
                                break;
                        }
                    }
                });
                ((CoordinatorLayout.LayoutParams) lp).setBehavior(behavior);
            }

            mTargetParent.addView(mView);
        }
        ...
    }


    final class Behavior extends SwipeDismissBehavior<SnackbarLayout> {
        @Override
        public boolean canSwipeDismissView(View child) {
            return child instanceof SnackbarLayout;
        }

        @Override
        public boolean onInterceptTouchEvent(CoordinatorLayout parent, SnackbarLayout child,
                MotionEvent event) {
            if (parent.isPointInChildBounds(child, (int) event.getX(), (int) event.getY())) {
                switch (event.getActionMasked()) {
                    case MotionEvent.ACTION_DOWN:
                        SnackbarManager.getInstance().cancelTimeout(mManagerCallback);
                        break;
                    case MotionEvent.ACTION_UP:
                    case MotionEvent.ACTION_CANCEL:
                        SnackbarManager.getInstance().restoreTimeout(mManagerCallback);
                        break;
                }
            }
            return super.onInterceptTouchEvent(parent, child, event);
        }
    }
这种情况,是我们自己 Activity 对应的 layout 布局的根节点是 CoordinatorLayout 布局的情况,这种情况,需要设置 Behavior 对象,这里设置的是继承 SwipeDismissBehavior 的对象,它 canSwipeDismissView( ) 方法中判断 child 是否是 SnackbarLayout 类型来决定是否可以滑动,onInterceptTouchEvent() 方法中是做一些触摸事件对应的方法,比如 ACTION_DOWN 时,SanckBar 本来是3秒后消失,此时手指按下,则cancelTimeout() 取消了Handler中的延迟操作,SnackBar 不会消失; ACTION_UP 或 ACTION_CANCEL 时,手指抬起来或移出SnackBar的区域时,重新恢复延迟3秒要执行的方法。 至于 behavior 中的 setListener() 监听方法,onDismiss() 是横滑移出时的回调,这时候调用 dispatchDismiss() 方法,移除 SnackBar。onDragStateChanged()是状态的改变,和 onInterceptTouchEvent() 类似。 至于 SnackBar 为什么一直是在底部出现,看看 design_layout_snackbar 中 android:layout_gravity="bottom" 这个属性就明白了。

假如连续展示两个不同的 SnackBar ,举个栗子
    Snackbar.make(view, "This is a test ", Snackbar.LENGTH_LONG).show(); // A
    Snackbar.make(view, "This is a test2 ", Snackbar.LENGTH_LONG).show();  // B
此时, SnackbarManager 中的 show(int duration, Callback callback) 方法中,看看 步骤二 的地方,即 

    public void show(int duration, Callback callback) {
        synchronized (mLock) {
            ...
                // 步骤一
                mNextSnackbar = new SnackbarRecord(duration, callback);
            }
            // 步骤二
            if (mCurrentSnackbar != null && cancelSnackbarLocked(mCurrentSnackbar,
                    Snackbar.Callback.DISMISS_EVENT_CONSECUTIVE)) {
                return;
            } else {
                // 步骤三
                mCurrentSnackbar = null;
                showNextSnackbarLocked();
            }
        }
    }
A刚show时,mNextSnackbar 为 A,此时进入步骤三,执行 showNextSnackbarLocked() 方法,此时 mCurrentSnackbar 为 A,mNextSnackbar 为 null,此时触发 callback.show() 进入 Handler的消息队列中;B执行show时,mNextSnackbar 为 B,此时进入步骤二,mCurrentSnackbar 为 A,执行 cancelSnackbarLocked() 方法 

    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;
    }
    
此时执行的也是A的  callback.dismiss(event) 方法,进入 Handler 消息队列中,此时 Handler 中有两个消息,分别执行 showView() 和 hideView() 方法,看到这就明白了,如果 A 和 B 之间间隔1秒,那么A刚出现1秒,马上消失, 执行到 onViewHidden() 方法,此时的SnackBar 是 A,注意 SnackbarManager.getInstance().onDismissed(mManagerCallback); 这行代码

    public void onDismissed(Callback callback) {
        synchronized (mLock) {
            if (isCurrentSnackbarLocked(callback)) {
                mCurrentSnackbar = null;
                if (mNextSnackbar != null) {
                    showNextSnackbarLocked();
                }
            }
        }
    }
此时,mCurrentSnackbar 为 A, mNextSnackbar 为 B,进入if判断,mCurrentSnackbar 置空,mNextSnackbar 为B不为null,执行 showNextSnackbarLocked() 方法,上文分析过了,这是新展示一个 SanckBar,下面流程之前都分析过了。

假如有三个或四个SanckBar一起show(),且命名为 ABCD,每个都设置了 setCallback() 回调监听 onShown() 和 onDismissed() 方法,会发现除了第四个D回调时正常的,前面三个SnackBar的回调,是第一个 SnackBar 即 A 的 onDismissed() 方法执行了三次,B 和 C 的回调根本没执行,原因还在 show(int duration, Callback callback) 中的步骤二,由于是在UI线程中同时调用了 ABCD 四个SanckBar 的show()方法,mNextSnackbar 值依次为 A、B、C、D,mCurrentSnackbar 则一直是 A, 由于是四个show(),所以步骤二被执行了三次,并且这三次 mCurrentSnackbar 一直是 A,故此 A 的 setCallback() 被执行了三次,根本没有 B 和 C 出厂的份,我现在的源码时版本23的,也不知道最新版本中有没有修复这个问题,或者说是故意这样设计的?

发布了176 篇原创文章 · 获赞 11 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/Deaht_Huimie/article/details/98106420
今日推荐