Android开发——Snackbar使用详解

Snackbar是Android支持库中用于显示简单消息并且提供和用户的一个简单操作的一种弹出式提醒。当使用Snackbar时,提示会出现在消息最底部,通常含有一段信息和一个可点击的按钮。下图是Gmail中删除一封邮件时弹出的Snackbar:
Gmail中删除邮件时弹出的Snackbar
在上图中,最下方的黑色区域,包含左边文字和右边"撤销"字样的就是Snackbar。Snackbar在显示一段时间后就会自动消失。同样作为消息提示,Snackbar相比于Toast而言,增加了一个用户操作,并且在同时弹出多个消息时,Snackbar会停止前一个,直接显示后一个,也就是说同一时刻只会有一个Snackbar在显示;而Toast则不然,如果不做特殊处理,那么同时可以有多个Toast出现;Snackbar相比于Dialog,操作更少,因为只有一个用户操作的接口,而Dialog最多可以设置三个,另外Snackbar的出现并不影响用户的继续操作,而Dialog则必须需要用户做出响应,所以相比Dialog,Snackbar更轻量。
经过上面的比较,可以看出Snackbar可以用于显示用户信息并且该信息不需要用户立即做出反馈的时候。

一、如何使用Snackbar?

Snackbar没有公有的构造方法,但是提供了静态方法make方法:

static Snackbar	make(View view, CharSequence text, int duration)

static Snackbar	make(View view, int resId, int duration)

其中view参数是用于查找合适父布局的一个起点,下面分析源码的时候会解释到。如果父布局是一个CoordinatorLayout,那么Snackbar还会有别的一些特性:可以滑动消除;并且如果有FloatingActionButton时,会将FloatingActionButton上移,而不会挡住Snackbar的显示。

1.1、父布局不是CoordinatorLayout

在创建了一个Snackbar对象后,可以调用一些set**方法进行设置,其中setAction()方法用于设置右侧的文字显示以及点击事件,setCallback()方法用于设置一个状态回调,在Snackbar显示和消失的时候会触发方法。下面是一段创建Snackbar的代码:

 Snackbar.make(view, "已删除一个会话", Snackbar.LENGTH_SHORT)
                .setAction("撤销", new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {

                        Toast.makeText(Main2Activity.this, "撤销了删除", Toast.LENGTH_SHORT).show();

                    }
                }).show();

以上代码在一个按钮的点击事件中创建一个Snackbar并显示,内容模仿上面的Gmail例子,并且给“撤销”一个点击事件,只是简单的显示一个Toast。Activity的根布局是一个RelativeLayout,并且下部有一个FloatingActionButton,在Snackbar出现后,可以看到Snackbar遮挡了FlaotingActionButton的一部分,具体效果如下:
父布局RelativeLayout,遮挡FloatingActionButton

1.2、父布局是CoordinatorLayout

在父布局不是CoordinatorLayout的情况下,如果有FloaingActionButton,那么弹出的Snackbar会遮挡FloatingActionButton,为了解决这个问题,可以将父布局改成CoordinatorLayout,并且这会带来一个新特性,就是Snackbar可以通过右滑消失。代码一样,只是布局不同。直接看效果图:
父布局CoordinatorLayout,不遮挡FloatingActionButton
可以看到当Snackbar出现时,FloatingActionButton会上移并且支持右滑消失。

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(Snackbar snackbar, int event)

void	onShown(Snackbar snackbar)

其中onShown在Snackbar可见时调用,onDismissed在Snackbar准备消失时调用。一般我们可以在onDismissed方法中正在处理我们所需要的操作,比如删除一封邮件,那么如果是点击了“撤销”按钮,那就应该不再删除邮件直接消失就可以了,但是对于其他的几种情况,就需要真正地删除邮件了(发送数据到后台等等…)。下面是模拟这样一段过程:

   Snackbar.make(view, "已删除一个会话", Snackbar.LENGTH_SHORT).setAction("撤销", new View.OnClickListener() {
            @Override
            public void onClick(View v) {

            }
        }).setCallback(new Snackbar.Callback() {
            @Override
            public void onDismissed(Snackbar snackbar, int 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:
                        //TODO 网络操作
                        Toast.makeText(MainActivity.this, "删除成功", Toast.LENGTH_SHORT).show();
                        break;
                    case Snackbar.Callback.DISMISS_EVENT_ACTION:
                        Toast.makeText(MainActivity.this, "撤销了删除操作", Toast.LENGTH_SHORT).show();
                        break;

                }
            }

            @Override
            public void onShown(Snackbar snackbar) {
                super.onShown(snackbar);
                Log.i(TAG, "onShown");
            }
        }).show();

