View的绘制(2)-SnackBar源码解析

作者: ZJ_Rocky | 来源:发表于2017-09-05 13:24 被阅读178次

    主目录见:Android高级进阶知识(这是总目录索引)

    一.目标

    首先我们来明确一下这次源码解析的目标:
     1.巩固上一篇《View的绘制(1)-setContentView源码分析》的源码机制.
     2.同时为下一篇《利用decorView机制实现底部弹出框》做准备.

    二.SnackBar源码分析

    1.SnackBar的基本使用

    1)只显示文本:

    Snackbar.make(view, "This is a message", Snackbar.LENGTH_LONG).show();
    

    2)有点击按钮:

    Snackbar.make(view, "This is a message", Snackbar.LENGTH_LONG)
         .setAction("UNDO", new View.OnClickListener() {
             @Override
             public void onClick(View v) {
                 //TODO do something
             }
         })
         .show();
    

    这两个就是SnackBar的基本使用,其他的使用方式可以查看文档,在这里不是重点,最后我们放上一张上篇分析源码得出的结论图,在这里会用到,以此来镇贴

    布局.png

    2.make 方法(注意:这里我的源代码版本是android-25)

    我们遵循一贯查看源码的套路,从第一个使用到的方法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;
        }
    

    方法很简单,这里有个关键方法是findSuitableParent(view)【这个方法很重要!!!】,这个方法的参数是我们传进来的视图,那他的作用是啥呢?我们跟进这个方法瞅瞅:

     private static ViewGroup findSuitableParent(View view) {
            ViewGroup fallback = null;
            do {
                if (view instanceof CoordinatorLayout) {
    //如果找到的父节点是CoordinatorLayout则返回这个父节点
                    // We've found a CoordinatorLayout, use it
                    return (ViewGroup) view;
                } else if (view instanceof FrameLayout) {
    //如果找到的id为content的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);//循环向上遍历
            return fallback;
        }
    

    这个方法里面的 if (view.getId() == android.R.id.content)用到的知识就是我们上次分析setContentView得出的结论,我们的视图是放在id为Content的Framelayout中即如下图,重要的事情贴两遍

    布局.png
    到这里我们的父视图已经找到,后面我们自己的视图会添加到父视图下面。然后我们跟进SnackBar的构造方法里。

    3.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);
        }
    

    我们看到源码里面会先调用ThemeUtils.checkAppCompatTheme(mContext);来检查主题,具体怎么检查这里不深究。我们直接看到下面一句会inflate一个design_layout_snackbar的layout来得到SnackBarLayout(这里的inflate方法干了什么在上一篇setContentView源码分析中有说过),那我们关注下两个东西:
    1)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"//这个地方设置为bottom,所以我们的snackBar会在底部
          style="@style/Widget.Design.Snackbar" />
    

    我们看到view标签里面有class="android.support.design.widget.Snackbar$SnackbarLayout"
    说明这个view对应的布局就是SanckBarLayout,所以我们直接就看SnackBar的内部类SnackbarLayout是个啥:
    2)SnackBarLayout

     public static class SnackbarLayout extends LinearLayout {
    }
    

    看到这里顿时豁然开朗,原来inflate的这个视图是个LinearLayout呀。一万只草泥马奔腾而过.....

    拉风草泥马.jpg
    那接下来我们分部分来看SnackBarLayout的构造函数,看看这家伙干了些神马事:
    2.1)第一部分是去获取属性,大家看代码应该是老友了
          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);
    

    2.2)然后就是我们的主要方法了,这里会去加载布局design_layout_snackbar_include布局

              // 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.
    //睁大眼睛认真看!!!!!!,这里加载了的layout作为linearlayout的布局
                LayoutInflater.from(context).inflate(R.layout.design_layout_snackbar_include, this);
    //底下省略一些代码
    ..................
                        return insets;
                    }
                });
    

    所以我们顺其自然地去看这个布局到底是何方神圣:

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

    这个就是我们snackBar的主布局了,一个TextView一个Button,是不是到现在明白了为啥snackbar长那样:

    SnackBar.png
    这里做个总结:我们的make方法会根据用户传进去的锚点view进行查找父视图(CoordinateLayout或者id为content的framelayout),然后往父视图添加SnackBarLayout这个LinearLayout.

    4.show方法

    现在我们分析完make方法,我们就继续分析我们的show方法了。

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

    头一热,倒地休息五分钟......这里怎么又蹦出SnackbarManager和mManagerCallback这个未知生物。What a fucking source code!!!!
    吐槽完默默继续,我们看下mManagerCallback是个什么东西:

     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));
            }
        };
    

    原来这个是一个回调,显示和隐藏,同时我们看到show和dismiss方法里面分别往Handler里面发送一个信息。我们直接跳到Handler里面看做了些啥动作:

     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;
                }
            });
    

    我们看到Handler里面又调用了SnackBar类的showView和hideView方法,我们继续转到showView方法:

        final void showView() {
    //首先判断SnackbarLayout没有挂到其他的父视图上面
            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 CoordinatorLayout.LayoutParams clp = (CoordinatorLayout.LayoutParams) lp;
    //新建一个Behavior,有用过MD库的人都知道这个Behavior,主要是配合CoordinateLayout使用,在以后的文章会重点介绍
                    final Behavior behavior = new Behavior();
                    behavior.setStartAlphaSwipeDistance(0.1f);
                    behavior.setEndAlphaSwipeDistance(0.6f);
                    behavior.setSwipeDirection(SwipeDismissBehavior.SWIPE_DIRECTION_START_TO_END);
    //设置一个SwipeDismissBehavior,用来滑动删除
                    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:
                                    // 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;
                            }
                        }
                    });
                    clp.setBehavior(behavior);
                    // Also set the inset edge so that views can dodge the snackbar correctly
                    clp.insetEdge = Gravity.BOTTOM;
                }
    //这个地方是重点mTargetParent就是我们刚才用锚点View查找到的父视图
                mTargetParent.addView(mView);
            }
    //省略一些代码
          .....................
            if (ViewCompat.isLaidOut(mView)) {
                if (shouldAnimate()) {
                    // If animations are enabled, animate it in
                    animateViewIn();
                } else {
                    // Else if anims are disabled just call back now
                    onViewShown();
                }
            } else {
                // Otherwise, add one of our layout change listeners and show it in when laid out
                mView.setOnLayoutChangeListener(new SnackbarLayout.OnLayoutChangeListener() {
                    @Override
                    public void onLayoutChange(View view, int left, int top, int right, int bottom) {
                        mView.setOnLayoutChangeListener(null);
    //判断是否进行动画显示或者不需要
                        if (shouldAnimate()) {
                            // If animations are enabled, animate it in
                            animateViewIn();
                        } else {
                            // Else if anims are disabled just call back now
                            onViewShown();
                        }
                    }
                });
            }
        }
    

    到这里我们已经把我们的SnackBar显示出来了,关键代码就是将视图添加进父视图Id为content的FrameLayout里面或者是CoordinateLayout里面(mTargetParent.addView(mView);)。然后就会判断需不需要有动画效果显示即 if (shouldAnimate()) {}.

    5.SnackbarManager show方法

    上面我们已经看完mManagerCallback 是啥了,我们是时候来看看SnackbarManager 的show方法了。首先我们看下SnackBarManager的getInstance():

        static SnackbarManager getInstance() {
            if (sSnackbarManager == null) {
                sSnackbarManager = new SnackbarManager();
            }
            return sSnackbarManager;
        }
    

    其实就是个单例,我们就不去说明单例模式了,我们直接看show方法吧:

     public void show(int duration, Callback callback) {
    //这个地方加了个同步代码块
            synchronized (mLock) {
    //这个地方判断是不是就是目前的SnackBar
                if (isCurrentSnackbarLocked(callback)) {
                    // Means that the callback is already in the queue. We'll just update the duration
    //如果要显示的snackBar已经在显示队列里面则更新duration
                    mCurrentSnackbar.duration = duration;
    
                    // If this is the Snackbar currently being shown, call re-schedule it's
                    // timeout//移除Callback,避免内存泄露
                    mHandler.removeCallbacksAndMessages(mCurrentSnackbar);
    //);//重新关联设置duration和Callback
                    scheduleTimeoutLocked(mCurrentSnackbar);
                    return;
                } else if (isNextSnackbarLocked(callback)) {
    // //判断是否是接下来要显示的Snackbar,是则更新duration
                    // 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)) {
    //取消当前的snackbar显示
                    // 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
    //显示我们的snackBar
                    showNextSnackbarLocked();
                }
            }
        }
    

    从显示的代码中可以知道当目前的mCurrentSnackbar不为空的话,则后面显示的snackBar都会存储在mNextSnackbar中,只有当当前显示的Snackbar duration到了后,调用onDismissed方法,清空mCurrentSnackbar,然后才会显示下一个Snackbar。其中onDismissed方法就是在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;
        }
    

    dismiss完之后会把视图从父视图中删除。如果当前的snackBar为空则就显示我们新创建的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;
                }
            }
        }
    

    到这里我们的snackBar源码已经分析完成,希望在下一篇我们能找到感觉。


    放飞自我.jpg

    相关文章

      网友评论

        本文标题:View的绘制(2)-SnackBar源码解析

        本文链接:https://www.haomeiwen.com/subject/wuhujxtx.html