(三十)Snackbar 使用及其源码分析

版权声明:本文为博主原创文章,未经博主允许不得转载。

本文纯个人学习笔记,由于水平有限,难免有所出错,有发现的可以交流一下。

一、Snackbar、Dialog、Toast

Dialog :交互性太强。当弹出的时候会阻断用户操作的连段性,降低用户体验

Toast:没有交互性,用户不能选择

Snackbar:介于 Dialog 和 Toast 之间,既不会打断用户操作,又可以与用户进行交互

二、Snackbar Demo

1.show()

activity_main:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.xiaoyue.snackbar.MainActivity">

    <Button
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="点击"
        android:gravity="center"
        android:onClick="click"/>

</RelativeLayout>

MainActivity:

public class MainActivity extends AppCompatActivity {

    Button button;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        button = findViewById(R.id.btn);
    }

    public void click(View view) {
        Snackbar.make(button, "SnackBar Test", Snackbar.LENGTH_LONG).show();
    }
}

效果:
这里写图片描述

Snackbar 使用 show 方法的时候,效果与 Toast 类似,只是换成从底部弹出提示信息。

注:SnackBar 的显示时间有三种模式,比 Toast 多一种。

public static final int LENGTH_INDEFINITE = BaseTransientBottomBar.LENGTH_INDEFINITE;

public static final int LENGTH_SHORT = BaseTransientBottomBar.LENGTH_SHORT;

public static final int LENGTH_LONG = BaseTransientBottomBar.LENGTH_LONG;

使用 LENGTH_INDEFINITE 则不会自动消失,需要手动调用 dismiss() 方法。

2.setAction()

为了添加与用户的交互性,需要调用 setAction 方法。

修改 OnClick 方法:

    public void click(View view) {
        Snackbar.make(button, "SnackBar Test", Snackbar.LENGTH_SHORT).setAction("确定", new View.OnClickListener(){

            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this, "点击确认了", Toast.LENGTH_SHORT).show();
            }
        }).show();
    }

效果:
这里写图片描述

3.setCallback(Callback callback)

添加 setCallback 可以对 Snackbar 的 onShown 和 onDismissed 进行监听。

