Snackbar原始碼分析

楊充發表於2019-03-04

目錄介紹

  • 1.最簡單創造方法
    • 1.1 Snackbar作用
    • 1.2 最簡單的建立
    • 1.3 Snackbar消失的幾種方式
  • 2.原始碼分析
    • 2.1 Snackbar的make方法原始碼分析
    • 2.2 對Snackbar屬性進行設定
    • 2.3 Snackbar的show顯示與點選消失
    • 2.4 顯示和隱藏中動畫原始碼分析
  • 3.經典總結
    • 3.1 Snackbar和SnackbarManager類的設計
  • 4.思考問題分析
    • 4.1 Snackbar的設計思路
    • 4.2 什麼時候Snackbar顯示會導致FloatingActionButton上移
    • 4.3 Snackbar控制元件show時為何從下往上移出來
    • 4.4 為什麼Snackbar總是顯示在最下面
    • 4.5 Snackbar與吐司有何區別
  • 5.Snackbar封裝庫

好訊息

  • 部落格筆記大彙總【16年3月到至今】,包括Java基礎及深入知識點,Android技術部落格,Python學習筆記等等,還包括平時開發中遇到的bug彙總,當然也在工作之餘收集了大量的面試題,長期更新維護並且修正,持續完善……開源的檔案是markdown格式的!同時也開源了生活部落格,從12年起,積累共計47篇[近20萬字],轉載請註明出處,謝謝!
  • 連結地址:github.com/yangchong21…
  • 如果覺得好,可以star一下,謝謝!當然也歡迎提出建議,萬事起於忽微,量變引起質變!
  • Snackbar封裝庫專案地址:github.com/yangchong21…
  • 02.Toast原始碼深度分析
    • 最簡單的建立,簡單改造避免重複建立,show()方法原始碼分析,scheduleTimeoutLocked吐司如何自動銷燬的,TN類中的訊息機制是如何執行的,普通應用的Toast顯示數量是有限制的,用程式碼解釋為何Activity銷燬後Toast仍會顯示,Toast偶爾報錯Unable to add window是如何產生的,Toast執行在子執行緒問題,Toast如何新增系統視窗的許可權等等
  • 03.DialogFragment原始碼分析
    • 最簡單的使用方法,onCreate(@Nullable Bundle savedInstanceState)原始碼分析,重點分析彈窗展示和銷燬原始碼,使用中show()方法遇到的IllegalStateException分析
  • 05.PopupWindow原始碼分析
    • 顯示PopupWindow,注意問題寬和高屬性,showAsDropDown()原始碼,dismiss()原始碼分析,PopupWindow和Dialog有什麼區別?為何彈窗點選一下就dismiss呢?
  • 06.Snackbar原始碼分析
    • 最簡單的建立,Snackbar的make方法原始碼分析,Snackbar的show顯示與點選消失原始碼分析,顯示和隱藏中動畫原始碼分析,Snackbar的設計思路,為什麼Snackbar總是顯示在最下面
  • 07.彈窗常見問題
    • DialogFragment使用中show()方法遇到的IllegalStateException,什麼常見產生的?Toast偶爾報錯Unable to add window,Toast執行在子執行緒導致崩潰如何解決?

1.最簡單創造方法

1.1 Snackbar作用

  • Snackbar是Android支援庫中用於顯示簡單訊息並且提供和使用者的一個簡單操作的一種彈出式提醒。當使用Snackbar時,提示會出現在訊息最底部,通常含有一段資訊和一個可點選的按鈕。
  • 同樣作為訊息提示,Snackbar相比於Toast而言,增加了一個使用者操作,並且在同時彈出多個訊息時,Snackbar會停止前一個,直接顯示後一個,也就是說同一時刻只會有一個Snackbar在顯示;而Toast則不然,如果不做特殊處理,那麼同時可以有多個Toast出現;Snackbar相比於Dialog,操作更少,因為只有一個使用者操作的介面,而Dialog最多可以設定三個,另外Snackbar的出現並不影響使用者的繼續操作,而Dialog則必須需要使用者做出響應,所以相比Dialog,Snackbar更輕量。

1.2 最簡單的建立

  • 如下所示
