轻量级控件SnackBar使用以及源码分析

2024-06-24 05:38

本文主要是介绍轻量级控件SnackBar使用以及源码分析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

本篇博客将会给大家带来一个轻量级控件SnackBar,为什么要讲SnackBar?Snackbar:的提出实际上是界于Toast和Dialog的中间产物。因为Toast与Dialog各有一定的不足,使用Toast的时候, 用户无法交互;使用Dialog:用户可以交互,但是体验会打折扣,会阻断用户的连贯性操作;但是使用Snackbar既可以做到轻量级的用户提醒效果,又可以有交互的功能,本博客将会从SnackBar的使用和源码分析两个方面进行介绍。

SnackBar的使用

SnackBar的使用十分简单,其实和Toast的使用方法差不多,我们写一个很简单的例子,来看一下SnackBar的使用,布局上有一个按钮,点击后弹出SnackBar,弹出的逻辑如下,布局代码很简单就不贴了。

public void showSnackBar(View view) {//LENGTH_INDEFINITE:无穷Snackbar snackbar = Snackbar.make(view,"您的Wifi已经开启!",Snackbar.LENGTH_INDEFINITE);snackbar.setAction("确定", new View.OnClickListener() {@Overridepublic void onClick(View v) {Toast.makeText(MainActivity.this, "确定啦", Toast.LENGTH_SHORT).show();}});snackbar.setCallback(new Snackbar.Callback() {@Overridepublic void onDismissed(Snackbar snackbar, int event) {Toast.makeText(MainActivity.this, "SnackBar消失了", Toast.LENGTH_SHORT).show();}@Overridepublic void onShown(Snackbar snackbar) {Toast.makeText(MainActivity.this, "SnackBar出现了", Toast.LENGTH_SHORT).show();}});snackbar.setActionTextColor(Color.BLUE);snackbar.show();
}

可以看到上面代码,setAction方法用于给SnackBar设置按钮,setCallback方法用于设置回调,当SnackBar出现时或者消失时都会有相应的回调,同时setActionTextColor方法可以给改变SnackBar中按钮的颜色。


SnackBar的源码分析

SnackBar是通过make方法进行创建的,所以我们首先需要查看SnackBar的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方法,Snackbar内部把view传递给了这个方法,查看该方法的逻辑

