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

主目录见: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的基本使用,其他的使用方式可以查看文档,在这里不是重点,最后我们放上一张上篇分析源码得出的结论图,在这里会用到,以此来镇贴

View的绘制(2)-SnackBar源码解析_第1张图片
布局.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中即如下图,重要的事情贴两遍

View的绘制(2)-SnackBar源码解析_第2张图片
布局.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标签里面有class="android.support.design.widget.Snackbar$SnackbarLayout"
说明这个view对应的布局就是SanckBarLayout,所以我们直接就看SnackBar的内部类SnackbarLayout是个啥:
2)SnackBarLayout

 public static class SnackbarLayout extends LinearLayout {
}

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

View的绘制(2)-SnackBar源码解析_第3张图片
拉风草泥马.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 
            // 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;
                }
            });

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


 

    

这个就是我们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源码已经分析完成,希望在下一篇我们能找到感觉。


View的绘制(2)-SnackBar源码解析_第4张图片
放飞自我.jpg

你可能感兴趣的:(View的绘制(2)-SnackBar源码解析)