SnackBar的使用详解及源码分析

SnackBar的使用详解及源码分析

简介

SnackBar的官方API文档

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.

Snackbars can contain an action which is set via setAction(CharSequence, android.view.View.OnClickListener).

To be notified when a snackbar has been shown or dismissed, you can provide a Snackbar.Callback via addCallback(BaseCallback).

Snackbar是类似Toast的东西,使用方式也类似,但是Snackbar提供了一个轻量级的操作按钮。可以实现类似对话框的效果。从底部弹出,同一时间只允许一个弹出显示,同时添加了监听SnackBar出现和消失的回调.

Material Design关于Snackbar的介绍

根据google提出的Material Design设计理念,SnackBar适用于action在0-1个的情况下。

简单使用

compile 'com.android.support:design:26.0.0-alpha1'

Snackbar.make(btnClick, "哈哈哈哈", Snackbar.LENGTH_LONG)
                        .setAction("取消", new View.OnClickListener() {
                            @Override
                            public void onClick(View v) {
                                Toast.makeText(SnackBarActivity.this, "取消", Toast.LENGTH_SHORT).show();
                            }
                        })
                        .setActionTextColor(Color.WHITE)
                        .addCallback(new Snackbar.Callback(){
                            @Override
                            public void onShown(Snackbar sb) {
                                Toast.makeText(SnackBarActivity.this, "onShown", Toast.LENGTH_SHORT).show();
                            }

                            @Override
                            public void onDismissed(Snackbar transientBottomBar, int event) {
                                Toast.makeText(SnackBarActivity.this, "onDismissed", Toast.LENGTH_SHORT).show();
                            }
                        })
                        .show();

Snackbar的使用方式和Toast类似,一些常用方法:

  • make(@NonNull View view, @NonNull CharSequence text,@Duration int duration)
 /**
     *
     * @param view     The view to find a parent from. 寻找合适父布局的起点
     * @param text     The text to show.  Can be formatted text. 显示的文本
     * @param duration How long to display the message.  Either {@link #LENGTH_SHORT} or {@link
     *                 #LENGTH_LONG} 还有一种LENGTH_INDEFINITE(一直显示,触发action点击事件或者有另外一个snackbar显示时消失)
     */
  • setAction:设置动作
  • setActionTextColor:设置动作文本的颜色
  • addCallback:添加消失和显示的监听

源码解析

 @NonNull
    public static Snackbar make(@NonNull View view, @NonNull CharSequence text,
            @Duration int duration) {
            //寻找父布局
        final ViewGroup parent = findSuitableParent(view);
        if (parent == null) {
            throw new IllegalArgumentException("No suitable parent found from the given view. "
                    + "Please provide a valid view.");
        }

        final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
        final SnackbarContentLayout content =
                (SnackbarContentLayout) inflater.inflate(
                        R.layout.design_layout_snackbar_include, parent, false);
        final Snackbar snackbar = new Snackbar(parent, content, content);
        snackbar.setText(text);
        snackbar.setDuration(duration);
        return snackbar;
    }

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) {
                if (view.getId() == android.R.id.content) {//如果是查找到DecorView,即最顶层的view,则直接使用它
                    // 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 {//如果不是DecorView,则先保存下来
                    // It's not the content view but we'll use it as our fallback
                    fallback = (ViewGroup) view;
                }
            }

            if (view != null) {//不是DecorView的情况下则继续往上寻找getParent
                // 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);//不是DecorView的情况下,一直循环直到找到最上层的view

        // If we reach here then we didn't find a CoL or a suitable content view so we'll fallback
        return fallback;
    }

final SnackbarContentLayout content =(SnackbarContentLayout) inflater.inflate(R.layout.design_layout_snackbar_include, parent, false);//创建一个内容布局,其中的design_layout_snackbar_include布局如下:

<view
    xmlns:android="http://schemas.android.com/apk/res/android"
    class="android.support.design.internal.SnackbarContentLayout"
    android:theme="@style/ThemeOverlay.AppCompat.Dark"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom">

    <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"
        android:textAlignment="viewStart"/>

    <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:minWidth="48dp"
        android:visibility="gone"
        android:textColor="?attr/colorAccent"
        style="?attr/borderlessButtonStyle"/>

</view>

由上面的布局可以分析出,如果需要自定义实现一些自定义效果,例如改变弹出框的底色或则加载不同的弹出界面,则可以从这入手。
获取到snackbar的布局即SnackbarContentLayout,然后可以根据这两个控件的id来设置一些样式,或者改变这个view的背景。

然后再到这里:

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

