Gradle引用

Step 1. Add the JitPack repository to your build file

Add it in your root build.gradle at the end of repositories:

allprojects {

repositories {

...

maven { url 'https://jitpack.io' }

}

}

Step 2. Add the dependency

dependencies {

implementation 'com.github.Dovar66:DToast:1.1.5'

//implementation 'com.github.Dovar66:DToast:1.1.6'//for androidx

}

使用示例

//使用默认布局

DToast.make(mContext)

.setText(R.id.tv_content_default, msg)

.setGravity(Gravity.BOTTOM | Gravity.CENTER, 0, 30)

.show();

//通过setView()设置自定义的Toast布局

DToast.make(mContext)

.setView(View.inflate(mContext, R.layout.layout_toast_center, null))

.setText(R.id.tv_content_custom, msg)

.setGravity(Gravity.CENTER, 0, 0)

.showLong();

正文分析

先看看使用系统Toast存在的问题:

1.当通知权限被关闭时在华为等手机上Toast不显示;

2.Toast的队列机制在不同手机上可能会不相同;

3.Toast的BadTokenException问题;

当发现系统Toast存在问题时,不少同学都会采用自定义的TYPE_TOAST弹窗来实现相同效果。虽然大部分情况下效果都是 OK的,但其实TYPE_TOAST弹窗依然存在兼容问题:

4.Android8.0之后的token null is not valid问题;

5.Android7.1之后,不允许同时展示两个TYPE_TOAST弹窗(实测部分机型问题)。

那么,DToast使用的解决方案是:

1.通知权限未被关闭时,使用SystemToast(修复了问题2和问题3的系统Toast);

2.通知权限被关闭时,如果系统版本为Android8.0/8.1则通过hook绕过通知栏权限,否则使用DovaToast(自定义的TYPE_TOAST弹窗);

3.当使用DovaToast出现token null is not valid时,尝试使用ActivityToast(自定义的TYPE_APPLICATION_ATTACHED_DIALOG

弹窗,只有当传入Context为Activity时,才会启用ActivityToast).

相信不少同学旧项目中封装的ToastUtil都是直接使用的ApplicationContext作为上下文,然后在需要弹窗的时候直接就是ToastUtil.show(str) ,这样的使用方式对于我们来说是最方便的啦。

当然,使用DToast你也依然可以沿用这种封装方式,但这种方式在下面这个场景中可能会无法成功展示出弹窗(该场景下原生Toast也一样无法弹出), 不过请放心不会导致应用崩溃,而且这个场景出现的概率较小,有以下几个必要条件:

1.你的应用设置的targetSdkVersion>=26.

2.通知栏权限被关闭(通知栏权限默认都是打开的).

3.非MIUI设备(MIUI弹吐司不需要通知栏权限).

4.运行设备的系统版本在Android9.0及以上。

所以,如果你的应用targetSdkVersion>=26,又想要保证在所有场景下都能正常展示弹窗,那么请在DToast.make(context)时传入Activity作为上下文,这样在该场景下DToast会启用ActivityToast展示出弹窗。而targetSdkVersion小于26的同学可以放心使用ApplicationContext创建DToast。

想了解为什么需要区别对待targetSdkVersion26+?点击查看API26做了什么

而如果你还不了解targetSdkVersion 点击这里查看

接下来再详细分析下上面提到的五个问题:

问题一:关闭通知权限时Toast不显示

看下方Toast源码中的show()方法,通过AIDL获取到INotificationManager,并将接下来的显示流程控制权

交给NotificationManagerService。

NMS中会对Toast进行权限校验,当通知权限校验不通过时,Toast将不做展示。

当然不同ROM中NMS可能会有不同,比如MIUI就对这部分内容进行了修改,所以小米手机关闭通知权限不会导致Toast不显示。

/**

* Show the view for the specified duration.

*/

public void show() {

if (mNextView == null) {

throw new RuntimeException("setView must have been called");

}

INotificationManager service = getService();

String pkg = mContext.getOpPackageName();

TN tn = mTN;

tn.mNextView = mNextView;

try {

service.enqueueToast(pkg, tn, mDuration);

} catch (RemoteException e) {

// Empty

}

}

如何解决这个问题?只要能够绕过NotificationManagerService即可。

DovaToast通过使用TYPE_TOAST实现全局弹窗功能,不使用系统Toast,也没有使用NMS服务,因此不受通知权限限制。

问题二:系统Toast的队列机制在不同手机上可能会不相同

我找了四台设备,创建两个Gravity不同的Toast并调用show()方法,结果出现了四种展示效果:

* 荣耀5C-android7.0(只看到展示第一个Toast)

* 小米8-MIUI10(只看到展示第二个Toast,即新的Toast.show会中止当前Toast的展示)

* 红米6pro-MIUI9(两个Toast同时展示)

* 荣耀5C-android6.0(第一个TOAST展示完成后,第二个才开始展示)

造成这个问题的原因应该是各大厂商ROM中NMS维护Toast队列的逻辑有差异。 同样的,DToast内部也维护着自己的队列逻辑,保证在所有手机上使用DToast的效果相同。

DToast中多个弹窗连续出现时:

1.相同优先级时,会终止上一个,直接展示后一个;

2.不同优先级时,如果后一个的优先级更高则会终止上一个,直接展示后一个。

问题三:系统Toast的BadTokenException问题

Toast有个内部类 TN(extends ITransientNotification.Stub),调用Toast.show()时会将TN传递给NMS; public void show() {

if (mNextView == null) {

throw new RuntimeException("setView must have been called");

}

INotificationManager service = getService();

String pkg = mContext.getOpPackageName();

TN tn = mTN;

tn.mNextView = mNextView;

try {

service.enqueueToast(pkg, tn, mDuration);

} catch (RemoteException e) {

// Empty

}

}

在NMS中会生成一个windowToken,并将windowToken给到WindowManagerService,WMS会暂时保存该token并用于之后的校验;

NotificationManagerService.java #enqueueToast源码: synchronized (mToastQueue) {

int callingPid = Binder.getCallingPid();

long callingId = Binder.clearCallingIdentity();

try {

ToastRecord record;

int index = indexOfToastLocked(pkg, callback);

// If it's already in the queue, we update it in place, we don't

// move it to the end of the queue.

if (index >= 0) {

record = mToastQueue.get(index);

record.update(duration);

} else {

// Limit the number of toasts that any given package except the android

// package can enqueue. Prevents DOS attacks and deals with leaks.

if (!isSystemToast) {

int count = 0;

final int N = mToastQueue.size();

for (int i=0; i

final ToastRecord r = mToastQueue.get(i);

if (r.pkg.equals(pkg)) {

count++;

if (count >= MAX_PACKAGE_NOTIFICATIONS) {

Slog.e(TAG, "Package has already posted " + count

+ " toasts. Not showing more. Package=" + pkg);

return;

}

}

}

}

Binder token = new Binder();//生成一个token

mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);

record = new ToastRecord(callingPid, pkg, callback, duration, token);

mToastQueue.add(record);

index = mToastQueue.size() - 1;

keepProcessAliveIfNeededLocked(callingPid);

}

// If it's at index 0, it's the current toast. It doesn't matter if it's

// new or just been updated. Call back and tell it to show itself.

// If the callback fails, this will remove it from the list, so don't

// assume that it's valid after this.

if (index == 0) {

showNextToastLocked();

}

} finally {

Binder.restoreCallingIdentity(callingId);

}

}

然后NMS通过调用TN.show(windowToken)回传token给TN; /**

* schedule handleShow into the right thread

*/

@Override

public void show(IBinder windowToken) {

if (localLOGV) Log.v(TAG, "SHOW: " + this);

mHandler.obtainMessage(SHOW, windowToken).sendToTarget();

}

TN使用该token尝试向WindowManager中添加Toast视图(mParams.token = windowToken);

在API25的源码中,Toast的WindowManager.LayoutParams参数新增了一个token属性,用于对添加的窗口进行校验。

当param.token为空时,WindowManagerImpl会为其设置 DefaultToken; @Override

public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {

applyDefaultToken(params);

mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);

}

private void applyDefaultToken(@NonNull ViewGroup.LayoutParams params) {

// Only use the default token if we don't have a parent window.

if (mDefaultToken != null && mParentWindow == null) {

if (!(params instanceof WindowManager.LayoutParams)) {

throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");

}

// Only use the default token if we don't already have a token.

final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;

if (wparams.token == null) {

wparams.token = mDefaultToken;

}

}

}