Snackbar sb = Snackbar.make(v,"瀟湘劍雨",Snackbar.LENGTH_LONG)
        .setAction("刪除嗎?", new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //點選了"是嗎?"字串操作
                ToastUtils.showRoundRectToast("逗比");
            }
        })
        .setActionTextColor(Color.RED)
        .setText("楊充是個逗比")
        .addCallback(new BaseTransientBottomBar.BaseCallback<Snackbar>() {
            @Override
            public void onDismissed(Snackbar transientBottomBar, int event) {
                super.onDismissed(transientBottomBar, event);
                switch (event) {
                    case Snackbar.Callback.DISMISS_EVENT_CONSECUTIVE:
                    case Snackbar.Callback.DISMISS_EVENT_MANUAL:
                    case Snackbar.Callback.DISMISS_EVENT_SWIPE:
                    case Snackbar.Callback.DISMISS_EVENT_TIMEOUT:
                        ToastUtils.showRoundRectToast("刪除成功");
                        break;
                    case Snackbar.Callback.DISMISS_EVENT_ACTION:
                        ToastUtils.showRoundRectToast("撤銷了刪除操作");
                        break;
                }
                Log.d("MainActivity","onDismissed");
            }
            @Override
            public void onShown(Snackbar transientBottomBar) {
                super.onShown(transientBottomBar);
                Log.d("MainActivity","onShown");
            }
        });
sb.show();
複製程式碼

1.3 Snackbar消失的幾種方式

  • Snackbar顯示只有一種方式,那就是呼叫show()方法,但是消失有幾種方式:時間到了自動消失、點選了右側按鈕消失、新的Snackbar出現導致舊的Snackbar消失、滑動消失或者通過呼叫dismiss()消失。
    • 分別對應於Snackbar.Callback中的幾個常量值。
      • DISMISS_EVENT_ACTION:點選了右側按鈕導致消失
      • DISMISS_EVENT_CONSECUTIVE:新的Snackbar出現導致舊的消失
      • DISMISS_EVENT_MANUAL:呼叫了dismiss方法導致消失
      • DISMISS_EVENT_SWIPE:滑動導致消失
      • DISMISS_EVENT_TIMEOUT:設定的顯示時間到了導致消失
    • Callback有兩個方法
      • void onDismissed(B transientBottomBar, @DismissEvent int event)
      • void onShown(B transientBottomBar)
      • 其中onShown在Snackbar可見時呼叫,onDismissed在Snackbar準備消失時呼叫。

2.原始碼分析

2.1 Snackbar的make方法原始碼分析

  • 建立Snackbar需要使用靜態的make方法,並且其中的view引數是一個查詢父佈局的起點
    • 這裡可以看到,snackBar的佈局是design_layout_snackbar_include,假如我們需要自定義SnackBar並且設定字型顏色,大小等屬性。則需要拿到這個佈局的控制元件id等。關於封裝庫,可以檢視:github.com/yangchong21…
    • image
  • 其中findSuitableParent()方法為以view為起點尋找合適的父佈局,下面看看findSuitableParent()如何做的?
    • 看了下面原始碼可知:可以看到如果view是CoordinatorLayout,那麼就直接作為父佈局了;如果是FrameLayout,並且如果是android.R.id.content,也就是查詢到了DecorView,即最頂部,那麼就只用這個view;如果不是的話,先儲存下來;接下來就是獲取view的父佈局,然後迴圈再次判斷。這樣導致的結果最終會有兩個選擇,要麼是CoordinatorLayout,要麼就是FrameLayout,並且是最頂層的那個佈局。
    • 如果從View往上搜尋,如果有CoordinatorLayout,那麼就使用該CoordinatorLayout ;如果從View往上搜尋,沒有CoordinatorLayout,那麼就使用android.R.id.content的FrameLayout
    • image

