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也分析完了哦。结束了哦。

时间: 2024-08-03 17:30:15

Snackbar源码分析的相关文章

轻量级控件SnackBar应用&amp;源码分析

前言 SnackBar是Android Support Design Library库支持的一个控件,它在使用的时候经常和CoordinatorLayout一起使用,它是介于Toast和Dialog之间的产物,属于轻量级控件很方便的提供提示和动作反馈,有时候我们需要这样的控件,和Toast一样显示便可以消失,又想这个消息提示上进行用户的反馈.然而写Dialog只能通过点击去取消它,所以SnackBar的出现更加让界面优雅. Part 1.SnackBar的常规使用 Snackbar snackb

DialogFragment源码分析

目录介绍 1.最简单的使用方法 1.1 官方建议 1.2 最简单的使用方法 1.3 DialogFragment做屏幕适配 2.源码分析 2.1 DialogFragment继承Fragment 2.2 onCreate(@Nullable Bundle savedInstanceState)源码分析 2.3 setStyle(@DialogStyle int style, @StyleRes int theme) 2.4 onActivityCreated(Bundle savedInstan

Dialog源码分析

目录介绍 1.简单用法 2.AlertDialog源码分析 2.1 AlertDialog.Builder的构造方法 2.2 通过AlertDialog.Builder对象设置属性 2.3 builder.create方法 2.4 看看create方法中的P.apply(dialog.mAlert)源码 2.5 看看AlertDialog的show方法 3.Dialog源码分析 3.1 Dialog的构造方法 3.2 Dialog生命周期 3.3 Dialog中show方法展示弹窗 3.4 Di

PopupWindow源码分析

目录介绍 1.最简单的创建方法 1.1 PopupWindow构造方法 1.2 显示PopupWindow 1.3 最简单的创建 1.4 注意问题宽和高属性 2.源码分析 2.1 setContentView(View contentView) 2.2 showAsDropDown()源码 2.3 dismiss()源码分析 2.4 PopupDecorView源码分析 3.经典总结 3.1 PopupWindow和Dialog有什么区别? 3.2 创建和销毁的大概流程 3.3 为何弹窗点击一下

深度理解Android InstantRun原理以及源码分析

深度理解Android InstantRun原理以及源码分析 @Author 莫川 Instant Run官方介绍 简单介绍一下Instant Run,它是Android Studio2.0以后新增的一个运行机制,能够显著减少你第二次及以后的构建和部署时间.简单通俗的解释就是,当你在Android Studio中改了你的代码,Instant Run可以很快的让你看到你修改的效果.而在没有Instant Run之前,你的一个小小的修改,都肯能需要几十秒甚至更长的等待才能看到修改后的效果. 传统的代

TeamTalk源码分析之login_server

login_server是TeamTalk的登录服务器,负责分配一个负载较小的MsgServer给客户端使用,按照新版TeamTalk完整部署教程来配置的话,login_server的服务端口就是8080,客户端登录服务器地址配置如下(这里是win版本客户端): 1.login_server启动流程 login_server的启动是从login_server.cpp中的main函数开始的,login_server.cpp所在工程路径为server\src\login_server.下表是logi

Android触摸屏事件派发机制详解与源码分析二(ViewGroup篇)

1 背景 还记得前一篇<Android触摸屏事件派发机制详解与源码分析一(View篇)>中关于透过源码继续进阶实例验证模块中存在的点击Button却触发了LinearLayout的事件疑惑吗?当时说了,在那一篇咱们只讨论View的触摸事件派发机制,这个疑惑留在了这一篇解释,也就是ViewGroup的事件派发机制. PS:阅读本篇前建议先查看前一篇<Android触摸屏事件派发机制详解与源码分析一(View篇)>,这一篇承接上一篇. 关于View与ViewGroup的区别在前一篇的A

HashMap与TreeMap源码分析

1. 引言     在红黑树--算法导论(15)中学习了红黑树的原理.本来打算自己来试着实现一下,然而在看了JDK(1.8.0)TreeMap的源码后恍然发现原来它就是利用红黑树实现的(很惭愧学了Java这么久,也写过一些小项目,也使用过TreeMap无数次,但到现在才明白它的实现原理).因此本着"不要重复造轮子"的思想,就用这篇博客来记录分析TreeMap源码的过程,也顺便瞅一瞅HashMap. 2. 继承结构 (1) 继承结构 下面是HashMap与TreeMap的继承结构: pu

Linux内核源码分析--内核启动之(5)Image内核启动(rest_init函数)(Linux-3.0 ARMv7)【转】

原文地址:Linux内核源码分析--内核启动之(5)Image内核启动(rest_init函数)(Linux-3.0 ARMv7) 作者:tekkamanninja 转自:http://blog.chinaunix.net/uid-25909619-id-4938395.html 前面粗略分析start_kernel函数,此函数中基本上是对内存管理和各子系统的数据结构初始化.在内核初始化函数start_kernel执行到最后,就是调用rest_init函数,这个函数的主要使命就是创建并启动内核线