private Snackbar(ViewGroup parent, View content, ContentViewCallback contentViewCallback) {
        super(parent, content, contentViewCallback);//调用父类(BaseTransientBottomBar<Snackbar> )的构造方法
    }


    /**
         * Constructor for the transient bottom bar.
         *
         * @param parent The parent for this transient bottom bar.//根布局
         * @param content The content view for this transient bottom bar.//snackbar的内容布局
         * @param contentViewCallback The content view callback for this transient bottom bar.//内容布局的回调
         */
        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);//获取snackbar的父容器
            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);
        }

        R.layout.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"//这便是snackbar从底部弹出的缘故,如果需要从其他地方弹出,比如从底部弹出,则可以修改此处
              android:theme="@style/ThemeOverlay.AppCompat.Dark"
              style="@style/Widget.Design.Snackbar" />

这样snackbar就被创建了。

接下来看一下snackbar设置的方法:

@NonNull
    public Snackbar setActionTextColor(@ColorInt int color) {
        final SnackbarContentLayout contentLayout = (SnackbarContentLayout) mView.getChildAt(0);
        final TextView tv = contentLayout.getActionView();
        tv.setTextColor(color);
        return this;
    }


 /**
      * Set the action to be displayed in this {@link BaseTransientBottomBar}.
      *
      * @param text     Text to display for the action
      * @param listener callback to be invoked when the action is clicked
      */
     @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);//调用父类BaseTransientBottomBar里的方法
                 }
             });
         }
         return this;
     }


     void dispatchDismiss(@BaseCallback.DismissEvent int event) {
             SnackbarManager.getInstance().dismiss(mManagerCallback, event);
         }

SnackbarManager是关于snackbar的一个管理类,负责显示,隐藏,回调,管理snackbar的队列这一些功能

显示方法(SnackbarManager):

 private SnackbarManager() {
        mLock = new Object();
        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;
            }
        });
    }


public void show(int duration, Callback callback) {
        synchronized (mLock) {//加锁,保证只有一个snackbar显示
            if (isCurrentSnackbarLocked(callback)) {//是否是当前的snackbar
                // Means that the callback is already in the queue. We'll just update the duration
                mCurrentSnackbar.duration = duration;

                // If this is the Snackbar currently being shown, call re-schedule it's
                // timeout
                mHandler.removeCallbacksAndMessages(mCurrentSnackbar);//如果这是当前在显示的snackbar,因为重新设置了mCurrentSnackbar.duration,移除掉之前的mCurrentSnackbar
                scheduleTimeoutLocked(mCurrentSnackbar);//把这个重新设置时间的mCurrentSnackbar提交上去
                return;
            } else if (isNextSnackbarLocked(callback)) {//是否是下一个snackbar
                // 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);//新建一个snackbar和队列
            }

            if (mCurrentSnackbar != null && cancelSnackbarLocked(mCurrentSnackbar,
                    Snackbar.Callback.DISMISS_EVENT_CONSECUTIVE)) {
                // If we currently have a Snackbar, try and cancel it and wait in line
                return;
            } else {//第一次显示时 mCurrentSnackbar = null
                // Clear out the current snackbar
                mCurrentSnackbar = null;
                // Otherwise, just show it now
                showNextSnackbarLocked();//把下一个snackbar移到队列当前,并显示
            }
        }
    }

    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);
        }

    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;
               }
           }
       }

    private static class SnackbarRecord {
            final WeakReference<Callback> callback;
            int duration;
            boolean paused;

            SnackbarRecord(int duration, Callback callback) {
                this.callback = new WeakReference<>(callback);
                this.duration = duration;
            }

            boolean isSnackbar(Callback callback) {
                return callback != null && this.callback.get() == callback;
            }
        }

callback.show();回调在BaseTransientBottomBar实现了显示snackbar的回调:

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));
        }
    };

  由当前类BaseTransientBottomBar的sHandler发送消息处理显示隐藏
  static {//静态代码块,类一加载就初始化这个handler
          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;
              }
          });
      }

最后调用showView方法,显示snackbar:

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();
                    }
                }
            });
        }
    }

    最后是执行onViewShown(已经显示)方法:
    void onViewShown() {
            SnackbarManager.getInstance().onShown(mManagerCallback);
            if (mCallbacks != null) {
                // Notify the callbacks. Do that from the end of the list so that if a callback
                // removes itself as the result of being called, it won't mess up with our iteration
                int callbackCount = mCallbacks.size();
                for (int i = callbackCount - 1; i >= 0; i--) {
                    mCallbacks.get(i).onShown((B) this);
                }
            }
        }

        SnackbarManager的onShown
    /**
         * Should be called when a Snackbar is being shown. This is after any entrance animation has
         * finished.
         */
        public void onShown(Callback callback) {
            synchronized (mLock) {
                if (isCurrentSnackbarLocked(callback)) {
                    scheduleTimeoutLocked(mCurrentSnackbar);//延时处理
                }
            }
        }

以上便是整个的显示流程。

Snackbar通过mManagerCallback向SnackbarManager发送消息,
Snackbar的sHandler负责处理两个消息,showView和hideView。SnackbarManager起到一个控制作用。

Github示例代码

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

推荐阅读更多精彩内容