private static ViewGroup findSuitableParent(View view) {ViewGroup fallback = null;do {if (view instanceof CoordinatorLayout) {// We've found a CoordinatorLayout, use itreturn (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 fallbackfallback = (ViewGroup) view;}}if (view != null) {// Else, we will loop and crawl up the view hierarchy and try to find a parentfinal 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 fallbackreturn fallback;
}

发现这里竟然是一个do while的循环,只要view!= null,就会一直循环下去,里面会对view进行判断,是CoordinatorLayout,则直接返回,如果是FrameLayout,并且当view.getId() == android.R.id.content时候,也将view进行返回,大家都知道R.id.content就是decorView下的content部分,否则就会将这个view赋值给fallback,这个fallback就是一个viewGroup。下面这一句非常关键

if (view != null) {// Else, we will loop and crawl up the view hierarchy and try to find a parentfinal ViewParent parent = view.getParent();view = parent instanceof View ? (View) parent : null;}

取出view的Parent并且只要这个parent是View,就将其赋值给我门的view,到这里我们明白了,这个死循环就是为了无限的从传进来的这个view开始无限的向上寻找view的父亲,直到没有父亲为止,最后会返回fallback。然后我们自然会先去查看Snackbar构造函数,看它里面是进行了什么逻辑

private Snackbar(ViewGroup parent) {mParent = parent;mContext = parent.getContext();LayoutInflater inflater = LayoutInflater.from(mContext);mView = (SnackbarLayout) inflater.inflate(R.layout.design_layout_snackbar, mParent, false);}

在这里面最重要的一句就是渲染了一个R.layout.design_layout_snackbar的布局,很明显这个布局是系统自带的,很明显在这里已经写死了,所以我们想修改这个SnackBar显然是不行的,而且它还强转成了SnackbarLayout布局,我们可以查看一下这个布局的代码,这个布局在design包的layout下

<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"style="@style/Widget.Design.Snackbar" />

在这里我们可以学到2点,一是如何引用某个类里面的内部类,就是通过class=“”,第二点就是自定义控件的第二种引用方法,使用View标签,然后内部使用class进行引用。我们看一下SnackbarLayout的代码:

<pre name="code" class="java"><pre name="code" class="java">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);}


 
 

里面会创建一个TypedArray,然后取出里面的属性进行设置,最后会渲染一个布局:R.layout.design_layout_snackbar_include,它被渲染到当前SnackbarLayout之中

<merge xmlns:android="http://schemas.android.com/apk/res/android"><TextViewandroid:id="@+id/snackbar_text"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_weight="1"android:paddingTop="@dimen/snackbar_padding_vertical"android:paddingBottom="@dimen/snackbar_padding_vertical"android:paddingLeft="@dimen/snackbar_padding_horizontal"android:paddingRight="@dimen/snackbar_padding_horizontal"android:textAppearance="@style/TextAppearance.Design.Snackbar.Message"android:maxLines="@integer/snackbar_text_max_lines"android:layout_gravity="center_vertical|left|start"android:ellipsize="end"/><TextViewandroid:id="@+id/snackbar_action"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginLeft="@dimen/snackbar_extra_spacing_horizontal"android:layout_marginStart="@dimen/snackbar_extra_spacing_horizontal"android:layout_gravity="center_vertical|right|end"android:background="?attr/selectableItemBackground"android:paddingTop="@dimen/snackbar_padding_vertical"android:paddingBottom="@dimen/snackbar_padding_vertical"android:paddingLeft="@dimen/snackbar_padding_horizontal"android:paddingRight="@dimen/snackbar_padding_horizontal"android:visibility="gone"android:textAppearance="@style/TextAppearance.Design.Snackbar.Action"/></merge>

Snackbar的布局里面果然是使用了这个布局,如果我们要改变布局的样式,我们就修改这个文件里面的相关属性就可以了,就比如这里的textAppearance。我们回到Snackbar的构造方法中,同时它还把parent传了进去,  看过LayoutInflater源码的都知道,只有同时满足root不为空,而且attachToRoot为真的时候,root才会去添加这个渲染的temp,也就是我们上面传进来的R.layout.design_layout_snackbar,明显没有添加进mParent中去,那么Snackbar到底是在哪里addView的呢?我们一定要去追寻出这个添加Snackbar的地方。

if (root != null && attachToRoot) {root.addView(temp, params);}

我们跟踪mView这个变量,终于在showView方法中,找到了addView的足迹

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 Behaviorfinal 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() {@Overridepublic void onDismiss(View view) {dispatchDismiss(Callback.DISMISS_EVENT_SWIPE);}@Overridepublic void onDragStateChanged(int state) {switch (state) {case SwipeDismissBehavior.STATE_DRAGGING:case SwipeDismissBehavior.STATE_SETTLING:// If the view is being dragged or settling, cancel the timeoutSnackbarManager.getInstance().cancelTimeout(mManagerCallback);break;case SwipeDismissBehavior.STATE_IDLE:// If the view has been released and is idle, restore the timeoutSnackbarManager.getInstance().restoreTimeout(mManagerCallback);break;}}});((CoordinatorLayout.LayoutParams) lp).setBehavior(behavior);}mParent.addView(mView);}if (ViewCompat.isLaidOut(mView)) {// If the view is already laid out, animate it nowanimateViewIn();} else {// Otherwise, add one of our layout change listeners and animate it in when laid outmView.setOnLayoutChangeListener(new SnackbarLayout.OnLayoutChangeListener() {@Overridepublic void onLayoutChange(View view, int left, int top, int right, int bottom) {animateViewIn();mView.setOnLayoutChangeListener(null);}});}}

这里的代码比较长,我们一点一点进行分析,当mView.getParent() == null时,就是mView已经没有父View的时候,会取出它的LayoutParams,如果这个LayoutParams instanceofCoordinatorLayout.LayoutParams,然后是new一个Behavior,给Behavior设置各种参数以及监听,最后这个Behavior会设置给LayoutParams,然后这个mView最终会添加mParent的ViewGroup容器之中。

当view已经绘制完毕后,会给它设置一个出现的动画animateViewIn,否则会给mView设置布局变化的监听,每一次布局改变都会调用动画,并把监听设置为null,这里设置为null也是非常巧妙的,如果不这样设置,这个监听就会一直回调。

我们粗略查看一下animateViewIn的内部逻辑:

private void animateViewIn() {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {ViewCompat.setTranslationY(mView, mView.getHeight());ViewCompat.animate(mView).translationY(0f).setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR).setDuration(ANIMATION_DURATION).setListener(new ViewPropertyAnimatorListenerAdapter() {@Overridepublic void onAnimationStart(View view) {mView.animateChildrenIn(ANIMATION_DURATION - ANIMATION_FADE_DURATION,ANIMATION_FADE_DURATION);}@Overridepublic void onAnimationEnd(View view) {if (mCallback != null) {mCallback.onShown(Snackbar.this);}SnackbarManager.getInstance().onShown(mManagerCallback);}}).start();} else {Animation anim = AnimationUtils.loadAnimation(mView.getContext(), R.anim.design_snackbar_in);anim.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);anim.setDuration(ANIMATION_DURATION);anim.setAnimationListener(new Animation.AnimationListener() {@Overridepublic void onAnimationEnd(Animation animation) {if (mCallback != null) {mCallback.onShown(Snackbar.this);}SnackbarManager.getInstance().onShown(mManagerCallback);}@Overridepublic void onAnimationStart(Animation animation) {}@Overridepublic void onAnimationRepeat(Animation animation) {}});mView.startAnimation(anim);}
}

其实就是进行判断,如果编译的版本大于3.0,就是用属性动画进行一系列的动画设置,否则就是用传统的动画设置。

接着我们查看一下Show方法的逻辑:

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

这里用到了SnackbarManager,我们查看一下它的源码,看到getInstance就知道它肯定使用了单例的设计模式

static SnackbarManager getInstance() {if (sSnackbarManager == null) {sSnackbarManager = new SnackbarManager();}return sSnackbarManager;}

直接查看show方法

        synchronized (mLock) {if (isCurrentSnackbar(callback)) {// Means that the callback is already in the queue. We'll just update the durationmCurrentSnackbar.duration = duration;// If this is the Snackbar currently being shown, call re-schedule it's// timeoutmHandler.removeCallbacksAndMessages(mCurrentSnackbar);scheduleTimeoutLocked(mCurrentSnackbar);return;} else if (isNextSnackbar(callback)) {// We'll just update the durationmNextSnackbar.duration = duration;} else {// Else, we need to create a new record and queue itmNextSnackbar = 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 linereturn;} else {// Clear out the current snackbarmCurrentSnackbar = null;// Otherwise, just show it nowshowNextSnackbarLocked();}}}

Show方法中会传进来一个callback,这个callback是一个接口,里面有两个抽象方法show和dismiss

interface Callback {void show();void dismiss(int event);
}

再回到show方法内部,可以发现首先是加了一个同步锁,这样的目的,我们也可以猜出来,就是防止多次对SnackBar调用show方法,只有当一个SnackBar show完事了之后,下一个SnackBar才能show,也可以看出来SnackbarManager是对SnackBar起到管理作用的。通过isCurrentSnackbar(callback)方法判断传入show方法的callback是否在队列之中,其中有一个SnackbarRecord类型的变量mCurrentSnackbar用于记录时间。

if (isCurrentSnackbar(callback)) {// Means that the callback is already in the queue. We'll just update the durationmCurrentSnackbar.duration = duration;// If this is the Snackbar currently being shown, call re-schedule it's// timeoutmHandler.removeCallbacksAndMessages(mCurrentSnackbar);scheduleTimeoutLocked(mCurrentSnackbar);return;}

如果当前的Snackbar已经展示完毕,同时它的展示时间已经到了,mHandler就会发送一个消息,移除这个Snackbar的callback,同时调用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 timeoutreturn;}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);
}

首先是根据给SnackBar设置的不同显示时长来进行相应处理,然后是调用mHandler的removeCallbacksAndMessages和sendMessageDelayed方法,进行消息的发送,接着我们可以看一下handler做了什么处理

mHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {@Overridepublic boolean handleMessage(Message message) {switch (message.what) {case MSG_TIMEOUT:handleTimeout((SnackbarRecord) message.obj);return true;}return false;}});

当时间到了,会调用handleTimeout方法,SnackbarRecord会被传入这个方法之中

private void handleTimeout(SnackbarRecord record) {synchronized (mLock) {if (mCurrentSnackbar == record || mNextSnackbar == record) {cancelSnackbarLocked(record, Snackbar.Callback.DISMISS_EVENT_TIMEOUT);}}
}
在handleTimeout中同样会同步的调用cancelSnackbarLocked方法
private boolean cancelSnackbarLocked(SnackbarRecord record, int event) {final Callback callback = record.callback.get();if (callback != null) {callback.dismiss(event);return true;}return false;
}

这方法内部会从SnackbarRecord内部把callback取出来,如果callback不为空的时候,会调用callback的dismiss方法,回到show方法中,如果调用show方法的是下一个Snackbar就更新一下mNextSnackbar的duration,否则就new 一个SnackbarRecord。

接下来是判定,如果当前有一个Snackbar,就不做处理。


if (mCurrentSnackbar != null && cancelSnackbarLocked(mCurrentSnackbar,Snackbar.Callback.DISMISS_EVENT_CONSECUTIVE)) {// If we currently have a Snackbar, try and cancel it and wait in linereturn;} else {// Clear out the current snackbarmCurrentSnackbar = null;// Otherwise, just show it nowshowNextSnackbarLocked();}

如果当前SnackbarRecord不为空,而且其中的callback正在dismiss时,return,否则会清空当前snackbar,然后展示下一个snackbar

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 SnackbarmCurrentSnackbar = null;}}
}

