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

布局.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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容