系统原生的Toast是用了INotificationManager类来显示的, Android 5.0以上系统用户只要关闭了通知权限,在大部分手机上Toast也将不能显示(有部分国产手机5.0以上的系统禁了通知权限仍能显示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
}
}
针对这种情况有以下几种解决方案:
3、使用Dialog或PopupWindow实现Toast(推荐)
1、提醒用户打开通知权限
优点:有了通知权限其他都不是问题;
缺点:是否打开权限取决于用户;
针对不同版本系统获取通知权限是否打开的方法也不一样,4.4之前的系统没有通知权限默认true,4.4到7.0系统没有直接获取通知权限方法需要通过反射获取,7.0以上系统可以使用NotificationManager#areNotificationsEnabled()方法直接获取应用是否有通知权限。
/**
* 检查通知栏权限有没有开启
*/
public static boolean isNotificationEnabled(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)).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 pkg = 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 (Integer) checkOpNoThrowMethod.invoke(appOps, value, uid, pkg) == 0;
} catch (NoSuchMethodException | NoSuchFieldException | InvocationTargetException | IllegalAccessException | RuntimeException | ClassNotFoundException ignored) {
return true;
}
} else {
return true;
}
}
虽然有些用户是不小心关闭通知权限的,但某些用户就是不想接受应用的通知才关闭的权限,让他们打开很大可能也会选择拒绝,这种方案只能pass。
2、让系统认定为系统Toast(推荐)
优点:对原先使用那套ToastUtil改动最小;
缺点:国内厂商对Android系统定制化严重,可能需要做机型兼容,不过暂时只发现华为P20;
查看系统源码可以看到NotificationManagerService.java里有这样一个判断是否是系统Toast,判断条件只要两者满足其一就行,由此我们只要将enqueueToast的参数pkg改成"android"即可。
通过反射代理INotificationManager,在执行enqueueToast方法时让系统认定为是原生Toast(但由于国内厂商对系统的定制化可能需要做其他兼容性处理,目前只知道华为P20有问题)。
只需要在原来封装的那套ToastUtil方法最终show的地方调用下面的show方法即可。
/**
* 显示
*
* @param context
* @param toast
*/
public static void show(Context context, Toast toast) {
if (context == null || toast == null) {
throw new RuntimeException("context 与 toast不能为null");
}
if (isNotificationEnabled(context)) {
toast.show();
} else {
try {
Method getServiceMethod = Toast.class.getDeclaredMethod("getService");
getServiceMethod.setAccessible(true);
if (iNotificationManagerObject == null) {
iNotificationManagerObject = getServiceMethod.invoke(null);
Class iNotificationManagerClazz = Class.forName("android.app.INotificationManager");
Object iNotificationManagerProxy = Proxy.newProxyInstance(toast.getClass().getClassLoader(), new Class[]{iNotificationManagerClazz}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("enqueueToast".equals(method.getName())//原生系统用了enqueueToast
|| "enqueueToastEx".equals(method.getName())//华为P20用了enqueueToastEx
) {
//强制变成系统Toast
args[0] = "android";
}
return method.invoke(iNotificationManagerObject, args);
}
});
Field field = Toast.class.getDeclaredField("sService");
field.setAccessible(true);
//替换Toast里的sService
field.set(null, iNotificationManagerProxy);
}
toast.show();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
}
3、使用Dialog或PopupWindow实现Toast(推荐)
缺点:需要传当前Activity的上下文;
优点:无需权限在页面上显示完全没问题;
根据应用是否有通知权限分别显示原生Toast和自定义实现的Toast,我这边用了PopupWindow实现了一个Toast。
public void show() {
initToast();
startTimer();
if (Util.isNotificationEnabled(activity)) {
//有权限使用原生Toast
if (contentView != null) {
//视图
setCustomView();
toast.setView(contentParentView);
} else {
//text
toast.setGravity(gravity, offsetX, offsetY);
toast.setText(text);
}
toast.show();
} else {
//无权限使用PopupWindow
if (contentView != null) {
//视图
setCustomView();
popupWindow.setContentView(contentParentView);
} else {
//text
((TextView) defaultView.findViewById(R.id.tv_default_text)).setText(text);
popupWindow.setContentView(defaultView);
}
if (!popupWindow.isShowing()) {
popupWindow.showAtLocation(activity.getWindow().getDecorView(), gravity, offsetX, offsetY);
}
}
}
用计时器来取消显示PopupWindow
/**
* 开始计时
*/
private void startTimer() {
stopTimer();
timer = new Timer();
timerTask = new TimerTask() {
@Override
public void run() {
handler.post(runnable);
}
};
timer.schedule(timerTask, duration == Toast.LENGTH_SHORT ? LENGTH_SHORT : LENGTH_LONG);
}
/**
* 结束计时
*/
private void stopTimer() {
if (timer != null) {
timer.cancel();
timer = null;
}
if (timerTask != null) {
timerTask.cancel();
timerTask = null;
}
}
连续显示View视图土司时,如果直接设置View,
原生Toast会等上一个消失后再显示;
而看PopupWindow#setContentView()方法可以发现如果PopupWindow已经显示是不能再次设置contentView的;
这里先让Toast或PopupWindow添加一个内容容器,通过内容容器的addView和removeView实现良好的用户体验。
/**
* 设置View视图
*/
private void setCustomView() {
contentParentView.removeAllViews();
if (contentView.getParent() != null) {
((ViewGroup) contentView.getParent()).removeView(contentView);
}
contentParentView.addView(contentView);
}
在依赖包里面也封装了一个ToastViewUtil(具体使用),方法与ToastUtil一样。
以上提供了几种Toast在无权限下的解决方案,2、3都是不错的选择。