詳解MaterialDesign Snackbar

鋸齒流沙發表於2018-01-08

簡介

Snackbar是design包下的一個特色控制元件,Android5.0新增。

Snackbar.png

Snackbars provide lightweight feedback about an operation. They show a brief message at the bottom of the screen on mobile and lower left on larger devices. Snackbars appear above all other elements on screen and only one can be displayed at a time.

根據Android API介紹:Snackbars 是提供有關操作的輕量級反饋。它在移動裝置的螢幕底部顯示簡短的資訊,並在較大的裝置上顯示左下角。 Snackbars顯示在螢幕上的所有其他元素上方,並且一次只能顯示一個。

使用

SnackbarActivity:

public class SnackbarActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_snackbar);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);


        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Snackbar snb = Snackbar.make(view, "this is my message", Snackbar.LENGTH_SHORT);
                snb.setAction("button", new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        Toast.makeText(SnackbarActivity.this, "toast", Toast.LENGTH_SHORT).show();
                    }
                });
                snb.setActionTextColor(0xffffffff);
                snb.show();

                //不使用action事件
//                Snackbar.make(view, "this is my message", Snackbar.LENGTH_SHORT).show();

            }
        });
    }

}
複製程式碼

使用Snackbar.make函式構建例項,直接然後呼叫show函式即可,這是最簡單的用法,如果想要喝Snackbar有互動的,需要呼叫setAction方法,為button設定點選事件。

Snackbar為開發者提供很多方法:

Snackbar.png

如:

setActionTextColor:設定button文字顏色 setText:設定提示文字

原始碼解析

首先看make函式

Snackbar.png

先呼叫findSuitableParent(view)方法:該方法就是一直尋找根View,首先優先判斷View是不是CoordinatorLayout型別的,如果是就直接返回。否則,最終會找到的是頂層的View,如果setContentView進來的是一個FrameLayout,則返回contentView 否則就會是頂層的decorView。

Snackbar.png

然後初始化佈局SnackbarContentLayout和Snackbar,並且把佈局傳入Snackbar裡,同時設定顯示的文字和時間。

SnackbarContentLayout佈局:

Snackbar.png

SnackbarContentLayout是繼承LinearLayout的自定義佈局,並且實現了動畫的介面ContentViewCallback。SnackbarContentLayout控制元件只有兩個子控制元件,也就是在Snackbar顯示出來的TextView和Button。

SnackbarContentLayout實現的動畫:

Snackbar.png

BaseTransientBottomBar.ContentViewCallback介面

Snackbar.png

從以上的SnackbarContentLayout控制元件知道,該控制元件就是show顯示出來的佈局,並且該控制元件實現了ContentViewCallback的介面,該介面就是控制元件顯示和隱藏的時的動畫介面。

下面我們看會make方法: make方法中new了一個Snackbar物件,而且是使用final修飾符。

final Snackbar snackbar = new Snackbar(parent, content, content);

一個final變數,如果是基本資料型別的變數,則其數值一旦在初始化之後便不能更改;如果是引用型別的變數,則在對其初始化之後便不能再讓其指向另一個物件。

確保snackbar只有一個物件。

接下來看下new Snackbar所做的事情:Snackbar是繼承BaseTransientBottomBar這個抽象類的,最後會呼叫BaseTransientBottomBar的構造方法。

