Snackbar源码分析

       Snackbar相信大家都用过,其实最初我好奇的是为什么CoordinatorLayout + FloatingActionButton 显示Snackbar的时候FloatingActionButton位置会往上移。带着这个疑问才去看的CoordinatorLayout和Behavior和Snackbar的大概的实现过程的。
CoordinatorLayout和Behavior的简单解释可以看看CoordinatorLayout里Behavior简单分析

言归正传我们这里要说道的是Snackbar的简单分析。让我们带着三个问题进入Snackbar的分析。
1. 为什么Snackbar总是显示在最下面。
2. 为什么Snackbar显示的时候是从下往上移出来的。消失的时候是从上往下出去的。
3. 为什么CoordinatorLayout + FloatingActionButton Snackbar显示的时候FloatingActionButton会上移。把CoordinatorLayout替换成FrameLayout确不行。

Snackbar的使用方法,我们一般是先调用了make,然后调用了setAction,最后调用了show。我们就按照我们使用的流程一步一步的来进行。

        Snackbar.make(mShowSnack, "Snack show", Snackbar.LENGTH_LONG).setAction("Action", new View.OnClickListener() {
            @Override
            public void onClick(View v) {

            }
        }).show();
一, Snackbar make() 方法
    @NonNull
    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;
    }

    @NonNull
    public static Snackbar make(@NonNull View view, @StringRes int resId, @Duration int duration) {
        return make(view, view.getResources().getText(resId), duration);
    }

注意是static方法,不管调用的是哪个make最后调用的都会走到第一个make,构造出一个Snackbar对象,参数是findSuitableParent函数,先看findSuitableParent()函数的作用,然后再看Snackbar构造函数里面做的具体事情。

findSuitableParent()函数

    private static ViewGroup findSuitableParent(View view) {
        ViewGroup fallback = null;
        do {
            if (view instanceof CoordinatorLayout) {
                // We've found a CoordinatorLayout, use it
                return (ViewGroup) view;
            } else if (view instanceof FrameLayout) {
                if (view.getId() == android.R.id.content) {
                    // If we've hit the decor content view, then we didn't find a CoL in the
                    // hierarchy, so use it.
                    return (ViewGroup) view;
                } else {
                    // It's not the content view but we'll use it as our fallback
                    fallback = (ViewGroup) view;
                }
            }

            if (view != null) {
                // Else, we will loop and crawl up the view hierarchy and try to find a parent
                final ViewParent parent = view.getParent();
                view = parent instanceof View ? (View) parent : null;
            }
        } while (view != null);

        // If we reach here then we didn't find a CoL or a suitable content view so we'll fallback
        return fallback;
    }

findSuitableParent函数干的事情就是根据make函数传递进来的view,找到距离view最近的CoordinatorLayout,或者找到离根布局最近的FrameLayout。然后把他们返回给Snackbar的构造函数。

Snackbar构造函数

    private Snackbar(ViewGroup parent) {
        mParent = parent;
        mContext = parent.getContext();

        LayoutInflater inflater = LayoutInflater.from(mContext);
        mView = (SnackbarLayout) inflater.inflate(R.layout.design_layout_snackbar, mParent, false);
    }

把findSuitableParent到的CoordinatorLayout或者是FrameLayout赋值给了mParent。
最后一行mView(SnackbarLayout 这个mView就是我们Snackbar要显示的View了,在后面这个mView是会被加入到mParent里面去显示的) inflater的是R.layout.design_layout_snackbar,不管三七二十一进去看下。

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

第5行layout_gravity 指定了SnackbarLayout的位置是在底部,这样为什么Snackbar总是显示在最下面的原因我们找到了。第一个问题的原因找到了哦。
第6行style=”@style/Widget.Design.Snackbar”里面给定了SnackbarLayout的一些配置,在SnackbarLayout里面measure的时候会用到。
那mView里面到底放了些什么了呢,这下就该看SnackbarLayout了,SnackbarLayout是extends LinearLayout的ViewGroup,直接去看构造函数了

    public SnackbarLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SnackbarLayout);
        mMaxWidth = a.getDimensionPixelSize(R.styleable.SnackbarLayout_android_maxWidth, -1);
        mMaxInlineActionWidth = a.getDimensionPixelSize(
            R.styleable.SnackbarLayout_maxActionInlineWidth, -1);
        if (a.hasValue(R.styleable.SnackbarLayout_elevation)) {
            ViewCompat.setElevation(this, a.getDimensionPixelSize(
                R.styleable.SnackbarLayout_elevation, 0));
        }
        a.recycle();

        setClickable(true);

        // Now inflate our content. We need to do this manually rather than using an <include>
        // in the layout since older versions of the Android do not inflate includes with
        // the correct Context.
        LayoutInflater.from(context).inflate(R.layout.design_layout_snackbar_include, this);
    }