上述代码在onDismissed中根据消失类型进行不同的处理。效果如下:
处理Snackbar的消失事件

二、Snackbar源码分析

2.1、Snackbar的创建分析

从前面的段落知道,创建Snackbar需要使用静态的make方法,并且其中的view参数是一个查找父布局的起点。下面是make方法的实现:

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为起点寻找合适的父布局,下面是findSuitableParent方法的实现:

private static ViewGroup findSuitableParent(View view) {
        ViewGroup fallback = null;
        do {
            if (view instanceof CoordinatorLayout) {
                // We've found a CoordinatorLayout, use it
                return (ViewGroup) view;
            } else if (view instanceof 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);

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

可以看到如果view是CoordinatorLayout,那么就直接作为父布局了;如果是FrameLayout,并且如果是android.R.id.content,也就是查找到了DecorView,即最顶部,那么就只用这个view;如果不是的话,先保存下来;接下来就是获取view的父布局,然后循环再次判断。这样导致的结果最终会有两个选择,要么是CoordinatorLayout,要么就是FrameLayout,并且是最顶层的那个布局。具体情况是这样的:

  • 如果从View往上搜寻,如果有CoordinatorLayout,那么就使用该CoordinatorLayout
  • 如果从View往上搜寻,没有CoordinatorLayout,那么就使用android.R.id.content的FrameLayout
    接下来再看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);
    }

其中SnackbarLayout就是Snackbar的样式,SnackbarLayout继承自LinearLayout并且有一个TextView和一个Button,其中TextView就是左边用于显示文字,Button就是右边用于设置点击事件的。SnackbarLayout的部分代码如下:

public static class SnackbarLayout extends LinearLayout {
        private TextView mMessageView;
        private Button mActionView;
       
       ...
       
        public SnackbarLayout(Context context, AttributeSet attrs) {
            super(context, attrs);
            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);

            // 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.
            LayoutInflater.from(context).inflate(R.layout.design_layout_snackbar_include, this);

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

            // Make sure that we fit system windows and have a listener to apply any insets
            ViewCompat.setFitsSystemWindows(this, true);
            ViewCompat.setOnApplyWindowInsetsListener(this,
                    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;
                }
            });
        }

        @Override
        protected void onFinishInflate() {
            super.onFinishInflate();
            mMessageView = (TextView) findViewById(R.id.snackbar_text);
            mActionView = (Button) findViewById(R.id.snackbar_action);
        }

        TextView getMessageView() {
            return mMessageView;
        }

        Button getActionView() {
            return mActionView;
        }
        
        ...
}

至此,Snackbar被创建了。

2.2、对Snackbar进行设置

Snackbar有一些setXX方法,比如setAction、setActionTextColor等方法,这里我们主要介绍setAction和setActionTextColor方法的实现,其余的类似。从2.1的分析我们知道,Snackbar其实就是一个包含了TextView和Button的LinearLayout。明白了这一点之后,就好理解setXX方法了,首先看setAction()方法的实现:

 /**
     * Set the action to be displayed in this {@link Snackbar}.
     *
     * @param text     Text to display
     * @param listener callback to be invoked when the action is clicked
     */
    @NonNull
    public Snackbar setAction(CharSequence text, final View.OnClickListener listener) {
        final TextView tv = mView.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(Callback.DISMISS_EVENT_ACTION);
                }
            });
        }
        return this;
    }

首先调用mView.getActionView()方法,返回的tv其实就是右边的Button,然后判断文本和监听器,设置可见性、文本、监听器。在前面的例子中,我们知道一旦点击了按钮,Snackbar就会消失,处理消失的逻辑在dispatchDismiss()方法中,下面是dispatchDismiss()方法的实现:

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