2.2 對Snackbar屬性進行設定

  • 2.2.1 setActionTextColor設定action顏色

    • 可以看到先是獲取父佈局contentLayout,然後在獲取snackbar_action的mActionView
    @NonNull
    public Snackbar setActionTextColor(@ColorInt int color) {
        final SnackbarContentLayout contentLayout = (SnackbarContentLayout) mView.getChildAt(0);
        final TextView tv = contentLayout.getActionView();
        tv.setTextColor(color);
        return this;
    }
    
    //然後看SnackbarContentLayout類中getActionView方法
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mMessageView = (TextView) findViewById(R.id.snackbar_text);
        mActionView = (Button) findViewById(R.id.snackbar_action);
    }
    public Button getActionView() {
        return mActionView;
    }
    複製程式碼
  • 2.2.2 看setAction()方法的實現

    • 首先是獲取父佈局contentLayout,然後通過contentLayout呼叫getActionView()方法,返回的tv其實就是右邊的Button,然後判斷文字和監聽器,設定可見性、文字、監聽器。
    @NonNull
    public Snackbar setAction(CharSequence text, final View.OnClickListener listener) {
        final SnackbarContentLayout contentLayout = (SnackbarContentLayout) mView.getChildAt(0);
        final TextView tv = contentLayout.getActionView();
    
        if (TextUtils.isEmpty(text) || listener == null) {
            tv.setVisibility(View.GONE);
            tv.setOnClickListener(null);
        } else {
            tv.setVisibility(View.VISIBLE);
            tv.setText(text);
            tv.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    listener.onClick(view);
                    // Now dismiss the Snackbar
                    dispatchDismiss(BaseCallback.DISMISS_EVENT_ACTION);
                }
            });
        }
        return this;
    }
    複製程式碼

2.3 Snackbar的show顯示與點選消失

  • 2.3.1 show顯示
    • 可以看到,首先獲取一個SnackbarManager物件,然後呼叫它的show方法。可以看到在這個方法中,先判斷如果是當前正在顯示的SnackBar對應的CallBack,則更新顯示時長,然後從訊息佇列中移除,最後呼叫scheduleTimeoutLocked方法傳送定時訊息dismiss;如果是下一個要顯示的,則更新顯示時長;如果都不是,那麼就建立一個SnackbarRecord物件。
    • isCurrentSnackbarLocked:如果當前已經有一個Snackbar顯示了,又再呼叫了該物件的show方法,但是隻是設定了不同時間,那麼isCurrentSnackbarLocked就會是true,執行裡面的方法。
    • isNextSnackbarLocked:如果當前已有一個Snackbar正在顯示,又建立了一個新的Snackbar並呼叫show方法,則執行這個條件程式碼
    • 如果兩條件都不成立,則需要建立一個新記錄並對其進行排隊。
    public void show() {
        SnackbarManager.getInstance().show(mDuration, mManagerCallback);
    }
    
    public void show(int duration, Callback callback) {
        synchronized (mLock) {
            if (isCurrentSnackbarLocked(callback)) {
                // 表示回撥已在佇列中。我們只需更新持續時間
                mCurrentSnackbar.duration = duration;
    
                // 如果這是當前正在顯示的Snackbar,請呼叫重新排程它的
                // timeout
                mHandler.removeCallbacksAndMessages(mCurrentSnackbar);
                // 這個方法很重要,當執行時間結束後,就會自動dismiss。下面再詳細分析
                scheduleTimeoutLocked(mCurrentSnackbar);
                return;
            } else if (isNextSnackbarLocked(callback)) {
                //我們只需更新持續時間
                mNextSnackbar.duration = duration;
            } else {
                //否則,我們需要建立一個新記錄並對其進行排隊。
                mNextSnackbar = new SnackbarRecord(duration, callback);
            }
            if (mCurrentSnackbar != null && cancelSnackbarLocked(mCurrentSnackbar,Snackbar.Callback.DISMISS_EVENT_CONSECUTIVE)) {
                // 如果我們目前有一個Snackbar,請嘗試取消它並排隊等待。
                return;
            } else {
                // 清除當前的快捷鍵
                mCurrentSnackbar = null;
                //很重要
                showNextSnackbarLocked();
            }
        }
    }
    
    //注意這個callback方法
    final SnackbarManager.Callback mManagerCallback = new SnackbarManager.Callback() {
        @Override
        public void show() {
            sHandler.sendMessage(sHandler.obtainMessage(MSG_SHOW, BaseTransientBottomBar.this));
        }
    
        @Override
        public void dismiss(int event) {
            sHandler.sendMessage(sHandler.obtainMessage(MSG_DISMISS, event, 0,
                    BaseTransientBottomBar.this));
        }
    };
    
    //處理sHandler傳送的訊息
    static {
        sHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
            @Override
            public boolean handleMessage(Message message) {
                switch (message.what) {
                    case MSG_SHOW:
                        ((BaseTransientBottomBar) message.obj).showView();
                        return true;
                    case MSG_DISMISS:
                        ((BaseTransientBottomBar) message.obj).hideView(message.arg1);
                        return true;
                }
                return false;
            }
        });
    }
    複製程式碼
    • 然後看看showNextSnackbarLocked這個方法,注意:mCurrentSnackbar當前正在顯示的,而mNextSnackbar是下一個要顯示的。能看到會呼叫callback的show方法,而這個calllback物件就是我們在呼叫snackbar的show方法是傳進去的那個。向Snackbar的Handler傳送一個訊息,最後顯示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;
            }
        }
    }
    複製程式碼
  • 2.3.2 看看scheduleTimeoutLocked原始碼如何銷燬snackBar
    • 可以發現,如果我們設定為無限期,則不會設定超時,直接return函式。然後傳送了一個叫做MSG_TIMEOUT的訊息,繼續追終,最後會到達cancelSnackbarLocked方法。在cancelSnackbarLocked這個方法中,首先移除SnackbarRecord發出的所有訊息,然後呼叫Callback的dismiss方法,從上面我們知道最終是向Snackbar的sHandler傳送了一條訊息,最終是呼叫Snackbar的hideView消失。
    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);
    }
    
    //接受mHandler訊息並且處理
    mHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
        @Override
        public boolean handleMessage(Message message) {
            switch (message.what) {
                case MSG_TIMEOUT:
                    handleTimeout((SnackbarRecord) message.obj);
                    return true;
            }
            return false;
        }
    });
    
    //
    void handleTimeout(SnackbarRecord record) {
        synchronized (mLock) {
            if (mCurrentSnackbar == record || mNextSnackbar == record) {
                cancelSnackbarLocked(record, Snackbar.Callback.DISMISS_EVENT_TIMEOUT);
            }
        }
    }
    
    //最終可以追蹤到這個方法
    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;
    }
    複製程式碼