最后一行R.layout.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"/>

    <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>

可以看到SnackbarLayout里面就两个东西一个TextView(mMessageView) 一个 Button(mActionView)。
SnackbarLayout里面其他的东西我们就不看了,onMeasure里面会根据一些长度的大小去判断这两个View是横向还是纵向显示。其他的函数可能就是在Snackbar显示或者dismmis的过程中的动画效果了。

到这里我们知道Snackbar对应的view其实就是一个SnackbarLayout并且他是继承自LinearLayout的,后面肯定是要把这个view加到parent里面让他显示出来的。

二, Snackbar setAction() 方法

Snackbar的setAction方法就直接跳过了,这个好像也没什么看的。就是设置text 设置listener了。对应SnackbarLayout里面view的一些设置。

二, Snackbar show() 方法

在看Snackbar show()方法之前我们先看下mManagerCallback,因为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));
        }
    };

里面就两个方法来控制Snackbar的显示和消失。具体的调用会在SnackbarManager里面调用。show()和dismiss()两个方法,里面都是发送了一个message消息,找到对应的handlerMessage()的地方,发现最后调用的分别是showView()和hideView()两个函数。我们就看showView()函数的具体过程,hideView()函数就是反着来的一个显示一个隐藏,一个显示的时候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) {
                        dispatchDismiss(Callback.DISMISS_EVENT_SWIPE);
                    }

                    @Override
                    public void onDragStateChanged(int state) {
                        switch (state) {
                            case SwipeDismissBehavior.STATE_DRAGGING:
                            case SwipeDismissBehavior.STATE_SETTLING:
                                // If the view is being dragged or settling, cancel the timeout
                                SnackbarManager.getInstance().cancelTimeout(mManagerCallback);
                                break;
                            case SwipeDismissBehavior.STATE_IDLE:
                                // If the view has been released and is idle, restore the timeout
                                SnackbarManager.getInstance().restoreTimeout(mManagerCallback);
                                break;
                        }
                    }
                });
                ((CoordinatorLayout.LayoutParams) lp).setBehavior(behavior);
            }

            mParent.addView(mView);
        }

        if (ViewCompat.isLaidOut(mView)) {
            // If the view is already laid out, animate it now
            animateViewIn();
        } else {
            // Otherwise, add one of our layout change listeners and animate it in when laid out
            mView.setOnLayoutChangeListener(new SnackbarLayout.OnLayoutChangeListener() {
                @Override
                public void onLayoutChange(View view, int left, int top, int right, int bottom) {
                    animateViewIn();
                    mView.setOnLayoutChangeListener(null);
                }
            });
        }
    }

第5行,看的出来如果mView对应的ViewGroup.LayoutParams是CoordinatorLayout.LayoutParams话设置了SwipeDismissBehavior。关于SwipeDismissBehavior的使用可以稍微看Behavior子类SwipeDismissBehavior简单分析。CoordinatorLayout情况下根据SwipeDismissBehavior的作用可以指定Snackbar是可以随着手指往左滑出去的。
第36行,mParent.addView(mView); 加到mParent里面去了,这样Snackbar就会显示在下面了。
第41行,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) {
                            if (mCallback != null) {
                                mCallback.onShown(Snackbar.this);
                            }
                            SnackbarManager.getInstance().onShown(mManagerCallback);
                        }
                    }).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) {
                    if (mCallback != null) {
                        mCallback.onShown(Snackbar.this);
                    }
                    SnackbarManager.getInstance().onShown(mManagerCallback);
                }

                @Override
                public void onAnimationStart(Animation animation) {}

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

就看第一个if吧,
第3行,先把mView往下移动mView.getHeight()的具体 正好看不到了。
第4 ~ 21行,设置mView显示出来的动画了,这也就是我们之前说的第二个问题为什么显示的时候是慢慢移出来显示的效果了。这里有一点主要就是在动画结束的时候会调用SnackbarManager.getInstance().onShown(mManagerCallback);告诉SnackbarManager已经显示出来的改启动timeout(Snackbar的显示时间)了。
总结下 SnackbarManager.Callback mManagerCallback做的事情
1. 控制Snackbar的显示和消失,在显示的时候是有一个动画效果慢慢的往上显示的。消失的时候慢慢的往下消失的。
并且某个父布局写的是CoordinatorLayout的时候Snackbar是可以通过手指往右滑动消失掉的。
2. mManagerCallback里面的两个函数的调用都是在SnackbarManager里面调用的(下面会讲到)。
3. 当Snackbar显示完或者消失完都要告诉SnackbarManager一声。