当WindowManager收到addView请求后会检查 mParams.token 是否有效,若有效则添加窗口展示,否则抛出BadTokenException异常. switch (res) {

case WindowManagerGlobal.ADD_BAD_APP_TOKEN:

case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:

throw new WindowManager.BadTokenException(

"Unable to add window -- token " + attrs.token

+ " is not valid; is your activity running?");

case WindowManagerGlobal.ADD_NOT_APP_TOKEN:

throw new WindowManager.BadTokenException(

"Unable to add window -- token " + attrs.token

+ " is not for an application");

case WindowManagerGlobal.ADD_APP_EXITING:

throw new WindowManager.BadTokenException(

"Unable to add window -- app for token " + attrs.token

+ " is exiting");

case WindowManagerGlobal.ADD_DUPLICATE_ADD:

throw new WindowManager.BadTokenException(

"Unable to add window -- window " + mWindow

+ " has already been added");

case WindowManagerGlobal.ADD_STARTING_NOT_NEEDED:

// Silently ignore -- we would have just removed it

// right away, anyway.

return;

case WindowManagerGlobal.ADD_MULTIPLE_SINGLETON:

throw new WindowManager.BadTokenException("Unable to add window "

+ mWindow + " -- another window of type "

+ mWindowAttributes.type + " already exists");

case WindowManagerGlobal.ADD_PERMISSION_DENIED:

throw new WindowManager.BadTokenException("Unable to add window "

+ mWindow + " -- permission denied for window type "

+ mWindowAttributes.type);

case WindowManagerGlobal.ADD_INVALID_DISPLAY:

throw new WindowManager.InvalidDisplayException("Unable to add window "

+ mWindow + " -- the specified display can not be found");

case WindowManagerGlobal.ADD_INVALID_TYPE:

throw new WindowManager.InvalidDisplayException("Unable to add window "

+ mWindow + " -- the specified window type "

+ mWindowAttributes.type + " is not valid");

}

什么情况下windowToken会失效?

UI线程发生阻塞,导致TN.show()没有及时执行,当NotificationManager的检测超时后便会删除WMS中的该token,即造成token失效。

如何解决?

Google在API26中修复了这个问题,即增加了try-catch:

// Since the notification manager service cancels the token right

// after it notifies us to cancel the toast there is an inherent

// race and we may attempt to add a window after the token has been

// invalidated. Let us hedge against that.

try {

mWM.addView(mView, mParams);

trySendAccessibilityEvent();

} catch (WindowManager.BadTokenException e) {

/* ignore */

}

因此对于8.0之前的我们也需要做相同的处理。DToast是通过反射完成这个动作,具体看下方实现:

//捕获8.0之前Toast的BadTokenException,Google在Android 8.0的代码提交中修复了这个问题

private void hook(Toast toast) {

try {

Field sField_TN = Toast.class.getDeclaredField("mTN");

sField_TN.setAccessible(true);

Field sField_TN_Handler = sField_TN.getType().getDeclaredField("mHandler");

sField_TN_Handler.setAccessible(true);

Object tn = sField_TN.get(toast);

Handler preHandler = (Handler) sField_TN_Handler.get(tn);

sField_TN_Handler.set(tn, new SafelyHandlerWrapper(preHandler));

} catch (Exception e) {

e.printStackTrace();

}

}

public class SafelyHandlerWrapper extends Handler {

private Handler impl;

public SafelyHandlerWrapper(Handler impl) {

this.impl = impl;

}

@Override

public void dispatchMessage(Message msg) {

try {

impl.dispatchMessage(msg);

} catch (Exception e) {

}

}

@Override

public void handleMessage(Message msg) {

impl.handleMessage(msg);//需要委托给原Handler执行

}

}

问题四:Android8.0之后的token null is not valid问题

Android8.0后对WindowManager做了限制和修改,特别是TYPE_TOAST类型的窗口,必须要传递一个token用于校验。

API25:(PhoneWindowManager.java源码)

public int checkAddPermission(WindowManager.LayoutParams attrs, int[] outAppOp) {

int type = attrs.type;

outAppOp[0] = AppOpsManager.OP_NONE;

if (!((type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW)

|| (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW)

|| (type >= FIRST_SYSTEM_WINDOW && type <= LAST_SYSTEM_WINDOW))) {

return WindowManagerGlobal.ADD_INVALID_TYPE;

}

if (type < FIRST_SYSTEM_WINDOW || type > LAST_SYSTEM_WINDOW) {

// Window manager will make sure these are okay.

return WindowManagerGlobal.ADD_OKAY;

}

String permission = null;

switch (type) {

case TYPE_TOAST:

// XXX right now the app process has complete control over

// this... should introduce a token to let the system

// monitor/control what they are doing.

outAppOp[0] = AppOpsManager.OP_TOAST_WINDOW;

break;

case TYPE_DREAM:

case TYPE_INPUT_METHOD:

case TYPE_WALLPAPER:

case TYPE_PRIVATE_PRESENTATION:

case TYPE_VOICE_INTERACTION:

case TYPE_ACCESSIBILITY_OVERLAY:

case TYPE_QS_DIALOG:

// The window manager will check these.

break;

case TYPE_PHONE:

case TYPE_PRIORITY_PHONE:

case TYPE_SYSTEM_ALERT:

case TYPE_SYSTEM_ERROR:

case TYPE_SYSTEM_OVERLAY:

permission = android.Manifest.permission.SYSTEM_ALERT_WINDOW;

outAppOp[0] = AppOpsManager.OP_SYSTEM_ALERT_WINDOW;

break;

default:

permission = android.Manifest.permission.INTERNAL_SYSTEM_WINDOW;

}

if (permission != null) {

...

}

return WindowManagerGlobal.ADD_OKAY;

}