可以看到,会获取一个SnackbarManager对象的实例,然后调用dismiss方法,具体的消失稍后再讲。
下面看setActionTextColor方法,该方法用于设置按钮文本颜色,方法如下:

/**
     * Sets the text color of the action specified in
     * {@link #setAction(CharSequence, View.OnClickListener)}.
     */
    @NonNull
    public Snackbar setActionTextColor(@ColorInt int color) {
        final TextView tv = mView.getActionView();
        tv.setTextColor(color);
        return this;
    }

首先是获取到Button实例,然后调用setTextColor方法,其余setXX之类的设置样式方法类似

2.3、Snackbar的显示与消失

如果需要让Snackbar显示,那么需要调用show方法,下面是show方法的实现:

/**
     * Show the {@link Snackbar}.
     */
    public void show() {
        SnackbarManager.getInstance().show(mDuration, mManagerCallback);
    }

首先获取一个SnackbarManager对象,然后调用它的show方法,show方法如下:

 public void show(int duration, Callback callback) {
        synchronized (mLock) {
            if (isCurrentSnackbarLocked(callback)) {
                // 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);
                scheduleTimeoutLocked(mCurrentSnackbar);
                return;
            } else if (isNextSnackbarLocked(callback)) {
                // 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)) {
                // 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
                showNextSnackbarLocked();
            }
        }
    }

上面的代码比较复杂,下面根据具体情况来分析,首先看其中的参数Callback。
其中mManagerCallback是SnackbarManager的Callback,每一个Snackbar都会有一个这样的对象,定义如下:

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发送了一个消息,sHandler的定义如下:

static final Handler sHandler;
    static final int MSG_SHOW = 0;
    static final int MSG_DISMISS = 1;

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

可以看到sHandler是一个静态的并且在Snackbar被加载进类加载器的时候就会创建,handlerMessage方法就是调用Snackbar的showView()显示和hideView()消失。showView和hideView方法后面再看。
下面针对show方法进行分析:

  1. 如果当前没有Snackbar显示,这时显示一个Snackbar并调用了show方法,那么最终会进入到SnackbarManager的show方法中,由于是第一个Snackbar,那么mCurrentSnackbar、mNextSnackbar均为null,则首先执行这一行代码,
mNextSnackbar = new SnackbarRecord(duration, callback);

接下来,由于mCurrentShackbar为null,则会执行else的代码:

 // Clear out the current snackbar
                mCurrentSnackbar = null;
                // Otherwise, just show it now
                showNextSnackbarLocked();

由于执行mNextSnackbar,自然要将mCurrentSnackbar置为null,然后调用showNextSnackbarLocked()方法,下面是该方法的实现:

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

首先将mCurrntSnackbar设为mNextSnackbar,然后获取Callback,调用Callback的show方法,从前面的分析知道show方法中向Snackbar的Handler发送一个消息,最后调用Snackbar的showView()方法显示Snackbar。
2. 如果当前已经有一个Snackbar显示了,又再调用了该对象的show方法,但是只是设置了不同时间,那么就会执行下段代码:

if (isCurrentSnackbarLocked(callback)) {
                // 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);
                scheduleTimeoutLocked(mCurrentSnackbar);
                return;
            }

重置mCurrentSnackbar的时间,然后移除mCureentSnackbar发出的消息和回调,mCurrentSnackbar会发出什么消息呢?mCurrentSnackbar会在Snackbar的时间到了后发送一个超时的消息给Handler,下面是handler的实现:

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

Handler的处理又是调用handleTimeout方法,handleTimeout方法的实现如下:

 void handleTimeout(SnackbarRecord record) {
        synchronized (mLock) {
            if (mCurrentSnackbar == record || mNextSnackbar == record) {
                cancelSnackbarLocked(record, Snackbar.Callback.DISMISS_EVENT_TIMEOUT);
            }
        }
    }

从上面可以知道会调用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;
    }

从上面可以看出,首先移除SnackbarRecord发出的所有消息,然后调用Callback的dismiss方法,从上面我们知道最终是向Snackbar的sHandler发送了一条消息,最终是调用Snackbar的hideView消失。
show方法中重置了时间以及删除了Handler中的消息后就是调用了scheduleTimeoutLocked方法

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