2.4 顯示和隱藏中動畫原始碼分析

  • 在顯示的時候是這樣設定動畫的,具體如下所示
    • image
  • 在隱藏的時候是這樣設定動畫的,具體如下所示
    • image
  • 最後具體看一下animateViewOut部分原始碼
    • 可以看到在動畫結束的最後都呼叫了onViewHidden方法,所以最終都是要呼叫onViewHidden方法的。
    private void animateViewOut(final int event) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
            ViewCompat.animate(mView)
                    .translationY(mView.getHeight())
                    .setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR)
                    .setDuration(ANIMATION_DURATION)
                    .setListener(new ViewPropertyAnimatorListenerAdapter() {
                        @Override
                        public void onAnimationStart(View view) {
                            mContentViewCallback.animateContentOut(0, ANIMATION_FADE_DURATION);
                        }
    
                        @Override
                        public void onAnimationEnd(View view) {
                            onViewHidden(event);
                        }
                    }).start();
        } else {
            Animation anim = AnimationUtils.loadAnimation(mView.getContext(),
                    R.anim.design_snackbar_out);
            anim.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
            anim.setDuration(ANIMATION_DURATION);
            anim.setAnimationListener(new Animation.AnimationListener() {
                @Override
                public void onAnimationEnd(Animation animation) {
                    onViewHidden(event);
                }
    
                @Override
                public void onAnimationStart(Animation animation) {}
    
                @Override
                public void onAnimationRepeat(Animation animation) {}
            });
            mView.startAnimation(anim);
        }
    }
    複製程式碼
  • onViewHidden提供具體的業務處理,具體如下所示
    • 首先呼叫SnackbarManager的onDismissed方法,然後判斷Snackbar.Callback是不是null,呼叫Snackbar.Callback的onDismissed方法,就是我們上面介紹的處理Snackbar消失的方法。最後就是將Snackbar的mView移除。
    • image

3.經典總結