showNextSnackbarLocked其中的逻辑也很简单,把下一个SnackbarRecord赋值给当前的,取出里面的callback,不为空时调用show方法。我们再查看一下SnackbarRecord的源码:

private static class SnackbarRecord {private final WeakReference<Callback> callback;private 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;}
}

里面使用了一个弱引用来包裹callback,这里是很值得我们学习的,使用WeakReference可以较好的避免内存泄漏的问题。Callback我们之前说过是一个接口,我们需要找一下它的实现类,既然是在show方法中把callback传进来的,所以我们要寻找一下SnackBarManager的show方法是在哪里调用的。本篇之前我们就看过SnackBar的show方法,里面调用了SnackbarManager的show方法

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

该方法内的参数mManagerCallback就是SnackBarManager内部Callback的实现类

private final SnackbarManager.Callback mManagerCallback = new SnackbarManager.Callback() {@Overridepublic void show() {sHandler.sendMessage(sHandler.obtainMessage(MSG_SHOW, Snackbar.this));}@Overridepublic void dismiss(int event) {sHandler.sendMessage(sHandler.obtainMessage(MSG_DISMISS, event, 0, Snackbar.this));}};

可以发现,其内部实现show与dismiss方法,使用sHandler发送不同的消息,查看sHandler的实现

sHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {@Overridepublic 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;}});

当message为MSG_SHOW时,会调用Snackbar的showView方法,当message为MSG_DISMISS时,会调用Snackbar的hideView,showView方法内部逻辑我们之前已经分析过了,再看一下hideView方法:

final void hideView(int event) {if (mView.getVisibility() != View.VISIBLE || isBeingDragged()) {onViewHidden(event);} else {animateViewOut(event);}}

hideView方法内调用onViewHidden方法:

private void onViewHidden(int event) {// First remove the view from the parentmParent.removeView(mView);// Now call the dismiss listener (if available)if (mCallback != null) {mCallback.onDismissed(this, event);}// Finally, tell the SnackbarManager that it has been dismissedSnackbarManager.getInstance().onDismissed(mManagerCallback);
}

首先mParent会把mView进行移除,然后如果mCallback!= null,会调用mCallback的onDismissed方法,最后调用SnackbarManager的onDismissed的方法,将callback移除出队列,到这里SnackBar和SnackbarManager的源码我们就基本分析完毕了。


























这篇关于轻量级控件SnackBar使用以及源码分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1089309

相关文章

使用Redis快速实现共享Session登录的详细步骤

《使用Redis快速实现共享Session登录的详细步骤》在Web开发中,Session通常用于存储用户的会话信息,允许用户在多个页面之间保持登录状态,Redis是一个开源的高性能键值数据库,广泛用于... 目录前言实现原理:步骤:使用Redis实现共享Session登录1. 引入Redis依赖2. 配置R