BaseTransientBottomBar的構造方法:

 protected BaseTransientBottomBar(@NonNull ViewGroup parent, @NonNull View content,
            @NonNull ContentViewCallback contentViewCallback) {
        if (parent == null) {
            throw new IllegalArgumentException("Transient bottom bar must have non-null parent");
        }
        if (content == null) {
            throw new IllegalArgumentException("Transient bottom bar must have non-null content");
        }
        if (contentViewCallback == null) {
            throw new IllegalArgumentException("Transient bottom bar must have non-null callback");
        }

        mTargetParent = parent;
        mContentViewCallback = contentViewCallback;
        mContext = parent.getContext();

        ThemeUtils.checkAppCompatTheme(mContext);

        LayoutInflater inflater = LayoutInflater.from(mContext);
        // Note that for backwards compatibility reasons we inflate a layout that is defined
        // in the extending Snackbar class. This is to prevent breakage of apps that have custom
        // coordinator layout behaviors that depend on that layout.
        mView = (SnackbarBaseLayout) inflater.inflate(
                R.layout.design_layout_snackbar, mTargetParent, false);
        mView.addView(content);

        ViewCompat.setAccessibilityLiveRegion(mView,
                ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE);
        ViewCompat.setImportantForAccessibility(mView,
                ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);

        // Make sure that we fit system windows and have a listener to apply any insets
        ViewCompat.setFitsSystemWindows(mView, true);
        ViewCompat.setOnApplyWindowInsetsListener(mView,
                new android.support.v4.view.OnApplyWindowInsetsListener() {
                    @Override
                    public WindowInsetsCompat onApplyWindowInsets(View v,
                            WindowInsetsCompat insets) {
                        // Copy over the bottom inset as padding so that we're displayed
                        // above the navigation bar
                        v.setPadding(v.getPaddingLeft(), v.getPaddingTop(),
                                v.getPaddingRight(), insets.getSystemWindowInsetBottom());
                        return insets;
                    }
                });

        mAccessibilityManager = (AccessibilityManager)
                mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
    }
複製程式碼

該構造方法主要做的事情:記錄在make方法中通過findSuitableParent(view)找到的根View也就是parent,以及記錄內容佈局mContnet和回撥mContentViewCallback。還把content新增到mView中,用mView包裝content。最後獲取AccessibilityManager服務,關於AccessibilityManager服務的可以參考這篇文章《你真的理解AccessibilityService嗎》

整個Snackbar初始化完成之後,接下來我們看下setAction方法的實現。

setAction的實現

Snackbar.png

改方法實現比較簡單,上面我們介紹過SnackbarContentLayout控制元件,該控制元件裡面就是包含一個TextView和一個Button,這裡直接給Button點選事件,然後回撥傳入的listener的onClick方法。最後呼叫dispatchDismiss()方法,該方法就是dismiss掉Snackbar。這樣就達到點選button的時候和使用者互動,接著就隱藏了Snackbar。

dispatchDismiss方法

上面我們說過該方法主要是dismiss掉Snackbar,下面我們看看是不是這樣子的呢。

Snackbar.png

呼叫了SnackbarManager的dismiss方法。

dismiss

Snackbar.png

這裡判斷是否是當前正在執行的callback和下一個callback。最終呼叫cancelSnackbarLocked方法。

Snackbar.png

最後還是呼叫了callba的dismiss方法,而這個callback就是在SnackbarManager的dismiss方法中傳入的mManagerCallback。我們看看mManagerCallback是如何實現的。

Snackbar.png

顯然這就是在主執行緒中顯示和隱藏Snackbar。

Snackbar.png

呼叫了hideView

Snackbar.png

如果mView顯示中的話需要呼叫animateViewOut動畫來隱藏,否則直接呼叫onViewHidden來hide即可。

其實animateViewOut動畫結束後還是呼叫了onViewHidden方法。

Snackbar.png

最後呼叫了removeView,將mView移除了。完成了整個將View隱藏。

show()

Snackbar是如何顯示的呢,我們看下show()方法。

Snackbar.png

呼叫了SnackbarManager的show方法。

Snackbar.png

這裡逐一判斷傳入的callback是否是當前正在執行,是否是下一個callback。這裡我們看下showNextSnackbarLocked()方法。

Snackbar.png

和dismiss一樣,都是呼叫了傳入的callback,這裡呼叫了傳入的callback的show方法。這個callback也就是mManagerCallback。我們看下sHandler的showView。

