* 本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布:
https://mp.weixin.qq.com/s/7qQdPLLfhZA_i7HjucfgWA
各位老铁,SmartShow更新至2.7.6了,感谢大家一直以来的支持。2.x版跟1.x版相比,有重大的更新,也有值得分享的东西。
BadTokenException解决方案
Android 7.1系统上,Toast会偶现BadTokenException。理解产生的原因,需要对Toast的基本工作原理以及不同系统版本Toast源码的变化有所了解,完整讲述可参考我的Toast系列博文。
说明:引用到系统源码时,我自己加的注释会标明“笔者注释”。为便于读者理解主要流程,无关代码用省略号代替。
限于篇幅,我简要说一下。Android 7.1开始,Google开始限制TYPE_TOAST窗口的滥用,系统在将Toast请求加入队列时,会为其创建一个Token。
@Override
public void enqueueToast(String pkg, ITransientNotification callback, int duration){
...
Binder token = new Binder();
//为该Toast窗口添加Token,笔者注释
mWindowManagerInternal.addWindowToken(token,WindowManager.LayoutParams.TYPE_TOAST);
record = new ToastRecord(callingPid, pkg, callback, duration, token);
//将该Toast请求加入队列,笔者注释
mToastQueue.add(record);
...
}
当Toast超时(duration耗尽)或者调用了cancel方法需要隐藏时,系统将这个显示请求从队列移除,并将Token设为失效。
void cancelToastLocked(int index) {
//从队列中把本次请求取出,笔者注释
ToastRecord record = mToastQueue.get(index);
...
//调用callback的hide方法隐藏掉窗口,这个callback实际上就是Toast的Tn,笔者注释
record.callback.hide();
...
//将该Toast请求从队列移除,笔者注释
ToastRecord lastToast = mToastQueue.remove(index);
//将该Toast窗口的Token设为失效,笔者注释
mWindowManagerInternal.removeWindowToken(lastToast.token, true);
...
//如果队列不为空,则说明还有Toast要显示,则继续显示下一个Toast,笔者注释
if (mToastQueue.size() > 0) {
showNextToastLocked();
}
}
Toast的工作流程是一个基于Binder的IPC(进程通信)过程,应用程序作为客户端仅仅发起Toast请求和被动接受回调。系统服务负责管理请求队列、Token等,并通过Toast的内部类Tn来实现与应用程序的交互,即通过回调Tn的show/hide方法来显示/隐藏Toast窗口。
应用程序的主线程是一个死循环,不断地从消息队列里取出消息执行。系统服务回调Tn的show方法,实际上的执行逻辑是发送一个SHOW消息给Tn的Handler,并最终在这个Handler的handleShow方法里执行具体的显示逻辑。
Tn的show方法:
@Override
public void show(IBinder windowToken) {
...
mHandler.obtainMessage(0, windowToken).sendToTarget();
}
Handler的handleShow方法:
public void handleShow(IBinder windowToken) {
...
//将系统服务创建的Token传递进来,笔者注释
mParams.token = windowToken;
...
//添加Toast窗口,若此时Token已然失效,引发BadTokenExceptin,笔者注释
mWM.addView(mView, mParams);
...
}
如果系统服务已然调用了Tn的show方法,而恰在此时,应用程序主线程因为某个消息阻塞或者其他原因,迟迟没能执行到handleShow方法。直到Toast超时,系统服务将该Toast请求从队列移除,并将Token设为失效。在这之后,应用程序才执行到handleShow方法,而Token已然失效,添加Toast窗口必然发生BadTokenException。
Google在Android8.0修复了这个bug。
首先,直接在产生BadTokenException的地方捕获该异常。
public void handleShow(IBinder windowToken) {
...
//捕获该异常,笔者注释
try {
mWM.addView(mView, mParams);
...
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
}
}
另外,在Tn的handler处理SHOW消息之前,判断其消息队列里是否存在HIDE或者CANCEL消息,有则表示Token已然失效,直接返回,什么都不需要做。
public void handleShow(IBinder windowToken) {
...
//判断是否有超时隐藏或者主动cancel的消息,有则表示系统服务已经将其从
//Toast队列移除,并将Token设为失效,笔者注释
if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
return;
}
...
try {
mWM.addView(mView, mParams);
...
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
}
但是,对于已经发布了的Android7.1,我们如何解决这个问题呢?我们可以仿照8.0的补救方法,直接添加try-catch块捕获它。注意,不能想当然的像下面这样捕获。
try {
toast.show();
}catch (WindowManager.BadTokenException e){
}
Toast的show方法仅仅是创建Tn提交给系统服务,请求显示Toast而已。前面说过,BadTokenException发生在handleShow方法内,而handleShow方法是在Tn的Handler的handleMessage方法内被调用的。我们可以新建一个Handler,命名为SafeHandler,作为Tn原有Handler的外壳。SafeHandler的handleMessage方法直接转交给原有Handler处理,只是在外层套上try-catch块。最后将SafeHandler注入Tn,取代原有Handler。
SafeHandler源码:
class SafeHandler extends Handler {
//用来保存Tn原有handler
private Handler mNestedHandler;
public SafeHandler(Handler nestedHandler) {
//构造方法里将Tn原有Handler传入
mNestedHandler = nestedHandler;
}
/**
* 在dispatchMessage里会调用handleMessage方法,可以直接在这里捕获异常
* @param msg
*/
@Override
public void dispatchMessage(Message msg) {
try {
super.dispatchMessage(msg);
} catch (WindowManager.BadTokenException e) {
}
}
@Override
public void handleMessage(Message msg) {
//交由原有Handler处理
mNestedHandler.handleMessage(msg);
}
通过反射拿到Tn,将SafeHandler注入
Field tnField = Toast.class.getDeclaredField("mTN");
tnField.setAccessible(true);
mTn = tnField.get(mToast);
if (isSdk25()) {
Field handlerField = mTn.getClass().getDeclaredField("mHandler");
handlerField.setAccessible(true);
Handler handlerOfTn = (Handler) handlerField.get(mTn);
handlerField.set(mTn, new SafeHandler(handlerOfTn));
}
类型消息
github上有不少花样Toast,五彩斑斓。SmartToast并不打算效仿,主流App很少采用如此花哨的消息提示。主流App如淘宝、微信、优酷、微博等等的消息提示,与ios非常相似。一是android UI确实丑,二是UI设计人员大多基于ios设计,很自然地把ios的风格带进android。SmartToast提供了8种常见的消息提示。
//普通
SmartToast.info("已在后台下载");
SmartToast.infoLong("已在后台下载");
//成功
SmartToast.success("重置成功");
SmartToast.successLong("重置成功");
//错误
SmartToast.error("保存失败");
SmartToast.errorLong("保存失败");
//警告
SmartToast.warning("电量过低,请充电");
SmartToast.warningLong("电量过低,请充电");
//完成
SmartToast.complete("下载完成");
SmartToast.completeLong("下载完成");
//失败
SmartToast.fail("保存失败");
SmartToast.failLong("保存失败");
//禁止
SmartToast.forbid("当前账户不允许汇款操作");
SmartToast.forbidLong("当前账户不允许汇款操作");
//等候
SmartToast.waiting("已在后台下载,请耐心等待");
SmartToast.waitingLong("已在后台下载,请耐心等待");
新的复用策略
在1.x版本中说过,Toast存在两个问题,一是当弹出一个新的Toast时,需等到前一个Toast的duration耗尽才弹出;二是短时间内多次触发相同内容的Toast会重复弹出。当然,大多厂商设备在android 7.0左右往后,都做了不同程度的优化,可能部分或全部避免了上述的问题。最常见的优化方式大致如下:
public final class ToastUtil {
private ToastUtil() {
}
//toast 单例
private static Toast sToast;
//默认的xOffset
private static int sDefaultXOffset;
//默认的yOffset
private static int sDefaultYOffset;
private static void createToast(CharSequence msg, int duration) {
if (sToast == null) {
//创建Toast单例,并保存默认的xOffset和yOffset
sToast = Toast.makeText(MyApplication.sContext, "", Toast.LENGTH_SHORT);
sDefaultXOffset = sToast.getXOffset();
sDefaultYOffset = sToast.getYOffset();
}
//修改message、duration
sToast.setText(msg);
sToast.setDuration(duration);
}
//默认位置显示Toast
public static void showToast(CharSequence msg, int duration) {
createToast(msg, duration);
sToast.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, sDefaultXOffset, sDefaultYOffset);
sToast.show();
}
//居中显示Toast
public static void showCenterToast(CharSequence msg, int duration) {
createToast(msg, duration);
sToast.setGravity(Gravity.CENTER, 0, 0);
sToast.show();
}
}
简单来说,就是复用Toast单例,每次显示时,修改message、duration、gravity等参数后调用Toast的show方法。
如此会存在两个缺陷。①如果本次显示尚未结束,再次显示不同文本的Toast无弹出效果;②如果本次显示尚未结束,再次显示不同位置(gravity)的Toast,Toast的位置并不会发生变化。前者尚可接受,后者就不可原谅了。看一下效果:
我们可以看到,第一遍,每次显示都等上一次显示结束后进行,结果是正常的。第二遍,反之,发现,没有动画,位置也没有改变。这是为什么呢?
原因是这样的:对于同一个Toast实例,多次调用show方法发起显示请求,如果它已在显示队列里,系统服务只会更改其duration,并没有添加新窗口。
下面两段代码取自sdk25。
@Override
public void enqueueToast(String pkg, ITransientNotification callback, int duration){
...
//ToastRecord 代表一次Toast显示请求,笔者注释
ToastRecord record;
//根据callback查找ToastRecord,callback其实就是Tn,Toast实例和Tn是一对一关系,笔者注释
int index = indexOfToastLocked(pkg, callback);
// 如果存在,则只更新duration,笔者注释
if (index >= 0) {
record = mToastQueue.get(index);
record.update(duration);
} else {
...
}
...
}
private static final class ToastRecord{
...
void update(int duration) {
this.duration = duration;
}
...
}
弹出动画和gravity都是对窗口起作用的,既然没有添加新的窗口,那么没有效果是正常的。而文本能够改变只是窗口内TextView自身重绘而已。
而且,自Android8.0开始,google及各大厂商都对Toast进行了优化。这种单例使用模式在部分设备上存在严重问题。我是在各大厂商的开发者平台进行远程真机测试时发现的。下面以华为麦芒7设备(android 8.1.0)为例,同一个Toast实例,短时间多次调用show方法,Toast会立即消失,而且也不会弹出新的Toast,持续触发,Toast将一直不显示。看下效果:
同样的,问题也是出现在本次显示尚未结束,再次调用show方法显示的时候。
曾经,在1.x版本中,我们通过反射调用Tn的hide方法,解决gravity不生效及消息文本改变无动画的问题,在Android8.0及以上设备上并不理想。
综合以上原因,为了尽可能兼容所有设备,我改变了Toast的复用策略。
首先,当一个Toast触发后,从显示到消失的时间内,再次触发相同的Toast(内容及位置一致),直接忽略。也就是说不再重复调用Toast的show方法。
另外,触发一个与上次显示内容或位置不相同的Toast,并且当前Toast尚未消失,不再调用Tn隐藏,而是直接cancel掉,并重建Toast实例。这种情况发生的次数较少(这也是很多人没有注意到这种场合下Gravity不能立即生效的原因),所以并不会引起频繁创建Toast实例,绝大多数时候都是复用实例。
现在看一下,上面两个案例,用SmartToast来显示的效果:
①魅族 pro 6 plus
②华为麦芒 7
小的优化
①改变Toast的背景不能通过setBackgroundColor。因为Toast的背景实际上是一张图片,而且不同厂商设备使用的图片大小和圆角不尽相同。如魅族的圆角就比较小,而华为的就比较大。如果直接设置setBackgroundColor,你的Toast就成方方正正的了,哈哈。在1.x版本中,我们采用ShapeDrawable设置,忽略不同厂商设备差异,设置了固定的圆角。在2.x我们改变了策略,直接获取Toast的背景,得到Drawable图片,如果是GradientDrawable实例,则直接设置新的颜色,否则一律采用Tint机制,改变其颜色。
@Override
protected void setupToast() {
...
Drawable bg = mView.getBackground();
if (bg instanceof GradientDrawable) {
((GradientDrawable) bg).setColor(ToastDelegate.get().getToastSetting().getBgColor());
} else {
DrawableCompat.setTint(bg, ToastDelegate.get().getToastSetting().getBgColor());
}
mView.setBackgroundDrawable(bg);
...
}
这样定制的Toast的大小和形状以及透明度都与目标设备完全一致了,仅仅颜色不同。
②离开当前Activity,Toast自动消失
Toast是独立窗口,并不会随着Activity的销毁而消失。在1.x版本发布时,有读者询问能否设置离开当前页面后Toast立即消失。2.x版中,只需如此设置:
SmartToast.setting()
.dismissOnLeave(true);
这样,无论是进入新的activity还是退出当前Activity,当前显示的Toast都会立即消失。
这种Topbar类似QQ、微信顶部弹窗。在代码实现上,可以说我是夺天之功。灵感是这么来的,在1.x版本中,有读者问是否能够实现顶部弹出的Snackbar。Snackbar是相当优秀的底部弹窗了,如果通过修改Snackbar实现类似QQ顶部弹窗功能是再好不过了。不过QQ、微信弹窗是独立窗口,可以悬浮于应用之外,Snackbar是依附于Activity的,只能应用内弹出。大多数应用的使用场景都是应用内弹窗,而且独立弹窗需要危险权限,用户拒绝则无法显示,且各大厂商设备关于悬浮窗方面的坑又比比皆是,所以应用内顶部弹窗不采用独立悬浮窗会更好一些。
开始改造过程。
首先,改变显示位置。1.x版本中说过,Snackbar是通过将View(实现类SnackbarLayout)嵌入到当前Activity的id为android.R.id.content的容器内或者某个CoordinateLayout中,具体会根据你提供的view为根基,沿着整个View Tree上溯,先找个哪个,就将其嵌入其内,且layout_gravity为bottom。
是不是直接把layout_gravity改为top就完事了?当然还不够,因为无论哪种嵌入方式,snackbar最终都是android.R.id.content这个容器的子View,而android.R.id.content 容器是位于状态栏下面的,弹窗是无法覆盖住状态栏的,状态栏的颜色和弹窗不一致的话,就不太协调了。当然,如果不考虑提供给别人使用的话,可以通过setSystemUiVisibility使其浸入状态栏。为了通用,我们将其嵌入DecorView。
Snackbar 是通过findSuitableParent(View view)方法来确定嵌入哪个容器的,我们修改它的实现,不再按照旧的逻辑沿View Tree上溯,而是直接返回传入的View,然后我们创建Topbar的时候直接将DecorView传入。
private static ViewGroup findSuitableParent(View view) {
return (ViewGroup) view;
}
这样,Topbar就会盖住状态栏了。
QQ、微信的顶部弹窗都是可以手滑消失的,Snackbar只有以CoordinateLayout为容器的时候才支持手滑消失。我们需要自己实现手滑消失么?No,牛顿哥说过,站在巨人的肩膀上,我们才能走的更远。哈哈。继续夺天之功。在嵌入Topbar之前,我先嵌入一个CoordinateLayout到DecorView,然后再将Topbar嵌入CoordinateLayout中,Topbar就可以支持手滑了。
获取Topbar的入口
/**
* 获取Topbar的入口
* @param activity
* @return
*/
public static IBarShow get(Activity activity) {
return TopbarDelegate.get().nestedDecorView(activity);
}
通过预定义的id值smart_show_top_bar_container,判断CoordinateLayout是否已嵌入,若不存在则先嵌入CoordinateLayout
public IBarShow nestedDecorView(Activity activity) {
//保存当前页面的Context
mPageContext = activity;
//取出DecorView
ViewGroup decorView = activity == null ? null : (ViewGroup) activity.getWindow().getDecorView();
CoordinatorLayout topbarContainer = null;
if (decorView != null) {
//判断CoordinateLayout是否已嵌入,不存在则先创建CoordinateLayout
topbarContainer = decorView.findViewById(R.id.smart_show_top_bar_container);
if (topbarContainer == null) {
topbarContainer = new CoordinatorLayout(activity);
topbarContainer.setId(R.id.smart_show_top_bar_container);
ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
);
//将CoordinateLayout嵌入DecorView
decorView.addView(topbarContainer, lp);
}
}
return getFromView(topbarContainer);
}
在上面,CoordinateLayout的布局参数中,高度设置为WRAP_CONTENT。Topbar消失时,布局从CoordinateLayout中移除,高度为0,CoordinateLayout不会绘制,不影响原有布局。CoordinateLayout没有设置layout_gravity为bottom,所以会顶部显示,SnackbarLayout被CoordinateLayout包裹着,也就不必修改layout_gravity为top了。
以CoordinateLayout为容器,将Topbar嵌入
protected IBarShow getFromView(View view) {
if (mBar == null || mBaseTraceView != view || isDismissByGesture()) {
mBaseTraceView = view;
rebuildBar(view);
}
return this;
}
最后,就剩下动画了。显示时向下弹出,消失时向上弹去。
Snackbar显示和消失动画最终是在animateViewIn()和animateViewOut(final int event)方法里设置的,实现上区分了sdk12(含)以上和以下的情况。我们的库最低支持sdk15,所以只考虑sdk >= 12的情况。
先分析Snackbar的显示动画,源码基于design 27.0.1
void animateViewIn() {
...
//取出View的高度,通过offset或者translationY的方式使其处于初始显示位置,
//也就是所嵌容器的下面,一般为屏幕下面,笔者注释
final int viewHeight = mView.getHeight();
if (USE_OFFSET_API) {
ViewCompat.offsetTopAndBottom(mView, viewHeight);
} else {
mView.setTranslationY(viewHeight);
}
//定义属性动画,变化值从view高度值到0,
final ValueAnimator animator = new ValueAnimator();
animator.setIntValues(viewHeight, 0);
animator.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
animator.setDuration(ANIMATION_DURATION);
...
//注册动画值变化监听器,按照变化值动态设置offset或者translationY形成动画,笔者注释
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
private int mPreviousAnimatedIntValue = viewHeight;
@Override
public void onAnimationUpdate(ValueAnimator animator) {
int currentAnimatedIntValue = (int) animator.getAnimatedValue();
if (USE_OFFSET_API) {
ViewCompat.offsetTopAndBottom(mView,
currentAnimatedIntValue - mPreviousAnimatedIntValue);
} else {
mView.setTranslationY(currentAnimatedIntValue);
}
mPreviousAnimatedIntValue = currentAnimatedIntValue;
}
});
animator.start();
}
}
总结一下,先取出View的高度,通过offset或者translationY的方式使其处于初始显示位置,即所嵌容器的下面,一般也就是屏幕下面。然后定义属性动画,变化值从view高度值到0,最后注册动画值变化监听器,按照变化值动态设置offset或者translationY形成动画。animateViewOut方法与此类似,只是定义的变化值是0到View高度,这里不再贴出。
要想达到显示时向下弹出,消失时向上弹去,只需将变化值分为别设为负的view高度到0和0到负的View高度即可。
新定义一个方法,获取设置动画时,提供的view height。
private int getAnimHeight() {
return -mView.getHeight();
}
分别在animateViewIn和animateViewOut中替换mView.getHeight(),以animateViewIn为例,
void animateViewIn() {
//替换mView.getHeight()
final int viewHeight = getAnimHeight();
if (USE_OFFSET_API) {
ViewCompat.offsetTopAndBottom(mView, viewHeight);
} else {
mView.setTranslationY(viewHeight);
}
final ValueAnimator animator = new ValueAnimator();
animator.setIntValues(viewHeight, 0);
...
}
复用策略
Topbar由Snackbar改造而来,所以复用策略是一样的,详情参看github文档。并且不同于1.x版中需要你在BaseActivity中调用资源回收的方法,2.x版本通过为application注册activityLifeCallback,自动回收资源及重建实例。
sApplication.registerActivityLifecycleCallbacks(new ActivityLifecycleCallback() {
...
@Override
public void onActivityDestroyed(Activity activity) {
super.onActivityDestroyed(activity);
if (SnackbarDeligate.hasCreated()) {
SnackbarDeligate.get().destroy(activity);
}
if (TopbarDelegate.hasCreated()) {
TopbarDelegate.get().destroy(activity);
}
}
});
如此,我们便巧夺天工般实现了顶部弹窗,你完全可以把它当做顶部Snackbar使用,它的api和SmartSnackbar几乎完全一致。
Dialog的使用过程中有可能出现下面三个问题:
①NullPointerException
这个问题是因为创建Dialog时传入的activity为null。一种常见的场景是在Fragment中创建Dialog时直接传入getActivity()。等到Fragment与activity解除关系时(detach),getActivity为null。此时创建Dialog引发NullPointerException。
②BadTokenException
显示对话框时,所依附的activity已经销毁了,导致BadTokenException。
③IllegalStateException
调用dimiss时,所依附的activity已经销毁了,引发IllegalStateException(DecorView not attached to window manager)。我的设备上没有复现,是在友盟崩溃上捕获到的。
其实上面的三种情况归根结底都是由生命周期引起的。举一个例子,网络访问的回调中显示Dialog,如果在接口返回之前当前Activity已退出,则引发BadTokenException。当然通常接口回调时,要根据activity生命周期决定是否取消掉操作UI的回调。不过,引发场景不只这一个,作为View层,我们要把好最后一道关。
SmartDialog在创建、显示Dialog及调用dismiss前,会判断activity的生命周期,生命周期不符合则不创建、不显示、不操作。
API
第一步,继承DialogCreator,实现你的Dialog创建逻辑。
public class ExampleDialogCreator extends DialogCreator {
/**
* 抽象方法,必须实现
*
* @param activity
* @return
*/
@Override
public Dialog createDialog(Activity activity) {
//创建Dialog,在这里可以保证activity不为null,并且没有destroyed或isFinishing
Dialog dialog = null;
...
return dialog;
}
/**
* 非抽象方法,默认实现为空,可选择性覆写,用于Dialog每次显示前的一些重置工作,例如EditText清空等
*
* @param dialog
*/
@Override
public void resetDialogPerShow(Dialog dialog) {
super.resetDialogPerShow(dialog);
}
}
第二步,创建SmartDialog,传入DialogCreator。实际上SmartDialog并没有继承Dialog,只是个Dialog的包装器,管理Dialog。reuse(boolean b)方法表示是否复用Dialog,如果不复用,每次显示都调用DialogCreator创建新的Dialog。
private SmartDialog mExampleDialog;
public void onShowDialogClick(View view) {
if (mExampleDialog == null) {
mExampleDialog = new SmartDialog()
.dialogCreator(new ExampleDialogCreator())
.reuse(true);
}
mExampleDialog.show(this);
}
SmartDialog的show方法和dismiss方法都需要传入activity,对activity的生命周期进行判断,避免BadTokenException等异常。
/**
* 判断 activity的状态是可以操作UI
* @param activity
* @return
*/
public static boolean isUpdateActivityUIPermitted(Activity activity) {
return activity != null && !activity.isFinishing() && !Utils.isActivityDestroyed(activity);
}
为了双重保险,对可能抛出异常的地方用try-catch包裹。
//mNestedDialog是被SmartDialog管理的Dialog实例
if (mNestedDialog != null) {
try {
mNestedDialog.show();
return true;
} catch (WindowManager.BadTokenException e) {
EasyLogger.e("BadToken has happened when show dialog: \n"
+ mNestedDialog.getClass().getSimpleName());
return false;
}
}
预定义的DialogCreator
预定了INotificationDialogCreator、IEnsureDialogCreator、IInputTextDialogCreator、ILoadingDialogCreator四种DialogCreator。通过DialogCreatorFactory获取相关实例。
private SmartDialog mExampleDialog;
public void onShowDialogClick(View view) {
if (mExampleDialog == null) {
mExampleDialog = SmartDialog.newInstance(new ExampleDialogCreator())
.reuse(true);
}
mExampleDialog.show(this);
}
预定义的DialogCreator可调用方法设置按钮文本和颜色、按钮点击事件、标题等等。具体可参见github上的文档。
下面是各种预定义DialogCreator创建的Dialog的展示图:
从2.5.3版本开始,各个模块单独抽离成库,可单独或自由组合依赖。
//smart toast
implementation 'com.github.the-pig-of-jungle.smart-show:toast:2.7.6'
// smart dialog
implementation 'com.github.the-pig-of-jungle.smart-show:dialog:2.7.6'
// smart topbar
implementation('com.github.the-pig-of-jungle.smart-show:topbar:2.7.6') {
exclude group: 'com.android.support'
}
//添加与你项目匹配的design依赖库的相应版本
implementation 'com.android.support:design:x.y.z'
// smart snackbar
implementation('com.github.the-pig-of-jungle.smart-show:snackbar:2.7.6') {
exclude group: 'com.android.support'
}
//添加与你项目匹配的design依赖库的相应版本
implementation 'com.android.support:design:x.y.z'
当然,也支持依赖所有模块的形式。
implementation ('com.github.the-pig-of-jungle.smart-show:all:2.7.6'){
exclude group: 'com.android.support'
}
//添加与你项目匹配的design依赖库的相应版本
implementation 'com.android.support:design:x.y.z'
最后,再次感谢大家的支持,记得github点赞哟!项目地址:https://github.com/the-pig-of-jungle/smart-show