从上面可以看出,如果显示的时间是一个不定的,那么就不管;然后设置时间,最后调用sendMessageDelayed,由于该Snackbar目前正在显示,所以就会在durationMs后发送MSG_TIMEOUT的消息,从上面的分析知道,SnackbarManager的Handler在收到MSG_TIMEOUT后最终会将消息发送给Snackbar的sHandler,最后调用hideView方法。
3. 如果当前已有一个Snackbar正在显示,又创建了一个新的Snackbar并调用show方法,那么SnackbarManager的show方法会执行

else if (isNextSnackbarLocked(callback)) {
                // 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);
            }

首先进入isNextSnackbarLocked方法,就是判断该callback是否是mNextSnackbar的,按照我们这个情况不是的,那么就会else语句创建mNextSnackbar。接下来执行下段代码:

 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 {
                // Clear out the current snackbar
                mCurrentSnackbar = null;
                // Otherwise, just show it now
                showNextSnackbarLocked();
            }

这时,mCurrentSnackbar不为null,然后调用cancelSnackbarLocked方法,cancelSnackbarLocked方法在前面已经提到就是在Handler中移除mCurrentSnackbar发出的消息,然后调用Callback的dismiss方法,最终是调用Snackbar的hideView方法,并且注意到传入的参数为DISMISS_EVENT_CONSECUTIVE,该参数代表新的Snackbar出现导致旧的消失。在这里我们只看到了旧的消失,而没有看到新的显示,答案在Snackbar的hideView中,下面是hideView的实现:

final void hideView(@Callback.DismissEvent final int event) {
        if (shouldAnimate() && mView.getVisibility() == View.VISIBLE) {
            animateViewOut(event);
        } else {
            // If anims are disabled or the view isn't visible, just call back now
            onViewHidden(event);
        }
    }

首先判断是调用animateViewOut还是onViewHidden方法,下面是animateViewOut方法的实现:

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) {
                            mView.animateChildrenOut(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方法,所以最终都是要调用onViewHidden方法的。animateViewOut提供动画效果,onViewHidden提供具体的业务处理,下面是onViewHidden方法

void onViewHidden(int event) {
        // First tell the SnackbarManager that it has been dismissed
        SnackbarManager.getInstance().onDismissed(mManagerCallback);
        // Now call the dismiss listener (if available)
        if (mCallback != null) {
            mCallback.onDismissed(this, event);
        }
        if (Build.VERSION.SDK_INT < 11) {
            // We need to hide the Snackbar on pre-v11 since it uses an old style Animation.
            // ViewGroup has special handling in removeView() when getAnimation() != null in
            // that it waits. This then means that the calculated insets are wrong and the
            // any dodging views do not return. We workaround it by setting the view to gone while
            // ViewGroup actually gets around to removing it.
            mView.setVisibility(View.GONE);
        }
        // Lastly, hide and remove the view from the parent (if attached)
        final ViewParent parent = mView.getParent();
        if (parent instanceof ViewGroup) {
            ((ViewGroup) parent).removeView(mView);
        }
    }

从代码中可以看出,首先调用SnackbarManager的onDismissed方法,然后判断Snackbar.Callback是不是null,调用Snackbar.Callback的onDismissed方法,就是我们上面介绍的处理Snackbar消失的方法。最后就是将Snackbar的mView移除。下面看SnackbarManager的onDismissed方法:

/**
     * Should be called when a Snackbar is no longer displayed. This is after any exit
     * animation has finished.
     */
    public void onDismissed(Callback callback) {
        synchronized (mLock) {
            if (isCurrentSnackbarLocked(callback)) {
                // If the callback is from a Snackbar currently show, remove it and show a new one
                mCurrentSnackbar = null;
                if (mNextSnackbar != null) {
                    showNextSnackbarLocked();
                }
            }
        }
    }

从上面的方法可以看到,将mCurrentSnackbar置为null,然后因为mNextSnackbar不为null,所以调用showNextSnackbarLocked方法,从上面的介绍知道showNextSnackbarLocked就是将其置为mCurrentSnackbar然后最后调用了Snackbar的showView方法显示。
下面我们看一下Snackbar的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(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.addView(mView);
        }

        mView.setOnAttachStateChangeListener(new SnackbarLayout.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(Callback.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 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();
                    }
                }
            });
        }
    }