final void showView() {
        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;

                final Behavior behavior = new Behavior();
                behavior.setStartAlphaSwipeDistance(0.1f);
                behavior.setEndAlphaSwipeDistance(0.6f);
                behavior.setSwipeDirection(SwipeDismissBehavior.SWIPE_DIRECTION_START_TO_END);
                behavior.setListener(new SwipeDismissBehavior.OnDismissListener() {
                    @Override
                    public void onDismiss(View view) {
                        view.setVisibility(View.GONE);
                        dispatchDismiss(BaseCallback.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, pause the timeout
                                SnackbarManager.getInstance().pauseTimeout(mManagerCallback);
                                break;
                            case SwipeDismissBehavior.STATE_IDLE:
                                // If the view has been released and is idle, restore the timeout
                                SnackbarManager.getInstance()
                                        .restoreTimeoutIfPaused(mManagerCallback);
                                break;
                        }
                    }
                });
                clp.setBehavior(behavior);
                // Also set the inset edge so that views can dodge the bar correctly
                clp.insetEdge = Gravity.BOTTOM;
            }

            mTargetParent.addView(mView);
        }

        mView.setOnAttachStateChangeListener(
                new BaseTransientBottomBar.OnAttachStateChangeListener() {
                @Override
                public void onViewAttachedToWindow(View v) {}

                @Override
                public void onViewDetachedFromWindow(View v) {
                    if (isShownOrQueued()) {
                        // If we haven't already been dismissed then this event is coming from a
                        // non-user initiated action. Hence we need to make sure that we callback
                        // and keep our state up to date. We need to post the call since
                        // removeView() will call through to onDetachedFromWindow and thus overflow.
                        sHandler.post(new Runnable() {
                            @Override
                            public void run() {
                                onViewHidden(BaseCallback.DISMISS_EVENT_MANUAL);
                            }
                        });
                    }
                }
            });

        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 BaseTransientBottomBar.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();
                    }
                }
            });
        }
    }
複製程式碼

先判斷是否是CoordinatorLayout,然後初始化Behavior,這裡對CoordinatorLayout和Behavior不做過多的介紹,接著呼叫的是mTargetParent.addView。

Snackbar.png

最後呼叫顯示的動畫,這樣完成了顯示流程。

Snackbar高階使用

public class SnackbarActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_snackbar);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);


        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Snackbar snb = Snackbar.make(view, "this is my message", Snackbar.LENGTH_SHORT);
                snb.setAction("button", new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        Toast.makeText(SnackbarActivity.this, "toast", Toast.LENGTH_SHORT).show();
                    }
                });
                snb.setActionTextColor(0xffffffff);
                snb.show();

                setSnackbarBgColor(snb, 0xffff0000, 0xff02f78e);

                addView(SnackbarActivity.this, snb, R.layout.header_layout, 0);

                //不使用action事件
//                Snackbar.make(view, "this is my message", Snackbar.LENGTH_SHORT).show();

            }
        });
    }


    /**
     * 新增背景和文字顏色
     *
     * @param snb
     * @param textColor
     * @param bgColor
     */
    public void setSnackbarBgColor(Snackbar snb, int textColor, int bgColor) {
        //獲取Snackbar的view
        View view = snb.getView();
        if (view != null) {
            //修改view的背景色
            view.setBackgroundColor(bgColor);
            //獲取Snackbar的message控制元件,修改字型顏色
            ((TextView) view.findViewById(R.id.snackbar_text)).setTextColor(textColor);
        }
    }


    /**
     * 新增View
     * @param context
     * @param snb
     * @param layoutId
     * @param index
     */
    public void addView(Context context, Snackbar snb, int layoutId, int index) {

        View snbView = snb.getView();

        Snackbar.SnackbarLayout snbLayout = (Snackbar.SnackbarLayout) snbView;

        SnackbarContentLayout layout = (SnackbarContentLayout) snbLayout.getChildAt(0);

        View mAddView = LayoutInflater.from(context).inflate(layoutId, null);

        LinearLayout.LayoutParams fp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);

        fp.gravity = Gravity.CENTER_VERTICAL;

        layout.addView(mAddView, index, fp);

    }

}
複製程式碼

通過前面的原始碼分析,我們知道其佈局是SnackbarContentLayout(包含一個TextView和一個Button),我們可以新增背景顏色和文字顏色,還可以新增View。

執行效果:

Snackbar.png

相關文章