Snackbar show()函数。
    public void show() {
        SnackbarManager.getInstance().show(mDuration, mManagerCallback);
    }

要到SnackbarManager里面去了,大概扫一下里面应该是通过handler sendmessagedelay的方式来控制Snackbar的显示时间的。SnackbarManager里面会调用mManagerCallback的show()和dismiss()函数的。所有这里就把显示时间和mManagerCallback都传进去了。直接去看SnackbarManager的show()函数了。

    public void show(int duration, Callback callback) {
        synchronized (mLock) {
            if (isCurrentSnackbar(callback)) {
                // Means that the callback is already in the queue. We'll just update the duration
                mCurrentSnackbar.duration = duration;

                // If this is the Snackbar currently being shown, call re-schedule it's
                // timeout
                mHandler.removeCallbacksAndMessages(mCurrentSnackbar);
                scheduleTimeoutLocked(mCurrentSnackbar);
                return;
            } else if (isNextSnackbar(callback)) {
                // We'll just update the duration
                mNextSnackbar.duration = duration;
            } else {
                // Else, we need to create a new record and queue it
                mNextSnackbar = new SnackbarRecord(duration, callback);
            }

            if (mCurrentSnackbar != null && cancelSnackbarLocked(mCurrentSnackbar,
                    Snackbar.Callback.DISMISS_EVENT_CONSECUTIVE)) {
                // If we currently have a Snackbar, try and cancel it and wait in line
                return;
            } else {
                // Clear out the current snackbar
                mCurrentSnackbar = null;
                // Otherwise, just show it now
                showNextSnackbarLocked();
            }
        }
    }

第一次进来的时候直接到了第17行mNextSnackbar = new SnackbarRecord(duration, callback); new了一个SnackbarRecord赋值个了mNextSnackbar,然后到了第28行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;
            }
        }
    }

第8行 callback.show(); 和我们前面提到的Snackbar类里面的mManagerCallback就接上了,跟着调用的就是Snackbar里面的showView()。这样Snackbar就显示出来了。这里还有 一点要注意在Snackbar显示出来动画结束的时候调用了SnackbarManager.getInstance().onShown(mManagerCallback); 这样有回到了SnackbarManager类里面

    public void onShown(Callback callback) {
        synchronized (mLock) {
            if (isCurrentSnackbar(callback)) {
                scheduleTimeoutLocked(mCurrentSnackbar);
            }
        }
    }
    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);
    }

最后一行看到sendMessageDelayed就知道是用来控制Snackbar的显示时间的了。

到此Snackbar的整个流程就分析完了(只是分析了第一次的显示和消失的过程)。

到了这里之前提到的三个问题我们回答出来了第一个和第二个。但是为什么CoordinatorLayout + FloatingActionButton Snackbar显示的时候FloatingActionButton会上移。把CoordinatorLayout替换成FrameLayout确不行。 这个问题我们还没说。其实这个不是在Snackbar里面处理的,是通过CoordinatorLayout和Behavior来处理的。关于具体的情况可以看下CoordinatorLayout里Behavior简单分析
那具体的处理在哪里呢。FloatingActionButton类里面Behavior类。正是Behavior里面的两个函数layoutDependsOn()和onDependentViewChanged()函数作用的结果。直接进去看下FloatingActionButton内部类Behavior里面这两个函数的代码

        @Override
        public boolean layoutDependsOn(CoordinatorLayout parent,
                FloatingActionButton child, View dependency) {
            // We're dependent on all SnackbarLayouts (if enabled)
            return SNACKBAR_BEHAVIOR_ENABLED && dependency instanceof Snackbar.SnackbarLayout;
        }

        @Override
        public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child,
                View dependency) {
            if (dependency instanceof Snackbar.SnackbarLayout) {
                updateFabTranslationForSnackbar(parent, child, dependency);
            } else if (dependency instanceof AppBarLayout) {
                // If we're depending on an AppBarLayout we will show/hide it automatically
                // if the FAB is anchored to the AppBarLayout
                updateFabVisibility(parent, (AppBarLayout) dependency, child);
            }
            return false;
        }

layoutDependsOn()函数 : dependency instanceof Snackbar.SnackbarLayout 看到FloatingActionButton的变化会依赖Snackbar.SnackbarLayout的变化。当SnackbarLayout位置变化的时候会调用到onDependentViewChanged()函数里面去。
onDependentViewChanged()函数:updateFabTranslationForSnackbar(parent, child, dependency);根据Snackbar.SnackbarLayout移动的距离来调整FloatingActionButton的距离。关于CoordinatorLayout和Behavior是怎么工作的可以看下CoordinatorLayout里Behavior简单分析

到此三个问题都解决了,Snackbar也分析完了哦。结束了哦。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值