API26:(PhoneWindowManager.java源码)

public int checkAddPermission(WindowManager.LayoutParams attrs, int[] outAppOp) {

int type = attrs.type;

outAppOp[0] = AppOpsManager.OP_NONE;

if (!((type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW)

|| (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW)

|| (type >= FIRST_SYSTEM_WINDOW && type <= LAST_SYSTEM_WINDOW))) {

return WindowManagerGlobal.ADD_INVALID_TYPE;

}

if (type < FIRST_SYSTEM_WINDOW || type > LAST_SYSTEM_WINDOW) {

// Window manager will make sure these are okay.

return ADD_OKAY;

}

if (!isSystemAlertWindowType(type)) {

switch (type) {

case TYPE_TOAST:

// Only apps that target older than O SDK can add window without a token, after

// that we require a token so apps cannot add toasts directly as the token is

// added by the notification system.

// Window manager does the checking for this.

outAppOp[0] = OP_TOAST_WINDOW;

return ADD_OKAY;

case TYPE_DREAM:

case TYPE_INPUT_METHOD:

case TYPE_WALLPAPER:

case TYPE_PRESENTATION:

case TYPE_PRIVATE_PRESENTATION:

case TYPE_VOICE_INTERACTION:

case TYPE_ACCESSIBILITY_OVERLAY:

case TYPE_QS_DIALOG:

// The window manager will check these.

return ADD_OKAY;

}

return mContext.checkCallingOrSelfPermission(INTERNAL_SYSTEM_WINDOW)

== PERMISSION_GRANTED ? ADD_OKAY : ADD_PERMISSION_DENIED;

}

}

为了解决问题一,DovaToast不得不选择绕过NotificationManagerService的控制,但由于windowToken是NMS生成的, 绕过NMS就无法获取到有效的windowToken,于是作为TYPE_TOAST的DovaToast就可能陷入第四个问题。因此,DToast选择在DovaToast出现 该问题时引入ActivityToast,在DovaToast无法正常展示时创建一个依附于Activity的弹窗展示出来,不过ActivityToast只会展示在当前Activity,不具有跨页面功能。 如果说有更好的方案,那肯定是去获取悬浮窗权限然后改用TYPE_PHONE等类型,但悬浮窗权限往往不容易获取,目前来看恐怕除了微信其他APP都不能保证拿得到用户的悬浮窗权限。

问题五:Android7.1之后,不允许同时展示两个TYPE_TOAST弹窗

DToast的弹窗策略就是同一时间最多只展示一个弹窗,逻辑上就避免了此问题。因此仅捕获该异常。

其他建议

新项目做应用架构的时候可以考虑把整个应用(除闪屏页等特殊界面外)做成只有一个Activity,其他全是Fragment,这样就不存在悬浮窗的问题啦。

如果能够接受Toast不跨界面的话,建议使用SnackBar

更新日志

1.1.5

新增IToast.setText(idRes,text)方法

[修复]issue#6

1.1.3

[修复]issue#7

1.1.2

新增思路:对Toast的INotificationManager对象进行hook可以成功绕过通知栏权限,但9.0之后Android限制调用非公开API,所以9.0之后此方法不可用。

代码更新:新增hook INotificationManager操作,在Android8.0/8.1上采用hook方式绕过通知栏权限。

