Skip to content

Commit

Permalink
修复 Toast 显示会被全局的悬浮窗遮挡的问题
Browse files Browse the repository at this point in the history
新增支持 Toast 显示后发送无障碍事件(用于盲人辅助)
移除 Toast 中会默认设置 TextView 最大显示行数的逻辑
  • Loading branch information
getActivity committed Jun 25, 2022
1 parent a932660 commit c974a7e
Show file tree
Hide file tree
Showing 9 changed files with 73 additions and 45 deletions.
26 changes: 14 additions & 12 deletions .github/ISSUE_TEMPLATE/issue_template_bug.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,35 @@ assignees: getActivity

## 问题描述

* 框架版本:XXX
* 框架版本【必填】:XXX

* 问题描述:XXX
* 问题描述【必填】:XXX

* 复现步骤:XXX
* 复现步骤【必填】:XXX

* 是否必现:填是/否
* 是否必现【必填】:填是/否

* 手机信息:例如某米 9 / Android 10
* 出现问题的手机信息【必填】:请填写出现问题的品牌和机型

* 出现问题的安卓版本【必填】:请填写出现问题的 Android 版本

## 请回答

* 是部分机型还是所有机型都会出现:部分/全部(例如:某为,某 Android 版本会出现)
* 是部分机型还是所有机型都会出现【必答】:部分/全部(例如:某为,某 Android 版本会出现)

* 框架最新的版本是否存在这个问题:是/否(如果用的是旧版本的话,建议升级看问题是否还存在)
* 框架最新的版本是否存在这个问题【必答】:是/否(如果用的是旧版本的话,建议升级看问题是否还存在)

* 是否已经查阅框架文档还未能解决的:是/否(文档会提供最常见的问题解答,可以看看是否有自己想要的)
* 是否已经查阅框架文档还未能解决的【必答】:是/否(文档会提供最常见的问题解答,可以看看是否有自己想要的)

* issue 是否有人曾提过类似的问题:是/否(看看曾经有人提过类似的问题,先参考一下别人是怎么解决的)
* issue 是否有人曾提过类似的问题【必答】:是/否(看看曾经有人提过类似的问题,先参考一下别人是怎么解决的)

* 是否可以通过 Demo 来复现该问题:是/否(排查一下是不是自己的项目代码写得有问题导致的)
* 是否可以通过 Demo 来复现该问题【必答】:是/否(排查一下是不是自己的项目代码写得有问题导致的)

* 使用原生的 Toast 是否也会出现该问题:是/否(排查一下是不是框架的代码存在问题导致的)
* 使用原生的 Toast 是否也会出现该问题【必答】:是/否(排查一下是不是框架的代码存在问题导致的)

## 其他

