本文共 11431 字,大约阅读时间需要 38 分钟。
转载请标明地址 QuincySx:
部分手机可能Toast不显示,换其他手机是正常的
这是因为Toast显示需要NotificationManagerService(查看Android源码)
部分手机把通知权限关闭了,所以Toast无法正常弹出
解决办法跳过NotificationManagerService自己维护一个队列
public class CustomToast implements IToast { private static Handler mHandler = new Handler(); /** * 维护toast的队列 */ private static BlockingQueuemQueue = new LinkedBlockingDeque<>(); /** * 原子操作:判断当前是否在读取{@linkplain #mQueue 队列}来显示toast */ private static AtomicInteger mAtomicInteger = new AtomicInteger(0); private WindowManager mWindowManager; private long mDurationMillis; private View mView; private WindowManager.LayoutParams mParams; private Context mContext; public void makeTextShow(String text, long duration) { new CustomToast(mContext) .setText(text) .setDuration(duration) .setGravity(Gravity.BOTTOM, 0, 30).show(); } public static IToast makeText(Context context, String text, long duration) { return new CustomToast(context) .setText(text) .setDuration(duration) .setGravity(Gravity.BOTTOM, 0, 30); } /** * 参照Toast源码TN()写 * * @param context */ public CustomToast(Context context) { mContext = context; mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); mParams = new WindowManager.LayoutParams(); mParams.height = WindowManager.LayoutParams.WRAP_CONTENT; mParams.width = WindowManager.LayoutParams.WRAP_CONTENT; mParams.format = PixelFormat.TRANSLUCENT; mParams.windowAnimations = android.R.style.Animation_Toast; mParams.type = WindowManager.LayoutParams.TYPE_TOAST; mParams.setTitle("Toast"); mParams.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager .LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; // 默认居中 mParams.gravity = Gravity.CENTER; } /** * Set the location at which the notification should appear on the screen. * * @param gravity * @param xOffset * @param yOffset */ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) @Override public IToast setGravity(int gravity, int xOffset, int yOffset) { // We can resolve the Gravity here by using the Locale for getting // the layout direction final int finalGravity; if (Build.VERSION.SDK_INT >= 14) { final Configuration config = mView.getContext().getResources().getConfiguration(); finalGravity = Gravity.getAbsoluteGravity(gravity, config.getLayoutDirection()); } else { finalGravity = gravity; } mParams.gravity = finalGravity; if ((finalGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) { mParams.horizontalWeight = 1.0f; } if ((finalGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) { mParams.verticalWeight = 1.0f; } mParams.y = yOffset; mParams.x = xOffset; return this; } @Override public IToast setDuration(long durationMillis) { if (durationMillis < 0) { mDurationMillis = 0; } if (durationMillis == Toast.LENGTH_SHORT) { mDurationMillis = 2000; } else if (durationMillis == Toast.LENGTH_LONG) { mDurationMillis = 3500; } else { mDurationMillis = durationMillis; } return this; } /** * 不能和{@link #setText(String)}一起使用,要么{@link #setView(View)} 要么{@link #setView(View)} * * @param view 传入view * @return 自身对象 */ @Override public IToast setView(View view) { mView = view; return this; } @Override public IToast setMargin(float horizontalMargin, float verticalMargin) { mParams.horizontalMargin = horizontalMargin; mParams.verticalMargin = verticalMargin; return this; } /** * 不能和{@link #setView(View)}一起使用,要么{@link #setView(View)} 要么{@link #setView(View)} * * @param text 字符串 * @return 自身对象 */ public IToast setText(String text) { // 模拟Toast的布局文件 com.android.internal.R.layout.transient_notification // 虽然可以手动用java写,但是不同厂商系统,这个布局的设置好像是不同的,因此我们自己获取原生Toast的view进行配置 View view = Toast.makeText(mContext, text, Toast.LENGTH_SHORT).getView(); if (view != null) { TextView tv = (TextView) view.findViewById(android.R.id.message); tv.setText(text); setView(view); } return this; } @Override public void show() { // 1. 将本次需要显示的toast加入到队列中 mQueue.offer(this); // 2. 如果队列还没有激活,就激活队列,依次展示队列中的toast if (0 == mAtomicInteger.get()) { mAtomicInteger.incrementAndGet(); mHandler.post(mActivite); } } @Override public void cancel() { // 1. 如果队列已经处于非激活状态或者队列没有toast了,就表示队列没有toast正在展示了,直接return if (0 == mAtomicInteger.get() && mQueue.isEmpty()) return; // 2. 当前显示的toast是否为本次要取消的toast,如果是的话 // 2.1 先移除之前的队列逻辑 // 2.2 立即暂停当前显示的toast // 2.3 重新激活队列 if (this.equals(mQueue.peek())) { mHandler.removeCallbacks(mActivite); mHandler.post(mHide); mHandler.post(mActivite); } } private void handleShow() { if (mView != null) { if (mView.getParent() != null) { mWindowManager.removeView(mView); } mWindowManager.addView(mView, mParams); } } private void handleHide() { if (mView != null) { // note: checking parent() just to make sure the view has // been added... i have seen cases where we get here when // the view isn't yet added, so let's try not to crash. if (mView.getParent() != null) { mWindowManager.removeView(mView); // 同时从队列中移除这个toast mQueue.poll(); } mView = null; } } private static void activeQueue() { CustomToast toast = mQueue.peek(); if (toast == null) { // 如果不能从队列中获取到toast的话,那么就表示已经暂时完所有的toast了 // 这个时候需要标记队列状态为:非激活读取 mAtomicInteger.decrementAndGet(); } else { // 如果还能从队列中获取到toast的话,那么就表示还有toast没有展示 // 1. 展示队首的toast // 2. 设置一定时间后主动采取toast消失措施 // 3. 设置展示完毕之后再次执行本逻辑,以展示下一个toast mHandler.post(toast.mShow); mHandler.postDelayed(toast.mHide, toast.mDurationMillis); mHandler.postDelayed(mActivite, toast.mDurationMillis); } } private final Runnable mShow = new Runnable() { @Override public void run() { handleShow(); } }; private final Runnable mHide = new Runnable() { @Override public void run() { handleHide(); } }; private final static Runnable mActivite = new Runnable() { @Override public void run() { activeQueue(); } };}
public interface IToast { void makeTextShow(String text, long duration); IToast setGravity(int gravity, int xOffset, int yOffset); IToast setDuration(long durationMillis); /** * 不能和{@link #setText(String)}一起使用,要么{@link #setView(View)} 要么{@link #setText(String)} */ IToast setView(View view); IToast setMargin(float horizontalMargin, float verticalMargin); /** * 不能和{@link #setView(View)}一起使用,要么{@link #setView(View)} 要么{@link #setText(String)} */ IToast setText(String text); void show(); void cancel();}
上面是两个是自己实现的Toast队列
我们还写了个工厂 来进行 当 App 初始化的时候判断手机的 Toast 是否能显示,如果不能显示则使用自己维护的 Toastpublic class SystemToast implements IToast { private Toast mToast; private Context mContext; public void makeTextShow(String text, long duration) { new SystemToast(mContext) .setText(text) .setDuration(duration).show(); } public static IToast makeText(Context context, String text, long duration) { return new CustomToast(context) .setText(text) .setDuration(duration); } public SystemToast(Context context) { mContext = context; mToast = Toast.makeText(context, "", Toast.LENGTH_SHORT); } @Override public IToast setGravity(int gravity, int xOffset, int yOffset) { mToast.setGravity(gravity, xOffset, yOffset); return this; } @Override public IToast setDuration(long durationMillis) { mToast.setDuration((int) durationMillis); return this; } /** * 不能和{@link #setText(String)}一起使用,要么{@link #setView(View)} 要么{@link #setView(View)} * * @param view 传入view * @return 自身对象 */ @Override public IToast setView(View view) { mToast.setView(view); return this; } @Override public IToast setMargin(float horizontalMargin, float verticalMargin) { mToast.setMargin(horizontalMargin, verticalMargin); return this; } /** * 不能和{@link #setView(View)}一起使用,要么{@link #setView(View)} 要么{@link #setView(View)} * * @param text 传入字符串 * @return 自身对象 */ @Override public IToast setText(String text) { mToast.setText(text); return this; } @Override public void show() { if (mToast != null) { mToast.show(); } } @Override public void cancel() { if (mToast != null) { mToast.cancel(); } }}
public class ToastFactory { private static final String CHECK_OP_NO_THROW = "checkOpNoThrow"; private static final String OP_POST_NOTIFICATION = "OP_POST_NOTIFICATION"; private int mCheckNotification = -1; private volatile static ToastFactory sToastFactory; private IToast mIToast; private ToastFactory(Context context) { if (isNotificationEnabled(context)) { mIToast = new SystemToast(context); } else { mIToast = new CustomToast(context); } } public static IToast getInstance(Context context) { if (sToastFactory == null) { synchronized (ToastFactory.class) { if (sToastFactory == null) { sToastFactory = new ToastFactory(context); } } } return sToastFactory.mIToast; } private static boolean isNotificationEnabled(Context context) { AppOpsManager mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); ApplicationInfo appInfo = context.getApplicationInfo(); String pkg = context.getApplicationContext().getPackageName(); int uid = appInfo.uid; Class appOpsClass = null; /* Context.APP_OPS_MANAGER */ try { appOpsClass = Class.forName(AppOpsManager.class.getName()); Method checkOpNoThrowMethod = appOpsClass.getMethod(CHECK_OP_NO_THROW, Integer.TYPE, Integer.TYPE, String.class); Field opPostNotificationValue = appOpsClass.getDeclaredField(OP_POST_NOTIFICATION); int value = (int) opPostNotificationValue.get(Integer.class); return ((int) checkOpNoThrowMethod.invoke(mAppOps, value, uid, pkg) == AppOpsManager .MODE_ALLOWED); } catch (Exception e) { e.printStackTrace(); } return true; }}
当用上边这个工厂进行 Toast 显示的时候会在自行维护的 Toast 队列与系统 Toast 之间选择