diff --git a/README.md b/README.md index eea0a9b..a132cc7 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ > 已投入公司项目多时,没有任何毛病,可胜任任何需求,[点击此处下载Demo](ToastUtils.apk) -> 想了解实现原理的可以点击此链接查看:[ToastUtils](https://github.com/getActivity/ToastUtils/blob/master/library/src/main/java/com/hjq/toast/ToastUtils.java) 源码 +> 想了解实现原理的可以点击此链接查看:[ToastUtils](library/src/main/java/com/hjq/toast/ToastUtils.java) 源码 ![](ToastUtils.gif) @@ -14,61 +14,69 @@ #### 集成步骤 - dependencies { - implementation 'com.hjq:toast:8.6' - } +```groovy +dependencies { + implementation 'com.hjq:toast:8.8' +} +``` #### 初始化 Toast - // 在 Application 中初始化 - ToastUtils.init(this); +```java +// 在 Application 中初始化 +ToastUtils.init(this); +``` #### 显示 Toast - ToastUtils.show("我是吐司"); +```java +ToastUtils.show("我是吐司"); +``` #### 其他 API - // 设置Toast布局 - ToastUtils.setView(); +```java +// 设置Toast布局 +ToastUtils.setView(); - // 设置吐司重心 - ToastUtils.setGravity(); +// 设置吐司重心 +ToastUtils.setGravity(); - // 获取Toast对象 - ToastUtils.getToast(); +// 获取Toast对象 +ToastUtils.getToast(); +``` #### 自定义Toast样式 -> 如果对Toast的默认样式不满意,可以在Application初始化样式,具体可参考[ToastBlackStyle](https://github.com/getActivity/ToastUtils/blob/master/library/src/main/java/com/hjq/toast/style/ToastBlackStyle.java)类的实现 +> 如果对Toast的默认样式不满意,可以在Application初始化样式,具体可参考[ToastBlackStyle](library/src/main/java/com/hjq/toast/style/ToastBlackStyle.java)类的实现 - ToastUtils.initStyle(new IToastStyle()); - -#### 混淆规则 - - -keep class com.hjq.toast.** {*;} +```java +ToastUtils.initStyle(new IToastStyle()); +``` #### 框架亮点 * 无需权限:不管有没有授予通知栏权限都不影响吐司的弹出 -* 功能强大:不分主次线程都可以弹出Toast,自动区分资源id和int类型 +* 兼容性强:处理原生 Toast 在 Android 7.1 产生崩溃的历史遗留问题 + +* 功能强大:不分主次线程都可以弹出Toast,自动区分资源 id 和 int 类型 * 使用简单:只需传入文本,会自动根据文本长度决定吐司显示的时长 -* 性能最佳:单例吐司,整个Toast只有一个TextView,并且通过代码创建 +* 性能最佳:单例吐司,整个 Toast 只有一个 TextView,并且通过代码创建 -* 体验最优:限制Toast短时间内弹出的次数,避免频繁弹出造成不良的用户体验 +* 体验最优:限制 Toast 短时间内弹出的次数,避免频繁弹出造成不良的用户体验 -* 支持多种样式:默认为黑色样式,夜间模式可使用白色样式,还有仿QQ吐司样式 +* 支持多种样式:默认为黑色样式,夜间模式可使用白色样式,还有仿 QQ 吐司样式 * 支持自定义样式:吐司(背景、圆角、重心、偏移),文字(大小、颜色、边距) -* 支持自定义扩展:支持获取ToastUtils中的Toast对象,支持重新自定义Toast布局 +* 支持自定义扩展:支持获取 ToastUtils 中的 Toast 对象,支持重新自定义 Toast 布局 -* 支持全局配置样式:可以在Application中初始化Toast样式,达到一劳永逸的效果 +* 支持全局配置样式:可以在 Application 中初始化 Toast 样式,达到一劳永逸的效果 -* 框架兼容性良好:本框架不依赖任何第三方库,支持 Eclipse 和 Studio 的集成使用 +* 已适配 Android R:Android 11 之后不能弹出自定义样式的 Toast,框架针对这种情况进行了适配 #### 关于通知栏权限 @@ -90,23 +98,37 @@ > 右击项目,Replace in path,勾选 Regex 选项 - Toast\.makeText\([^,]+,\s(.+{1}),\s[^,]+\)\.show\(\); +```java +Toast\.makeText\([^,]+,\s*(.+{1}),\s*[^,]+\)\.show\(\) +``` > 替换使用 - ToastUtils.show($1); +```java +ToastUtils.show($1) +``` > 包名替换 - import android.widget.Toast; +```java +import android.widget.Toast +``` --- - import com.hjq.toast.ToastUtils; +```java +import com.hjq.toast.ToastUtils +``` + +> 再全局搜索,手动更换一些没有替换成功的 + +```java +Toast.makeText +``` #### 作者的其他开源项目 -* 架构工程:[AndroidProject](https://github.com/getActivity/AndroidProject) +* 安卓架构:[AndroidProject](https://github.com/getActivity/AndroidProject) * 网络框架:[EasyHttp](https://github.com/getActivity/EasyHttp) diff --git a/ToastUtils.apk b/ToastUtils.apk index 732ee21..46810e6 100644 Binary files a/ToastUtils.apk and b/ToastUtils.apk differ diff --git a/app/build.gradle b/app/build.gradle index 3db9afa..fe342da 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.hjq.toast.demo" minSdkVersion 14 targetSdkVersion 30 - versionCode 86 - versionName "8.6" + versionCode 88 + versionName "8.8" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { @@ -22,10 +22,10 @@ android { dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') implementation project(':library') - implementation 'androidx.appcompat:appcompat:1.3.0-alpha01' - implementation 'com.google.android.material:material:1.3.0-alpha01' + implementation 'androidx.appcompat:appcompat:1.3.0-alpha02' + implementation 'com.google.android.material:material:1.3.0-alpha02' // 标题栏:https://github.com/getActivity/TitleBar - implementation 'com.hjq:titlebar:6.5' + implementation 'com.hjq:titlebar:8.0' // 悬浮窗:https://github.com/getActivity/XToast implementation 'com.hjq:xtoast:5.5' // 内存泄漏捕捉:https://github.com/square/leakcanary diff --git a/app/src/main/java/com/hjq/toast/demo/ToastActivity.java b/app/src/main/java/com/hjq/toast/demo/ToastActivity.java index bd6ff3f..91579c8 100644 --- a/app/src/main/java/com/hjq/toast/demo/ToastActivity.java +++ b/app/src/main/java/com/hjq/toast/demo/ToastActivity.java @@ -7,7 +7,7 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.NotificationManagerCompat; -import com.hjq.toast.SupportToast; +import com.hjq.toast.CustomToast; import com.hjq.toast.ToastUtils; import com.hjq.toast.style.ToastAliPayStyle; import com.hjq.toast.style.ToastBlackStyle; @@ -87,10 +87,10 @@ protected void onRestart() { // 请注意这段代码强烈建议不要放到实际开发中,因为用户屏蔽通知栏和开启应用状态下的概率极低,可以忽略不计 // 如果通知栏的权限被手动关闭了 - if (!SupportToast.class.equals(ToastUtils.getToast().getClass()) && + if (!CustomToast.class.equals(ToastUtils.getToast().getClass()) && !NotificationManagerCompat.from(this).areNotificationsEnabled()) { // 因为吐司只有初始化的时候才会判断通知权限有没有开启,根据这个通知开关来显示原生的吐司还是兼容的吐司 - ToastUtils.setToast(new SupportToast(getApplication())); + ToastUtils.setToast(new CustomToast(getApplication())); getWindow().getDecorView().postDelayed(new Runnable() { @Override public void run() { diff --git a/library/build.gradle b/library/build.gradle index 2a3f658..aef2282 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -2,13 +2,12 @@ apply plugin: 'com.android.library' apply plugin: 'com.novoda.bintray-release' android { - compileSdkVersion 26 + compileSdkVersion 30 defaultConfig { - minSdkVersion 3 - targetSdkVersion 26 - versionCode 86 - versionName "8.6" + minSdkVersion 4 + versionCode 88 + versionName "8.8" } } @@ -16,7 +15,7 @@ publish { userOrg = 'getactivity' groupId = 'com.hjq' artifactId = 'toast' - version = '8.6' + version = '8.8' description = 'This is a very functional Toast' website = "https://github.com/getActivity/ToastUtils" } diff --git a/library/src/main/java/com/hjq/toast/SupportToast.java b/library/src/main/java/com/hjq/toast/CustomToast.java similarity index 84% rename from library/src/main/java/com/hjq/toast/SupportToast.java rename to library/src/main/java/com/hjq/toast/CustomToast.java index 800d58e..762505e 100644 --- a/library/src/main/java/com/hjq/toast/SupportToast.java +++ b/library/src/main/java/com/hjq/toast/CustomToast.java @@ -7,9 +7,9 @@ * author : Android 轮子哥 * github : https://github.com/getActivity/ToastUtils * time : 2018/11/02 - * desc : Toast 无通知栏权限兼容 + * desc : 自定义 Toast(用于解决关闭通知栏权限之后不能弹吐司的问题和 Android 11 不能自定义吐司样式的问题) */ -public final class SupportToast extends BaseToast { +public final class CustomToast extends NormalToast { /** 吐司弹窗显示辅助类 */ private final ToastHelper mToastHelper; @@ -27,20 +27,20 @@ public final class SupportToast extends BaseToast { /** 垂直间距百分比 */ private float mVerticalMargin; - public SupportToast(Application application) { + public CustomToast(Application application) { super(application); mToastHelper = new ToastHelper(this, application); } @Override public void show() { - // 显示吐司 + // 替换成 WindowManager 来显示 mToastHelper.show(); } @Override public void cancel() { - // 取消显示 + // 取消 WindowManager 的显示 mToastHelper.cancel(); } diff --git a/library/src/main/java/com/hjq/toast/IToastStrategy.java b/library/src/main/java/com/hjq/toast/IToastStrategy.java index c3fc012..c7c277d 100644 --- a/library/src/main/java/com/hjq/toast/IToastStrategy.java +++ b/library/src/main/java/com/hjq/toast/IToastStrategy.java @@ -1,5 +1,6 @@ package com.hjq.toast; +import android.app.Application; import android.widget.Toast; /** @@ -16,7 +17,12 @@ public interface IToastStrategy { int LONG_DURATION_TIMEOUT = 3500; /** - * 绑定 Toast 对象 + * 创建 Toast + */ + Toast create(Application application); + + /** + * 绑定 Toast */ void bind(Toast toast); diff --git a/library/src/main/java/com/hjq/toast/BaseToast.java b/library/src/main/java/com/hjq/toast/NormalToast.java similarity index 94% rename from library/src/main/java/com/hjq/toast/BaseToast.java rename to library/src/main/java/com/hjq/toast/NormalToast.java index b08e77c..3543cb9 100644 --- a/library/src/main/java/com/hjq/toast/BaseToast.java +++ b/library/src/main/java/com/hjq/toast/NormalToast.java @@ -10,14 +10,14 @@ * author : Android 轮子哥 * github : https://github.com/getActivity/ToastUtils * time : 2018/11/03 - * desc : Toast 基类 + * desc : 普通的 Toast */ -public class BaseToast extends Toast { +public class NormalToast extends Toast { /** 吐司消息 View */ private TextView mMessageView; - public BaseToast(Application application) { + public NormalToast(Application application) { super(application); } diff --git a/library/src/main/java/com/hjq/toast/SafeToast.java b/library/src/main/java/com/hjq/toast/SafeToast.java index aab344c..4b63a4f 100644 --- a/library/src/main/java/com/hjq/toast/SafeToast.java +++ b/library/src/main/java/com/hjq/toast/SafeToast.java @@ -12,10 +12,10 @@ * author : Android 轮子哥 * github : https://github.com/getActivity/ToastUtils * time : 2018/12/06 - * desc : Toast 显示安全处理 + * desc : Toast 崩溃处理 */ @TargetApi(Build.VERSION_CODES.KITKAT) -public final class SafeToast extends BaseToast { +public final class SafeToast extends NormalToast { public SafeToast(Application application) { super(application); diff --git a/library/src/main/java/com/hjq/toast/ToastStrategy.java b/library/src/main/java/com/hjq/toast/ToastStrategy.java index a4ae389..89386eb 100644 --- a/library/src/main/java/com/hjq/toast/ToastStrategy.java +++ b/library/src/main/java/com/hjq/toast/ToastStrategy.java @@ -1,10 +1,18 @@ package com.hjq.toast; +import android.app.AppOpsManager; +import android.app.Application; +import android.app.NotificationManager; +import android.content.Context; +import android.os.Build; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.widget.Toast; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.Queue; import java.util.concurrent.ArrayBlockingQueue; @@ -43,6 +51,36 @@ public ToastStrategy() { mQueue = getToastQueue(); } + @Override + public Toast create(Application application) { + Toast toast; + // 初始化吐司 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + // 适配 Android 11 无法使用自定义 Toast 的问题 + // 官方文档:https://developer.android.google.cn/preview/features/toasts + toast = new CustomToast(application); + } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1) { + // 处理 Android 7.1 上 Toast 在主线程被阻塞后会导致报错的问题 + toast = new SafeToast(application); + } else { + boolean check = + // 对比不同版本的 NMS 的源码发现这个问题在 Android 9.0 已经被谷歌修复了 + Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q || + // 判断当前应用是否有通知栏权限,如果关闭会导致弹 Toast 无法显示 + areNotificationsEnabled(application) || + // 判断当前是否是小米手机,因为只有小米手机做了特殊处理,就算没有通知栏权限也能弹吐司 + "xiaomi".equals(Build.MANUFACTURER.toLowerCase()); + if (check) { + // 检查通过,返回正常类型的 Toast 即可 + toast = new NormalToast(application); + } else { + // 修复关闭通知栏权限后 Toast 不显示的问题 + toast = new CustomToast(application); + } + } + return toast; + } + @Override public void bind(Toast toast) { mToast = toast; @@ -122,7 +160,30 @@ public Queue getToastQueue() { * 根据文本来获取吐司的显示时长 */ public int getToastDuration (CharSequence text) { - // 如果显示的文字超过了10个就显示长吐司,否则显示短吐司 + // 如果显示的文字超过了 20 个字符就显示长吐司,否则显示短吐司 return text.length() > 20 ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT; } + + /** + * 检查通知栏权限有没有开启 + * + * 参考 SupportCompat 包中的方法: NotificationManagerCompat.from(context).areNotificationsEnabled(); + */ + private static boolean areNotificationsEnabled(Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return context.getSystemService(NotificationManager.class).areNotificationsEnabled(); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + AppOpsManager appOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); + try { + Method method = appOps.getClass().getMethod("checkOpNoThrow", Integer.TYPE, Integer.TYPE, String.class); + Field field = appOps.getClass().getDeclaredField("OP_POST_NOTIFICATION"); + int value = (Integer) field.get(Integer.class); + return ((int) method.invoke(appOps, value, context.getApplicationInfo().uid, context.getPackageName())) == AppOpsManager.MODE_ALLOWED; + } catch (NoSuchMethodException | NoSuchFieldException | InvocationTargetException | IllegalAccessException | RuntimeException ignored) { + return true; + } + } else { + return true; + } + } } \ No newline at end of file diff --git a/library/src/main/java/com/hjq/toast/ToastUtils.java b/library/src/main/java/com/hjq/toast/ToastUtils.java index 2c0c4d2..1025185 100644 --- a/library/src/main/java/com/hjq/toast/ToastUtils.java +++ b/library/src/main/java/com/hjq/toast/ToastUtils.java @@ -1,10 +1,9 @@ package com.hjq.toast; -import android.app.AppOpsManager; +import android.app.Activity; import android.app.Application; -import android.app.NotificationManager; +import android.app.Service; import android.content.Context; -import android.content.pm.ApplicationInfo; import android.content.res.Resources; import android.graphics.drawable.GradientDrawable; import android.os.Build; @@ -19,10 +18,6 @@ import com.hjq.toast.style.ToastQQStyle; import com.hjq.toast.style.ToastWhiteStyle; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - /** * author : Android 轮子哥 * github : https://github.com/getActivity/ToastUtils @@ -31,19 +26,22 @@ */ public final class ToastUtils { + /** Toast 拦截器 */ private static IToastInterceptor sInterceptor; + /** Toast 处理策略 */ private static IToastStrategy sStrategy; + /** Toast 单例对象 */ private static Toast sToast; /** - * 不允许外部实例化 + * 不允许被外部实例化 */ private ToastUtils() {} /** - * 初始化 ToastUtils,在 Application 中初始化 + * 初始化 Toast,需要在 Application.create 中初始化 * * @param application 应用的上下文 */ @@ -52,7 +50,7 @@ public static void init(Application application) { } /** - * 初始化 ToastUtils 及样式 + * 初始化 Toast 及样式 */ public static void init(Application application, IToastStyle style) { checkNullPointer(application); @@ -66,22 +64,8 @@ public static void init(Application application, IToastStyle style) { setToastStrategy(new ToastStrategy()); } - // 初始化吐司 - if (areNotificationsEnabled(application)) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O + 4) { - // 适配 Android 11 无法使用自定义 Toast 的问题 - // 官方文档:https://developer.android.google.cn/preview/features/toasts - setToast(new SupportToast(application)); - } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1) { - // 解决 Android 7.1 上主线程被阻塞后吐司会报错的问题 - setToast(new SafeToast(application)); - } else { - setToast(new BaseToast(application)); - } - } else { - // 解决关闭通知栏权限后 Toast 不显示的问题 - setToast(new SupportToast(application)); - } + // 创建 Toast 对象 + setToast(sStrategy.create(application)); // 设置 Toast 视图 setView(createTextView(application, style)); @@ -117,26 +101,6 @@ public static void show(int id) { } } - /** - * 显示一个吐司 - * - * @param id 资源 id - * @param args 参数集 - */ - public static void show(int id, Object... args) { - show(getContext().getResources().getString(id), args); - } - - /** - * 显示一个吐司 - * - * @param format 原字符串 - * @param args 参数集 - */ - public static void show(String format, Object... args) { - show(String.format(format, args)); - } - /** * 显示一个吐司 * @@ -173,14 +137,14 @@ public static void setGravity(int gravity, int xOffset, int yOffset) { // 适配 Android 4.2 新特性,布局反方向(开发者选项 - 强制使用从右到左的布局方向) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - gravity = Gravity.getAbsoluteGravity(gravity, sToast.getView().getResources().getConfiguration().getLayoutDirection()); + gravity = Gravity.getAbsoluteGravity(gravity, getContext().getResources().getConfiguration().getLayoutDirection()); } sToast.setGravity(gravity, xOffset, yOffset); } /** - * 给当前Toast设置新的布局,具体实现可看{@link BaseToast#setView(View)} + * 给当前Toast设置新的布局,具体实现可看{@link NormalToast#setView(View)} */ public static void setView(int id) { checkToastState(); @@ -195,7 +159,7 @@ public static void setView(View view) { // 当前必须用 Application 的上下文创建的 View,否则可能会导致内存泄露 Context context = view.getContext(); - if (!(context instanceof Application)) { + if (context instanceof Activity || context instanceof Service) { throw new IllegalArgumentException("The view must be initialized using the context of the application"); } @@ -256,9 +220,9 @@ public static void setToast(Toast toast) { /** * 设置 Toast 显示策略 */ - public static void setToastStrategy(IToastStrategy handler) { - checkNullPointer(handler); - sStrategy = handler; + public static void setToastStrategy(IToastStrategy strategy) { + checkNullPointer(strategy); + sStrategy = strategy; if (sToast != null) { sStrategy.bind(sToast); } @@ -300,22 +264,15 @@ private static void checkNullPointer(Object object) { } /** - * 生成默认的 TextView 对象 + * 根据样式生成默认的 TextView 对象 */ private static TextView createTextView(Context context, IToastStyle style) { - - GradientDrawable drawable = new GradientDrawable(); - // 设置背景色 - drawable.setColor(style.getBackgroundColor()); - // 设置圆角大小 - drawable.setCornerRadius(style.getCornerRadius()); - TextView textView = new TextView(context); textView.setId(android.R.id.message); textView.setTextColor(style.getTextColor()); textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, style.getTextSize()); - // 适配布局反方向 + // 适配布局反方向特性 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { textView.setPaddingRelative(style.getPaddingStart(), style.getPaddingTop(), style.getPaddingEnd(), style.getPaddingBottom()); } else { @@ -324,6 +281,12 @@ private static TextView createTextView(Context context, IToastStyle style) { textView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + GradientDrawable drawable = new GradientDrawable(); + // 设置背景色 + drawable.setColor(style.getBackgroundColor()); + // 设置圆角大小 + drawable.setCornerRadius(style.getCornerRadius()); + // setBackground API 版本兼容 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { textView.setBackground(drawable); @@ -351,33 +314,4 @@ private static Context getContext() { checkToastState(); return sToast.getView().getContext(); } - - /** - * 检查通知栏权限有没有开启 - * 参考 SupportCompat 包中的方法: NotificationManagerCompat.from(context).areNotificationsEnabled(); - */ - private static boolean areNotificationsEnabled(Context context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - return manager != null && manager.areNotificationsEnabled(); - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - AppOpsManager appOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); - ApplicationInfo appInfo = context.getApplicationInfo(); - String packageName = context.getApplicationContext().getPackageName(); - int uid = appInfo.uid; - - try { - Class appOpsClass = Class.forName(AppOpsManager.class.getName()); - Method checkOpNoThrowMethod = appOpsClass.getMethod("checkOpNoThrow", Integer.TYPE, Integer.TYPE, String.class); - Field opPostNotificationValue = appOpsClass.getDeclaredField("OP_POST_NOTIFICATION"); - int value = (Integer) opPostNotificationValue.get(Integer.class); - return ((int) checkOpNoThrowMethod.invoke(appOps, value, uid, packageName) == AppOpsManager.MODE_ALLOWED); - } catch (ClassNotFoundException | NoSuchMethodException | NoSuchFieldException - | InvocationTargetException | IllegalAccessException | RuntimeException ignored) { - return true; - } - } else { - return true; - } - } } \ No newline at end of file