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

来源:互联网 发布:百度地图js api ios10 编辑:程序博客网 时间:2024/05/22 15:24

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

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

一、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 添加到全局窗口中。有时间回来实现。。。