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"http://這個地方設置為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
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容