3.1 Snackbar和SnackbarManager類的設計

  • Snackbar和SnackbarManager,SnackbarManager內部有兩個SnackbarRecord,一個mCurrentSnackbar,一個mNextSnackbar,SnackbarManager通過這兩個物件實現Snackbar的順序顯示,如果在一個Snackbar顯示之前有Snackbar正在顯示,那麼使用mNextSnackbar儲存第二個Snackbar,然後讓第一個Snackbar消失,然後消失之後再呼叫SnackbarManager顯示下一個Snackbar,如此迴圈,實現了Snackbar的順序顯示。
  • Snackbar負責顯示和消失,具體來說其實就是新增和移除View的過程。Snackbar和SnackbarManager的設計很巧妙,利用一個SnackbarRecord物件儲存Snackbar的顯示時間以及SnackbarManager.Callback物件,前面說到每一個Snackbar都有一個叫做mManagerCallback的SnackbarManager.Callback物件,下面看一下SnackRecord類的定義:
    • image
  • Snackbar向SnackbarManager傳送訊息主要是呼叫SnackbarManager.getInstace()返回一個單例物件;而SnackManager向Snackbar傳送訊息就是通過show方法傳入的Callback物件。SnackbarManager中的Handler只處理一個MSG_TIMEOUT事件,最後是呼叫Snackbar的hideView消失的;Snackbar的sHandler處理兩個訊息,showView和hideView,而訊息的傳送者是mManagerCallback,控制者是SnackbarManager。

4.思考問題分析

4.1 Snackbar的設計思路

  • 具體可以看經典總結3.1

4.2 什麼時候Snackbar顯示會導致FloatingActionButton上移

  • 為什麼CoordinatorLayout + FloatingActionButton,當Snackbar顯示的時候FloatingActionButton會上移呢,這個是怎麼實現的?
    • 把CoordinatorLayout替換成FrameLayout確不行。這個問題我們還沒說。其實這個不是在Snackbar裡面處理的,是通過CoordinatorLayout和Behavior來處理的。那具體的處理在哪裡呢。FloatingActionButton類裡面Behavior類。正是Behavior裡面的兩個函式layoutDependsOn()和onDependentViewChanged()函式作用的結果。直接進去看下FloatingActionButton內部類Behavior裡面這兩個函式的程式碼。

4.3 Snackbar控制元件show時為何從下往上移出來

  • 至於說Snackbar控制元件show時為何從下往上移出來,看下面這段程式碼就知道呢,如下所示
    • image

4.4 為什麼Snackbar總是顯示在最下面

  • 直接找到make方法中的填充佈局,然後去看design_layout_snackbar_include的佈局引數,結果如下:
    • image

4.5 Snackbar與吐司有何區別

  • 與Toast進行比較,SnackBar有優勢:
    • 1.SnackBar可以自動消失,也可以手動取消(側滑取消,但是需要在特殊的佈局中,後面會仔細說)
    • 2.SnackBar可以通過setAction()來與使用者進行互動
    • 3.通過CallBack我們可以獲取SnackBar的狀態
    • image

5.Snackbar封裝庫

  • 可以一行程式碼呼叫,也可以自己使用鏈式程式設計呼叫。支援設定顯示時長屬性;可以設定背景色;可以設定文字大小,顏色;可以設定action內容,文字大小,顏色,還有點選事件;可以設定icon;程式碼如下所示,更多內容可以直接執行demo哦!
    //1.只設定text
    SnackBarUtils.showSnackBar(this,"滾犢子");
    
    //2.設定text,action,和點選事件
    SnackBarUtils.showSnackBar(this, "滾犢子", "ACTION", new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            ToastUtils.showRoundRectToast("滾犢子啦?");
        }
    });
    
    //3.設定text,action,和點選事件,和icon
    SnackBarUtils.showSnackBar(this, "滾犢子", "ACTION",R.drawable.icon_cancel, new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            ToastUtils.showRoundRectToast("滾犢子啦?");
        }
    });
    
    //4.鏈式呼叫
    SnackBarUtils.builder()
        .setBackgroundColor(this.getResources().getColor(R.color.color_7f000000))
        .setTextSize(14)
        .setTextColor(this.getResources().getColor(R.color.white))
        .setTextTypefaceStyle(Typeface.BOLD)
        .setText("滾犢子")
        .setMaxLines(4)
        .centerText()
        .setActionText("收到")
        .setActionTextColor(this.getResources().getColor(R.color.color_f25057))
        .setActionTextSize(16)
        .setActionTextTypefaceStyle(Typeface.BOLD)
        .setActionClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ToastUtils.showRoundRectToast("滾犢子啦?");
            }
        })
        .setIcon(R.drawable.icon_cancel)
        .setActivity(MainActivity.this)
        .setDuration(SnackBarUtils.DurationType.LENGTH_INDEFINITE)
        .build()
        .show();
    複製程式碼

關於其他內容介紹

01.關於部落格彙總連結

02.關於我的部落格

相關文章