修改 OnClick 方法:

    public void click(View view) {
        Snackbar.make(button, "SnackBar Test", Snackbar.LENGTH_SHORT).setAction("确定", new View.OnClickListener(){

            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this, "点击确认了", Toast.LENGTH_SHORT).show();
            }
        }).setCallback(new Snackbar.Callback(){
            @Override
            public void onShown(Snackbar sb) {
                Toast.makeText(MainActivity.this, "Snackbar onShown", Toast.LENGTH_SHORT).show();
            }

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

效果:
这里写图片描述

4.setActionTextColor()

设置弹出字体的颜色。

修改 OnClick 方法:

    public void click(View view) {
        Snackbar.make(button, "SnackBar Test", Snackbar.LENGTH_SHORT).setAction("确定", new View.OnClickListener(){

            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this, "点击确认了", Toast.LENGTH_SHORT).show();
            }
        }).setCallback(new Snackbar.Callback(){
            @Override
            public void onShown(Snackbar sb) {
                Toast.makeText(MainActivity.this, "Snackbar onShown", Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onDismissed(Snackbar transientBottomBar, @DismissEvent int event) {
                Toast.makeText(MainActivity.this, "Snackbar onDismissed", Toast.LENGTH_SHORT).show();
            }
        }).setActionTextColor(Color.BLUE).show();
    }

效果:
这里写图片描述

5.修改提示信息字体样式

修改 OnClick 方法:

    public void click(View view) {
        Snackbar snackbar = Snackbar.make(button, "SnackBar Test", Snackbar.LENGTH_SHORT).setAction("确定", new View.OnClickListener(){

            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this, "点击确认了", Toast.LENGTH_SHORT).show();
            }
        }).setCallback(new Snackbar.Callback(){
            @Override
            public void onShown(Snackbar sb) {
                Toast.makeText(MainActivity.this, "Snackbar onShown", Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onDismissed(Snackbar transientBottomBar, @DismissEvent int event) {
                Toast.makeText(MainActivity.this, "Snackbar onDismissed", Toast.LENGTH_SHORT).show();
            }
        }).setActionTextColor(Color.BLUE);

        View view1 = snackbar.getView();
        TextView textView = view1.findViewById(R.id.snackbar_text);
        textView.setTextColor(Color.RED);

        snackbar.show();
    }

效果:
这里写图片描述

Snackbar 没有提供直接的方法或接口供我们去修改提示信息的样式,也可以去重写 Snackbar 实现这个样式的改变。这边是通过 getView() 获取到 SnackBar 的布局,再通过 findViewById()获取到对应的 TextView ,进行修改。

三、源码分析

1.make

 Snackbar.make(button, "SnackBar Test", Snackbar.LENGTH_SHORT).show();

Snackbar 的 make 方法:

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

Snackbar 的 findSuitableParent 方法:

    private static ViewGroup findSuitableParent(View view) {
        ViewGroup fallback = null;
        do {
            //CoordinatorLayout 可以作为 Material Design 其他控件的跟布局
            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 通过循环,不断的获取父容器,最终获取到当前 View 的根节点。CoordinatorLayout 是跟 SnackBar 同属于 Material Design,可以作为 Material Design 下其他控件的跟布局。所以碰见 CoordinatorLayout 就可以直接返回,否则的话会去寻找 最顶层窗口 DecorView 下的一个 FrameLayout 布局,他的 id 是 content,这个系统都会帮我们添加。

可以利用 CoordinatorLayout 可以作为其他控件的跟布局进行修改 SnackBar 的显示位置。

修改 activity_main:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.xiaoyue.snackbar.MainActivity">

    <android.support.design.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="500dp">

        <Button
            android:id="@+id/btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="点击"
            android:gravity="center"
            android:onClick="click"/>
    </android.support.design.widget.CoordinatorLayout>
</RelativeLayout>

效果:
这里写图片描述

继续 make 方法往下:

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

R.layout.design_layout_snackbar_include 这个布局就是显示出来的 SnackBar 的布局,这个代码在对应的缓存里面。

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 的构造函数。

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

Snackbar 构造函数:

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

直接调用了父类 BaseTransientBottomBar 的构造函数。
BaseTransientBottomBar 的构造函数:

 protected BaseTransientBottomBar(@NonNull ViewGroup parent, @NonNull View content,

        ...

        mView = (SnackbarBaseLayout) inflater.inflate(
                R.layout.design_layout_snackbar, mTargetParent, false);
        mView.addView(content);

        ...
    }

BaseTransientBottomBar 有重新加载了一个布局 R.layout.design_layout_snackbar,然后把 上面加载的 SnackBar 布局添加进来。

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"
      android:theme="@style/ThemeOverlay.AppCompat.Dark"
      style="@style/Widget.Design.Snackbar" />

2.show

接下去看一下 show 方法。

show:

    public void show() {
        SnackbarManager.getInstance().show(mDuration, mManagerCallback);
    }

    public void show(int duration, Callback callback) {
        synchronized (mLock) {
            //判断是否有 SnackBar 已经显示出来
            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;

            //判断当前的这个 SnackBar 是否是再次调用显示,是的话,只更新时间
            } else if (isNextSnackbarLocked(callback)) {
                // We'll just update the duration
                mNextSnackbar.duration = duration;
            } else {
                //第一次进来的时候,创建一个 SnackBar 记录 SnackbarRecord
                // 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 {
                // Clear out the current snackbar
                mCurrentSnackbar = null;
                // Otherwise, just show it now
                showNextSnackbarLocked();
            }
        }
    }

mManagerCallback :

    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 传递进来的 Callback,在后面的 showNextSnackbarLocked()中被调用 。

SnackbarRecord :

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

SnackbarRecord 是一个内部类,就是一个简单的赋值,要注意的地方是在这里,传进来的 callback 变成了弱引用,避免内存泄漏

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

show 方法最后是调用到 showNextSnackbarLocked (),在这里又重新获取前面的 Callback (这样在这边就是弱引用了),然后调用 Callback 的 show 方法,调用了 sHandler 发送 MSG_SHOW 消息。

sHandler :

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

sHandler 发送消息,调用了 BaseTransientBottomBar (这里的 BaseTransientBottomBar 即要显示的 SnackBar)的 showView ()。

BaseTransientBottomBar 的 showView:

    final void showView() {
        if (mView.getParent() == null) {
            final ViewGroup.LayoutParams lp = mView.getLayoutParams();

            if (lp instanceof CoordinatorLayout.LayoutParams) {
              ...
            }

            mTargetParent.addView(mView);
        }

BaseTransientBottomBar 的 showView () 中间有一段对父容器是 CoordinatorLayout 的时候进行处理,这个不管。在最后,BaseTransientBottomBar 的 showView ()会调用 mTargetParent.addView(mView) 这个语句,mTargetParent 就是在上面 make ()中获取到的根节点,mView 就是我们包装了一层的 SnackBar 布局,这样就把 SnackBar 布局添加到界面上了

三、扩展

可以学习这个实现把一个 View 添加到全局窗口中。有时间回来实现。。。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值