Snackbar源碼分析

目錄介紹

  • 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顏色php

    • 能夠看到先是獲取父佈局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()方法的實現android

    • 首先是獲取父佈局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.關於個人博客

相關文章
相關標籤/搜索