使用Python的requests库调用API接口的详细步骤

《使用Python的requests库调用API接口的详细步骤》使用Python的requests库调用API接口是开发中最常用的方式之一,它简化了HTTP请求的处理流程,以下是详细步骤和实战示例,涵... 目录一、准备工作:安装 requests 库二、基本调用流程(以 RESTful API 为例)1.

使用Python开发一个Ditto剪贴板数据导出工具

《使用Python开发一个Ditto剪贴板数据导出工具》在日常工作中,我们经常需要处理大量的剪贴板数据,下面将介绍如何使用Python的wxPython库开发一个图形化工具,实现从Ditto数据库中读... 目录前言运行结果项目需求分析技术选型核心功能实现1. Ditto数据库结构分析2. 数据库自动定位3

Python yield与yield from的简单使用方式

《Pythonyield与yieldfrom的简单使用方式》生成器通过yield定义,可在处理I/O时暂停执行并返回部分结果,待其他任务完成后继续,yieldfrom用于将一个生成器的值传递给另一... 目录python yield与yield from的使用代码结构总结Python yield与yield

Go语言使用select监听多个channel的示例详解

《Go语言使用select监听多个channel的示例详解》本文将聚焦Go并发中的一个强力工具,select,这篇文章将通过实际案例学习如何优雅地监听多个Channel,实现多任务处理、超时控制和非阻... 目录一、前言:为什么要使用select二、实战目标三、案例代码:监听两个任务结果和超时四、运行示例五

python使用Akshare与Streamlit实现股票估值分析教程(图文代码)

《python使用Akshare与Streamlit实现股票估值分析教程(图文代码)》入职测试中的一道题,要求:从Akshare下载某一个股票近十年的财务报表包括,资产负债表,利润表,现金流量表,保存... 目录一、前言二、核心知识点梳理1、Akshare数据获取2、Pandas数据处理3、Matplotl

Java使用Thumbnailator库实现图片处理与压缩功能

《Java使用Thumbnailator库实现图片处理与压缩功能》Thumbnailator是高性能Java图像处理库,支持缩放、旋转、水印添加、裁剪及格式转换,提供易用API和性能优化,适合Web应... 目录1. 图片处理库Thumbnailator介绍2. 基本和指定大小图片缩放功能2.1 图片缩放的

python panda库从基础到高级操作分析

《pythonpanda库从基础到高级操作分析》本文介绍了Pandas库的核心功能,包括处理结构化数据的Series和DataFrame数据结构,数据读取、清洗、分组聚合、合并、时间序列分析及大数据... 目录1. Pandas 概述2. 基本操作:数据读取与查看3. 索引操作:精准定位数据4. Group

Python使用Tenacity一行代码实现自动重试详解

《Python使用Tenacity一行代码实现自动重试详解》tenacity是一个专为Python设计的通用重试库,它的核心理念就是用简单、清晰的方式,为任何可能失败的操作添加重试能力,下面我们就来看... 目录一切始于一个简单的 API 调用Tenacity 入门:一行代码实现优雅重试精细控制:让重试按我

MySQL中EXISTS与IN用法使用与对比分析

《MySQL中EXISTS与IN用法使用与对比分析》在MySQL中,EXISTS和IN都用于子查询中根据另一个查询的结果来过滤主查询的记录,本文将基于工作原理、效率和应用场景进行全面对比... 目录一、基本用法详解1. IN 运算符2. EXISTS 运算符二、EXISTS 与 IN 的选择策略三、性能对比