Android--Snackbar基本使用以及源码分析

2020-07-02 15:27:58 浏览数 (1)

Snackbar是谷歌集成的design包下的一个用于消息提示的控件,介于Dialog和Toast之间,结合了两者的优点,又解决了Dialog强提示和Toast无法交互的缺点,使用方法也很简单,首先我们需要引入design包,然后在代码中调用
代码语言:javascript复制
    public void click(View view) {
        Snackbar.make(view, "这是一个提示", Snackbar.LENGTH_SHORT).show();
    }
其中第二个参数是提示内容,第三个参数是消失的时间

Snackbar.gif

我们需要给与用户点击反馈后才能消除提示的情况下,可以这样使用
代码语言:javascript复制
    public void click(View view) {
        Snackbar.make(view, "这是一个提示", Snackbar.LENGTH_INDEFINITE).setAction("确定", new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this, "点击了确定", Toast.LENGTH_SHORT).show();
            }
        }).show();
    }
把消失时间传入Snackbar.LENGTH_INDEFINITE,点击完成后Snackbar会自动消失

Snackbar.gif

我们还可以利用setCallback方法监听SnackBar的显示和消失,利用setActionTextColor方法设置点击按钮的字体颜色
代码语言:javascript复制
    public void click(View view) {
        Snackbar.make(view, "这是一个提示", Snackbar.LENGTH_INDEFINITE).setAction("确定", new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                
            }
        }).setCallback(new Snackbar.Callback() {
            @Override
            public void onShown(Snackbar sb) {
                Toast.makeText(MainActivity.this, "显示了", Toast.LENGTH_SHORT).show();
                super.onShown(sb);
            }

            @Override
            public void onDismissed(Snackbar transientBottomBar, int event) {
                Toast.makeText(MainActivity.this, "消失了", Toast.LENGTH_SHORT).show();
                super.onDismissed(transientBottomBar, event);
            }
        }).setActionTextColor(Color.YELLOW).show();
    }

Snackbar.gif

通常情况下,我们无法修改提示的颜色和字体大小,如果我们想要修改的话,就从分析Snackbar的源码开始,了解这个控件的内部
首先我们来看Snackbar的make方法
代码语言:javascript复制
    /**
     * Make a Snackbar to display a message
     *
     * <p>Snackbar will try and find a parent view to hold Snackbar's view from the value given
     * to {@code view}. Snackbar will walk up the view tree trying to find a suitable parent,
     * which is defined as a {@link CoordinatorLayout} or the window decor's content view,
     * whichever comes first.
     *
     * <p>Having a {@link CoordinatorLayout} in your view hierarchy allows Snackbar to enable
     * certain features, such as swipe-to-dismiss and automatically moving of widgets like
     * {@link FloatingActionButton}.
     *
     * @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}
     */
    @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;
    }
首先它调用了findSuitableParent方法,传入我们在make方法中传入的view,获取了一个ViewGroup
代码语言:javascript复制
    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;
    }
findSuitableParent方法很有意思,它会不断的寻找父容器
如果是CoordinatorLayout的话,就直接返回,所以说我们使用CoordinatorLayout,是可以改变Snackbar的显示位置的;
如果是id为content的FrameLayout的话就直接返回这个FrameLayout,之前分析Activity启动源码时,我们了解到我们自己写的布局,最终会被添加到DecorView的一个id为content的FrameLayout上,所以这就是一般情况下Snackbar是显示在最下方的原因
回到make方法
代码语言:javascript复制
    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;
    }
之后,又加载了一个design_layout_snackbar_include的布局,我们看下这个布局
代码语言:javascript复制
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright (C) 2015 The Android Open Source Project
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~      http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
-->

<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>
很明显,这就是我们的提示信息和点击按钮,我们继续看make方法,之后它实例化了Snackbar :final Snackbar snackbar = new Snackbar(parent, content, content);其中parent为id为content的FrameLayout(暂时不考虑CoordinatorLayout),content为上面的布局
来看Snackbar的构造方法,发现最终调用的父类BaseTransientBottomBar的构造方法
代码语言:javascript复制
    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 ,并添加了content(design_layout_snackbar_include.xml生成的View),我们看下mView的布局文件design_layout_snackbar
代码语言:javascript复制
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright (C) 2015 The Android Open Source Project
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~      http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
-->

<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"
      android:theme="@style/ThemeOverlay.AppCompat.Dark"
      style="@style/Widget.Design.Snackbar" />
这边只是将内容(content)再包装一层容器(mView),可以更好的管理样式
在BaseTransientBottomBar类中,我们还发现了值得注意的成员变量,这边先记一下
代码语言:javascript复制
    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;
            }
        });
    }

    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));
        }
    };
好了,Snackbar的初始化,到这边就结束了,我们继续来看Snackbar的show()方法
代码语言:javascript复制
    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();
            }
        }
    }
其中的callback就是我们上面记录的mManagerCallback,再看mNextSnackbar = new SnackbarRecord(duration, callback);这段代码
代码语言:javascript复制
    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;
        }
    }
这边将mManagerCallback使用了弱引用,我们继续往下,来到showNextSnackbarLocked();方法
代码语言:javascript复制
    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;
            }
        }
    }
如果没有被回收,则调用mManagerCallback的show方法,我们再看下mManagerCallback
代码语言:javascript复制
    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));
        }
    };
由于这边使用到了Handler,所以防止内存泄漏,使用了弱引用,再来看sHandler对象
代码语言:javascript复制
    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;
            }
        });
    }
调用了showView方法
代码语言:javascript复制
    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();
                    }
                }
            });
        }
    }
里面有一行关键代码mTargetParent.addView(mView);
mTargetParent是DecorView中的id为content的FrameLayout,mView就是之前解析xml的Snackbar,最终通过addView方法将Snackbar显示出来。

SnackBar时序图.png

Snackbar的源码分析就到这里。

0 人点赞