* 提供报错堆栈(如果有报错的话必填)
* 提供报错堆栈(如果有报错的话必填,注意不要拿被混淆过的代码堆栈上来

* 提供截图或视频(根据需要提供,此项不强制)

Expand Down
6 changes: 3 additions & 3 deletions .github/ISSUE_TEMPLATE/issue_template_suggest.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ assignees: getActivity

## 建议收集

* issue 是否有人曾提过类似的问题?(必答项,一旦出现重复提问我将不会再次解答)
* issue 是否有人曾提过类似的问题?【必答】(一旦出现重复提问我将不会再次解答)

* 你觉得框架有什么不足之处?(必答项,你可以描述框架有什么令你不满意的地方)
* 你觉得框架有什么不足之处?【必答】(你可以描述框架有什么令你不满意的地方)

* 你觉得该怎么去完善会比较好?(非必答项,你可以提供一下自己的想法或者做法供作者参考)
* 你觉得该怎么去完善会比较好?【非必答】(你可以提供一下自己的想法或者做法供作者参考)
22 changes: 13 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

* 博客地址:[只需体验三分钟,你就会跟我一样,爱上这款 Toast](https://www.jianshu.com/p/9b174ee2c571)

* 可以扫码下载 Demo 进行演示或者测试,如果扫码下载不了的,[点击此处可直接下载](https://github.com/getActivity/ToastUtils/releases/download/10.3/ToastUtils.apk)
* 可以扫码下载 Demo 进行演示或者测试,如果扫码下载不了的,[点击此处可直接下载](https://github.com/getActivity/ToastUtils/releases/download/10.5/ToastUtils.apk)

![](picture/demo_code.png)

Expand Down Expand Up @@ -47,7 +47,7 @@ android {
dependencies {
// 吐司框架:https://github.com/getActivity/ToastUtils
implementation 'com.github.getActivity:ToastUtils:10.3'
implementation 'com.github.getActivity:ToastUtils:10.5'
}
```

Expand Down Expand Up @@ -140,9 +140,9 @@ ToastUtils.init(this, new ToastStrategy() {

| 功能或细节 | [ToastUtils](https://github.com/getActivity/ToastUtils) | [AndroidUtilCode](https://github.com/Blankj/AndroidUtilCode) | [Toasty](https://github.com/GrenderG/Toasty) |
| :----: | :------: | :-----: | :-----: |
| 对应版本 | 10.3 | 1.30.6 | 1.5.0 |
| 对应版本 | 10.5 | 1.30.6 | 1.5.0 |
| issues 数 | [![](https://img.shields.io/github/issues/getActivity/ToastUtils.svg)](https://github.com/getActivity/ToastUtils/issues) | [![](https://img.shields.io/github/issues/Blankj/AndroidUtilCode.svg)](https://github.com/Blankj/AndroidUtilCode/issues) | [![](https://img.shields.io/github/issues/GrenderG/Toasty.svg)](https://github.com/GrenderG/Toasty/issues) |
| **aar 包大小** | 28 KB | 500 KB | 50 KB |
| **aar 包大小** | 29 KB | 500 KB | 50 KB |
| **调用代码定位** ||||
| 支持在子线程中调用显示 ||||
| 支持全局设置统一 Toast 样式 ||||
Expand All @@ -158,13 +158,15 @@ ToastUtils.init(this, new ToastStrategy() {

#### Toast 在 Android 7.1 崩溃的问题介绍

> [Toast 在 Android 7.1 崩溃排查及修复](https://www.jianshu.com/p/437f473017d6)
* 这个问题是由于 Android 7.1 加入 WindowToken 校验机制导致的,而这个 WindowToken 是 NotificationManagerService 生成的,这个 WindowToken 是存在一定时效性的,而当应用的主线程被阻塞时,WindowManager 在 addView 时会对 WindowToken 进行校验,但是 WindowToken 已经过期了,这个时候 addView 就会抛出异常。

* 谷歌在 Android 8.0 就修复了这个问题,修复方式十分简单粗暴,就是直接捕获这个异常,而框架的修复思路跟谷歌类似,只不过修复方式不太一样,因为框架无法直接修改系统源码,所以是直接通过 Hook 的方式对异常进行捕获,大家如果对修复过程感兴趣可以看一下我写的这篇文章[Toast 在 Android 7.1 崩溃排查及修复](https://www.jianshu.com/p/437f473017d6)
* 谷歌在 Android 8.0 就修复了这个问题,修复方式十分简单粗暴,就是直接捕获这个异常,而框架的修复思路跟谷歌类似,只不过修复方式不太一样,因为框架无法直接修改系统源码,所以是直接通过 Hook 的方式对异常进行捕获。

#### 通知栏权限关闭后 Toast 显示不出来的问题介绍

* [Toast通知栏权限填坑指南](https://www.jianshu.com/p/1d64a5ccbc7c)
> [Toast通知栏权限填坑指南](https://www.jianshu.com/p/1d64a5ccbc7c)
* 这个问题的出现是因为原生 Toast 的显示要通过 NMS(NotificationManagerService) 才会 addView 到 Window 上面,而在 NMS 中有一个 `static final boolean ENABLE_BLOCKED_TOASTS = true` 的字段,当这个常量值为 true 时,会触发 NMS 对应用通知栏权限的检查,如果没有通知栏权限,那么这个 Toast 将会被 NMS 所拦截,并输出 `Suppressing toast from package` 日志信息,而小米手机没有这个问题是因为它是将 `ENABLE_BLOCKED_TOASTS` 字段值修改成 `false`,所以就不会触发对通知栏权限的检查,另外我为什么会知道有这个事情?因为我曾经和一名 MIUI 工程师一起确认过这个事情。

Expand All @@ -186,7 +188,7 @@ ToastUtils.init(this, new ToastStrategy() {

* 兼容性强:[处理原生 Toast 在 Android 7.1 产生崩溃的历史遗留问题](https://www.jianshu.com/p/437f473017d6)

* 功能强大:不分主次线程都可以弹出Toast,自动区分资源 id 和 int 类型
* 功能强大:不分主次线程都可以弹出Toast,自动识别资源 id 和 int 类型

* 使用简单:只需传入文本,会自动根据文本长度决定吐司显示的时长

Expand All @@ -201,7 +203,7 @@ ToastUtils.init(this, new ToastStrategy() {
* 在项目中右击弹出菜单,Replace in path,勾选 Regex 选项,点击替换

```text
Toast\.makeText\([^,]+,\s*(.+{1}),\s*[^,]+\)\.show\(\)
Toast\.makeText\([^,]+,\s*(.+),\s*[^,]+\)\.show\(\)
```

```text
Expand Down Expand Up @@ -251,6 +253,8 @@ new Toast

* Android 代码规范:[AndroidCodeStandard](https://github.com/getActivity/AndroidCodeStandard) ![](https://img.shields.io/github/stars/getActivity/AndroidCodeStandard.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidCodeStandard.svg)

* Android 开源排行榜:[AndroidGithubBoss](https://github.com/getActivity/AndroidGithubBoss) ![](https://img.shields.io/github/stars/getActivity/AndroidGithubBoss.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidGithubBoss.svg)

* Studio 精品插件:[StudioPlugins](https://github.com/getActivity/StudioPlugins) ![](https://img.shields.io/github/stars/getActivity/StudioPlugins.svg) ![](https://img.shields.io/github/forks/getActivity/StudioPlugins.svg)

* 表情包大集合:[EmojiPackage](https://github.com/getActivity/EmojiPackage) ![](https://img.shields.io/github/stars/getActivity/EmojiPackage.svg) ![](https://img.shields.io/github/forks/getActivity/EmojiPackage.svg)
Expand All @@ -261,7 +265,7 @@ new Toast

![](https://raw.githubusercontent.com/getActivity/Donate/master/picture/official_ccount.png)

#### Android 技术分享 QQ 群:78797078
#### Android 技术 Q 群:10047167

#### 如果您觉得我的开源库帮你节省了大量的开发时间,请扫描下方的二维码随意打赏,要是能打赏个 10.24 :monkey_face:就太:thumbsup:了。您的支持将鼓励我继续创作:octocat:

Expand Down
6 changes: 3 additions & 3 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ android {
applicationId "com.hjq.toast.demo"
minSdkVersion 16
targetSdkVersion 31
versionCode 1030
versionName "10.3"
versionCode 1050
versionName "10.5"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}

Expand Down Expand Up @@ -65,7 +65,7 @@ dependencies {
implementation 'com.github.getActivity:TitleBar:9.3'

// 权限请求框架:https://github.com/getActivity/XXPermissions
implementation 'com.github.getActivity:XXPermissions:13.2'
implementation 'com.github.getActivity:XXPermissions:13.6'

// 悬浮窗框架:https://github.com/getActivity/XToast
implementation 'com.github.getActivity:XToast:8.5'
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/com/hjq/toast/demo/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public void show6(View v) {
}

public void show7(View v) {
Snackbar.make(getWindow().getDecorView(), "正在准备跳转到手机桌面,请系好安全带", Snackbar.LENGTH_SHORT).show();
Snackbar.make(getWindow().getDecorView(), "温馨提示:安卓 10 在后台显示 Toast 需要有通知栏权限或者悬浮窗权限的情况下才可以显示", Snackbar.LENGTH_SHORT).show();

v.postDelayed(new Runnable() {
@Override
Expand All @@ -98,7 +98,7 @@ public void run() {
if (XXPermissions.isGranted(MainActivity.this, Permission.SYSTEM_ALERT_WINDOW)) {
ToastUtils.show("我是在后台显示的 Toast(有悬浮窗权限真的可以为所欲为)");
} else {
ToastUtils.show("我是在后台显示的 Toast(Android 11 及以上在后台显示 Toast 只能使用系统样式)");
ToastUtils.show("我是在后台显示的 Toast(安卓 11 及以上在后台显示只能使用系统样式)");
}
} else {
ToastUtils.show("我是在后台显示的 Toast");
Expand Down
4 changes: 2 additions & 2 deletions library/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ android {

defaultConfig {
minSdkVersion 14
versionCode 1030
versionName "10.3"
versionCode 1050
versionName "10.5"
}

// 使用 JDK 1.8
Expand Down
27 changes: 26 additions & 1 deletion library/src/main/java/com/hjq/toast/ToastImpl.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package com.hjq.toast;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.graphics.PixelFormat;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.widget.Toast;

/**
Expand Down Expand Up @@ -98,8 +102,28 @@ private boolean isMainThread() {
return Looper.myLooper() == Looper.getMainLooper();
}

/**
* 发送无障碍事件
*/
private void trySendAccessibilityEvent(View view) {
final Context context = view.getContext();
AccessibilityManager accessibilityManager =
(AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
if (!accessibilityManager.isEnabled()) {
return;
}
// 将 Toast 视为通知,因为它们用于向用户宣布短暂的信息
AccessibilityEvent event = AccessibilityEvent.obtain(
AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
event.setClassName(Toast.class.getName());
event.setPackageName(context.getPackageName());
view.dispatchPopulateAccessibilityEvent(event);
accessibilityManager.sendAccessibilityEvent(event);
}

private final Runnable mShowRunnable = new Runnable() {

@SuppressLint("WrongConstant")
@Override
public void run() {

Expand Down Expand Up @@ -142,7 +166,8 @@ public void run() {
mWindowLifecycle.register(ToastImpl.this);
// 当前已经显示
setShow(true);

// 发送无障碍事件
trySendAccessibilityEvent(mToast.getView());
} catch (IllegalStateException | WindowManager.BadTokenException e) {
// 如果这个 View 对象被重复添加到 WindowManager 则会抛出异常
// java.lang.IllegalStateException: View android.widget.TextView has already been added to the window manager.
Expand Down
11 changes: 7 additions & 4 deletions library/src/main/java/com/hjq/toast/ToastStrategy.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.hjq.toast;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AppOpsManager;
import android.app.Application;
Expand Down Expand Up @@ -65,12 +66,13 @@ public void bindStyle(IToastStyle<?> style) {
public IToast createToast(Application application) {
Activity foregroundActivity = mActivityStack.getForegroundActivity();
IToast toast;
if (foregroundActivity != null) {
toast = new ActivityToast(foregroundActivity);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
Settings.canDrawOverlays(application)) {
// 如果有悬浮窗权限,就开启全局的 Toast
toast = new WindowToast(application);
} else if (foregroundActivity != null) {
// 如果没有悬浮窗权限,就开启一个依附于 Activity 的 Toast
toast = new ActivityToast(foregroundActivity);
} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1) {
// 处理 Android 7.1 上 Toast 在主线程被阻塞后会导致报错的问题
toast = new SafeToast(application);
Expand Down Expand Up @@ -105,7 +107,7 @@ public void showToast(CharSequence text, long delayMillis) {
mLatestText = text;
HANDLER.removeCallbacks(mShowRunnable);
// 延迟一段时间之后再执行,因为在没有通知栏权限的情况下,Toast 只能显示当前 Activity
// 如果当前 Activity 在 ToastUtils.show 之后进行 finish 了,那么这个时候 Toast 可能会显示不出来
// 如果当前 Activity 在 showToast 之后立马进行 finish 了,那么这个时候 Toast 可能会显示不出来
// 因为 Toast 会显示在销毁 Activity 界面上,而不会显示在新跳转的 Activity 上面
HANDLER.postDelayed(mShowRunnable, delayMillis + DELAY_TIMEOUT);
}
Expand Down Expand Up @@ -173,6 +175,7 @@ protected int getToastDuration(CharSequence text) {
* 是否有通知栏权限
*/
@SuppressWarnings("ConstantConditions")
@SuppressLint("PrivateApi")
protected boolean areNotificationsEnabled(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return context.getSystemService(NotificationManager.class).areNotificationsEnabled();
Expand Down
12 changes: 3 additions & 9 deletions library/src/main/java/com/hjq/toast/style/BlackToastStyle.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,21 +40,19 @@ public TextView createView(Context context) {

textView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));

Drawable background = getBackgroundDrawable(context);
Drawable backgroundDrawable = getBackgroundDrawable(context);
// 设置背景
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
textView.setBackground(background);
textView.setBackground(backgroundDrawable);
} else {
textView.setBackgroundDrawable(background);
textView.setBackgroundDrawable(backgroundDrawable);
}

// 设置 Z 轴阴影
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
textView.setZ(getTranslationZ(context));
}

// 设置最大显示行数
textView.setMaxLines(getMaxLines(context));
return textView;
}

Expand Down Expand Up @@ -90,8 +88,4 @@ protected Drawable getBackgroundDrawable(Context context) {
protected float getTranslationZ(Context context) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, context.getResources().getDisplayMetrics());
}

protected int getMaxLines(Context context) {
return 5;
}
}

0 comments on commit c974a7e

Please sign in to comment.