转载请标明地址 QuincySx: http://www.jianshu.com/p/f59022012132
部分手机可能Toast不显示,换其他手机是正常的
这是因为Toast显示需要NotificationManagerService(查看Android源码)
部分手机把通知权限关闭了,所以Toast无法正常弹出
解决办法跳过NotificationManagerService自己维护一个队列
public class CustomToast implements IToast {
private static Handler mHandler = new Handler();
/**
* 维护toast的队列
*/
private static BlockingQueue<CustomToast> mQueue = 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 是否能显示,如果不能显示则使用自己维护的 Toast
public 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 之间选择