Snackbar简单使用及源码浅析

snackbar的简单用法效果图:
这里写图片描述

贴上面效果的代码

public void onClick(View view){
        Snackbar snackbar = Snackbar.make(btn,"长沙下雨了",Snackbar.LENGTH_SHORT)
                .setAction("确定", new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        Toast.makeText(MainActivity.this,"点击确定了",Toast.LENGTH_SHORT).show();

                    }
                })
                .setCallback(new Snackbar.Callback(){
                    @Override
                    public void onShown(Snackbar sb) {
                        super.onShown(sb);
                    }

                    @Override
                    public void onDismissed(Snackbar transientBottomBar, int event) {
                        super.onDismissed(transientBottomBar, event);
                    }
                })
                .setActionTextColor(Color.YELLOW);
        View v = snackbar.getView();
        TextView tv = v.findViewById(R.id.snackbar_text);
        tv.setTextColor(Color.BLUE);
        snackbar.show();


    }

通过Snackbar的make(@NonNull View view, @NonNull CharSequence text,
@Duration int duration)方法来创建一个实例,第一个参数是点击的view,第二个参数是要提示的字符串,第三个参数是snackbar要显示的时间,duration的取值有三个:LENGTH_SHORT较短时间,LENGTH_LONG较长时间,LENGTH_INDEFINITE不会消失(如果没有调用dismiss方法或者没有其他的snackbar需要显示),创建完成后可以用show()方法进行显示。

也可以通过setAction(CharSequence text, final View.OnClickListener listener)方法设置一个用于交互的按钮,例如上面的确定按钮。

通过setActionTextColor(@ColorInt int color)方法来设置交互按钮文字的颜色。

还可以通过setCallback(Callback callback)方法设置回调,实现onShown和onDismissed方法,就是在snackbar显示和消失时的回调,我们可以做一些我们想做的事情。

View v = snackbar.getView();
TextView tv = v.findViewById(R.id.snackbar_text);
tv.setTextColor(Color.BLUE);
这段代码是为了设置snackbar设置要显示的文字的颜色,(例如上面的“长沙下雨了”)
Snackbar是没有提供给我们改变文字颜色的方法的。我们可以在源码中可以发现这个从下面弹出来的snackbar的布局是由左边一个TextView,右边一个Button组成。我们通过id,直接获取TextView进行设置。

下面对Snackbar的源码就行浅浅的分析:
Snackbar继承BaseTransientBottomBar。
入口Snackbar.make()方法。

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

这一行代码
final ViewGroup parent = findSuitableParent(view);
通过findSuitableParent()方法找到顶层View,这里的顶层View不是DecorView,而是CoordinatorLayout或者FrameLayout,看这个方法的实现就知道为什么了:

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

这个方法里面是个do while循环 ,通过view.getParent()获取父布局,并再复制给view,直到找到CoordinatorLayout,然后return,或者找到id为android.R.id.content的FrameLayout布局。
那它为什么要找到这个布局呢,其实就是要在后面把我们的snackbar的实质布局添加到这上面显示出来。

看完这个方法,再回到Snackbar的make()方法里接着向下走,

final SnackbarContentLayout content =(SnackbarContentLayout) inflater.inflate( R.layout.design_layout_snackbar_include, parent, false);

这行代码就是创建了Snackbar显示的布局SnackbarContentLayout,SnackbarContentLayout是继承LinearLayout的,我们可以看一下这个layout布局:

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

里面有一个id为snackbar_text的TextView,和一个id为snackbar_action的Button。所以我们可以直接通过findviewbyid的方式拿到TextView和Button来改变它们的属性。

好,接下来再回到make方法向下走,
final Snackbar snackbar = new Snackbar(parent, content, content);
将找到的parent,和解析出的布局SnackbarContentLayout通过Snackbar的构造函数传进去,创建Snackbar实例,然后返回。这就是make()的实现。我们还需要再看一下Snackbar的创建,

private Snackbar(ViewGroup parent, View content, ContentViewCallback contentViewCallback) {
        super(parent, content, contentViewCallback);
    }

调用了super,看一下父类实现,

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

首先将找到的父布局parent赋值给mTargetParent,然后给mView赋值,通过mView = (SnackbarBaseLayout) inflater.inflate(R.layout.design_layout_snackbar, mTargetParent, false);这行代码,再将content布局添加到mView里。

到这里make()方法结束。
下面就是调用show()方法来显示了;
在看show方法前,先看两个初始化的操作,在Snackbar的父类BaseTransientBottomBar里有一个static静态代码块,初始化了一个Handler,用来发消息。

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

还有一个是SnackbarManager的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));
        }
    };

show()和dismiss()方法都会通过Handler发消息,分别调用对用的BaseTransientBottomBar的showView()和hideView()方法。

好下面我们来看show方法,

/**
     * Show the {@link BaseTransientBottomBar}.
     */
    public void show() {
        SnackbarManager.getInstance().show(mDuration, mManagerCallback);
    }
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();
            }
        }
    }

在第一个if else里面如果snackbar是第一次显示,会走到最后一个else里面mNextSnackbar = new SnackbarRecord(duration, callback);
在SnackbarRecord的构造方法里就是简单的赋值操作

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

然后再往下执行,就到了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;
            }
        }
    }

这里的callback就是前面传入并封装到SnackbarRecord里的SnackbarManager的内部类实例mManagerCallback,调用了callback的show方法。然后就衔接到了上面一开始初始化Hanlder和Callback的地方,show方法里Handler发消息调用BaseTransientBottomBar的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);
        }

showView方法最后会执行mTargetParent.addView(mView);结束end。

这样snackbar显示的布局就添加到了所谓的顶层布局FrameLayout的最上层了,而不会被我们的contentView布局所覆盖。

Snackbar是 Material Design Support 8大控件之一,它和Dialog,Toast都有类似之处,
Dialog略显笨拙,会阻断用户的连续性,体验会打折扣,交互性太强。而Dialog的生命周期跟Snackbar一样,都是跟随当前Activity. 而Toast与它们两不同,跟当前的Activity的无关,因为Toast是通过Window实现的。Toast与Snackbar的不同处还有就是Toast不会一直显示,Snackbar可以通过设置Snackbar.LENGTH_INDEFINITE不消失,还有Toast只是用于显示没有交互性,Snackbar可以交互。

暂且到此结束。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值