android封装全局调用的toast_自定义Toast,解决系统Toast存在的问题相关推荐

  1. android封装全局调用的toast_【Android】Android中WebView实现Java与JS交互

    现在混合式开发是大趋势,H5不断蚕食移动互联网的份额,有的公司甚至只用H5就搞了一个APP,我们搞Android的不说会点H5,至少要懂怎么和H5(和JavaScript)交互,费话不多说. 一.先看 ...

  2. android封装全局调用的toast_Android Toast提示封装实例代码

    Android Toast提示封装 Android中经常用到Toast提示,项目中很多Toast提示,写很长的一行,简单的封装一下,将Toast方法提出来,很方便使用: 实例代码: /** * 提示字 ...

  3. android封装全局调用的toast_Android实用的Toast工具类封装

    1 importandroid.content.Context;2 importandroid.view.View;3 importandroid.widget.ImageView;4 importa ...

  4. android listview 连续调用 getview问题分析及解决。

    当我们在使用listview的时候.有时候自定义adapter的时候,是不是会发现在getview里打印日志的时候,重复调用很多次?有时候 4次.有的严重甚至到10次,当我们在listview中移动的 ...

  5. Unity调用Android封装的声网sdk

    文章目录 unity3调用Android 封装的声网SDK 1.环境版本 2.创建Android library 工程 3.unity3D 依赖包添加到工程libs下 4.UnityPlayerAct ...

  6. 关于Android封装一个全局的BaseActivity你需要知道的

    关于Android封装一个全局的BaseActivity你需要知道的 1.前言 2.特点 3.代码及说明 3.1.优缺点 3.2.代码 3.3.注意点 4.总结 5.最后 1.前言 对于一个Andro ...

  7. Android自定义浮框,Android实现全局悬浮框

    本文实例为大家分享了Android实现全局悬浮框的具体代码,供大家参考,具体内容如下 效果图: 代码实现: Androidmanifest.xml添加弹框权限 自定义悬浮窗类FloatWindow.j ...

  8. 封装一个简单showToast组件 / 自定义toast组件

    父组件: <tempalte><view><toast ref="mytoast"></toast></view> &l ...

  9. Android基于高德SDK的开发——自定义地图主题样式(悬浮按钮+底部弹窗)

    日常的地图使用中,平台一般只会给我们提供地图的标准样式,造成了一定程度上的审美疲劳,那么如何实现地图的自定义样式呢?本文使用Android Studio 4.1,给开发者提供了一个基于高德地图SDK进 ...

  10. Android View体系(十)自定义组合控件

    相关文章 Android View体系(一)视图坐标系 Android View体系(二)实现View滑动的六种方法 Android View体系(三)属性动画 Android View体系(四)从源 ...

最新文章

  1. Maven 概要介绍
  2. django captcha 验证码插件
  3. 图像处理(七)导向滤波磨皮
  4. Octave相关学习资源整理出
  5. 最近用.NET实现DHT爬虫,全.NET实现
  6. python selenium 用法 和 Chrome headless
  7. UVA763 LA5339 Fibinary Numbers【大数】
  8. 防火墙设置对外开放port
  9. python怎么用pandas查找指定字符串_Python Pandas:通过搜索子字符串查找表
  10. Linux中命令行进行WiFi连接(零基础详解)
  11. 浏览器兼容性问题和解决方案
  12. 高斯输出文件批量读取能量
  13. Unity全面的面试题(包含答案)
  14. SSH公钥原理(密钥,秘钥,私钥)(看了还是懵逼啊!)
  15. Barsetto百胜图BAV02自助咖啡机,创造便捷生活的无限可能
  16. 《恋爱厚黑学》杨冰阳
  17. 个人站长不要把理想和青春赌到网站上,写得太对了!
  18. 零基础入门--中文实体关系抽取(BiLSTM+attention,含代码)
  19. 洛谷P1023 税收与补贴
  20. 一文读懂AlphaGo背后的强化学习

热门文章

  1. c mysql 数据更新_MySQL数据更新
  2. 3d游戏编程大师技巧 源代码_C/C++编程新手入门基础系列:俄罗斯方块小游戏制作源代码...
  3. 【优化算法】学生心理学优化算法(SPBO)【含Matlab源码 1430期】
  4. 【优化算法】差分蜂群优化算法(DEABC)【含Matlab源码 1423期】
  5. 【路径规划】基于matlab遗传结合模拟退火算法仓库拣货小车最优路径规划【含Matlab源码 649期】
  6. 【优化调度】基于matlab粒子群算法求解燃机冷热电优化联供问题【含Matlab源码 330期】
  7. 【语音分离】基于matlab FastICA语音信号采集+混合+分离【含Matlab源码 008期】
  8. 机器视觉科学计算可视化_模因视觉:对模因进行分类的科学
  9. 方舟生存进化联机显示没有找到服务器,方舟生存进化搭建服务器联机教程_方舟生存进化怎么联机_牛游戏网...
  10. c语言考试答案,C语言考试题及答案