前面的先不看,后面的和hideView类似,animateViewIn负责动画,但是最终会调用onViewShown,所以直接看onViewShown方法,

void onViewShown() {
        SnackbarManager.getInstance().onShown(mManagerCallback);
        if (mCallback != null) {
            mCallback.onShown(this);
        }
    }

可以看到会调用SnackbarManager的onShown方法,然后如果Snackbar.Callback不为null,就调用其onShown回调。下面是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);
            }
        }
    }

可以看到最终调用了scheduleTimeoutLocked方法,从上面的分析知道scheduleTimeoutLocked方法就是在设定的时间到达后发送一条MSG_TIMEOUT消息给SnackbarManager的Handler,最后又是回到了Snackbar的hideView方法。
4. 显式调用dismiss方法,Snackbar的dismiss方法如下:

/**
     * Dismiss the {@link Snackbar}.
     */
    public void dismiss() {
        dispatchDismiss(Callback.DISMISS_EVENT_MANUAL);
    }

前面介绍过dispatchDismiss方法,最终是调用SnackbarManager的dismiss方法,如下:

public void dismiss(Callback callback, int event) {
        synchronized (mLock) {
            if (isCurrentSnackbarLocked(callback)) {
                cancelSnackbarLocked(mCurrentSnackbar, event);
            } else if (isNextSnackbarLocked(callback)) {
                cancelSnackbarLocked(mNextSnackbar, event);
            }
        }
    }

从上面的代码可以看出,就是调用cancelSnackbarLocked方法,而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;
    }

可以看到该方法首先移除Handler中的消息,然后调用dismiss方法,最终还是回到Snackbar的hideView方法。

2.4、总结

上面设计到两个类,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类的定义:

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

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

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

Snackbar向SnackbarManager发送消息主要是调用SnackbarManager.getInstace()返回一个单例对象;而SnackManager向Snackbar发送消息就是通过show方法传入的Callback对象。
SnackbarManager中的Handler只处理一个MSG_TIMEOUT事件,最后是调用Snackbar的hideView消失的;Snackbar的sHandler处理两个消息,showView和hideView,而消息的发送者是mManagerCallback,控制者是SnackbarManager。

关注我的技术公众号,不定期会有优质技术文章推送。

微信扫一扫下方二维码即可关注:
微信公众号二维码

  • 20
    点赞
  • 64
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
要在Android Studio中使用Snackbar点击按钮带图标方式显示消息提示,需要遵循以下步骤: 1.在app/build.gradle文件中添加以下依赖项: ``` implementation 'com.android.support:design:28.0.0' ``` 这将添加支持Snackbar的Material Design库。 2.在布局文件中添加一个Button和一个Snackbar容器View: ``` <RelativeLayout android:id="@+id/main_layout" android:layout_width="match_parent" android:layout_height="match_parent"> <Button android:id="@+id/my_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Show Snackbar"/> <LinearLayout android:id="@+id/snackbar_container" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true"/> </RelativeLayout> ``` 3.在Activity中获取Button和Snackbar容器View的引用: ``` Button myButton = findViewById(R.id.my_button); View snackbarContainer = findViewById(R.id.snackbar_container); ``` 4.在Button的onClick事件中,创建Snackbar实例并设置消息内容和图标: ``` myButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Snackbar snackbar = Snackbar.make(snackbarContainer, "This is a Snackbar message", Snackbar.LENGTH_LONG); //设置Snackbar中的图标 Drawable icon = getResources().getDrawable(R.drawable.ic_info_outline); icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight()); TextView textView = snackbar.getView().findViewById(android.support.design.R.id.snackbar_text); textView.setCompoundDrawables(icon, null, null, null); textView.setCompoundDrawablePadding(getResources().getDimensionPixelOffset(R.dimen.snackbar_icon_padding)); snackbar.show(); } }); ``` 这将创建一个Snackbar实例,将其附加到Snackbar容器View中,并在Snackbar消息中添加一个带有指定图标的TextView。 现在,当用户点击按钮时,Snackbar将显示在屏幕底部,并显示指定的消息和图标。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值