一启具、發(fā)現(xiàn)問(wèn)題
只是想關(guān)閉Notification, 但Toast意外躺槍不顯示了本讥,我的第一想法這不科學(xué)啊,所以去看看源碼WTF?
二拷沸、定位問(wèn)題:
源碼中可發(fā)現(xiàn)
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
}
}
public void cancel() {
mTN.hide();
try {
getService().cancelToast(mContext.getPackageName(), mTN);
} catch (RemoteException e) {
// Empty
}
}
可以看到先是獲取一個(gè)服務(wù)INotificationManager service = getService();旨椒,顯示時(shí)調(diào)用其service.enqueueToast(pkg, tn, mDuration);
而這個(gè)INotificationManager在用戶(hù)關(guān)閉消息通知權(quán)限的同時(shí)被禁用了,所以我們的Toast無(wú)法顯示堵漱。
三综慎、解決方案
經(jīng)過(guò)一番看源碼和在某一篇關(guān)于Toast源碼分析的博文中了解到
Toast的顯示路徑:
- 通過(guò)new Toast(Context context)或者makeText(...)方法實(shí)例化Toast對(duì)象
- 調(diào)用show()方法之后,實(shí)例會(huì)加入到一個(gè)TN變量(AIDL)的服務(wù)隊(duì)列中勤庐,而這個(gè)隊(duì)列由系統(tǒng)維護(hù)
- TN控制Toast的顯示和消息
解決思路就有了:
既然系統(tǒng)不允許我們調(diào)用Toast示惊,那么我們就自立門(mén)戶(hù)——自己寫(xiě)一個(gè)Toast出來(lái)。
我們自己參照Toast的源碼愉镰,重寫(xiě)一份米罚,最后show的時(shí)候,不進(jìn)入TN維護(hù)的隊(duì)列丈探,我們自己用Handler+Queue來(lái)維護(hù)Toast的消息隊(duì)列录择。
public class CustomToast implements IToast {
private static Handler mHandler = new Handler();
/**
* 維護(hù)toast的隊(duì)列
*/
private static BlockingQueue<CustomToast> mQueue = new LinkedBlockingQueue<CustomToast>();
/**
* 原子操作:判斷當(dāng)前是否在讀取{**@linkplain **#mQueue 隊(duì)列}來(lái)顯示toast
*/
protected static AtomicInteger mAtomicInteger = new AtomicInteger(0);
private WindowManager mWindowManager;
private long mDurationMillis;
private View mView;
private WindowManager.LayoutParams mParams;
private Context mContext;
public static IToast makeText(Context context, String text, long duration) {
return new CustomToast(context).setText(text).setDuration(duration)
.setGravity(Gravity.BOTTOM, 0, DisplayUtil.dip2px(context, 64));
}
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;
// 默認(rèn)CustomToast在下方居中
mParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
}
/**
* 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 **自身對(duì)象
*/
@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 **自身對(duì)象
*/
@Override
public IToast setText(String text) {
// 模擬Toast的布局文件 com.android.internal.R.layout.transient_notification
// 雖然可以手動(dòng)用java寫(xiě)隘竭,但是不同廠(chǎng)商系統(tǒng),這個(gè)布局的設(shè)置好像是不同的讼渊,因此我們自己獲取原生Toast的view進(jìn)行配置
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加入到隊(duì)列中
mQueue.offer(this);
// 2. 如果隊(duì)列還沒(méi)有激活动看,就激活隊(duì)列,依次展示隊(duì)列中的toast
if (0 == mAtomicInteger.get()) {
mAtomicInteger.incrementAndGet();
mHandler.post(mActivite);
}
}
@Override
public void cancel() {
// 1. 如果隊(duì)列已經(jīng)處于非激活狀態(tài)或者隊(duì)列沒(méi)有toast了爪幻,就表示隊(duì)列沒(méi)有toast正在展示了菱皆,直接return
if (0 == mAtomicInteger.get() && mQueue.isEmpty()) {
return;
}
// 2. 當(dāng)前顯示的toast是否為本次要取消的toast,如果是的話(huà)
// 2.1 先移除之前的隊(duì)列邏輯
// 2.2 立即暫停當(dāng)前顯示的toast
// 2.3 重新激活隊(duì)列
if (this.equals(mQueue.peek())) {
mHandler.removeCallbacks(mActivite);
mHandler.post(mHide);
mHandler.post(mActivite);
}
//TODO 如果一個(gè)Toast在隊(duì)列中的等候展示挨稿,當(dāng)調(diào)用了這個(gè)toast的取消時(shí)仇轻,考慮是否應(yīng)該從對(duì)隊(duì)列中移除,看產(chǎn)品需求吧
}
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);
// 同時(shí)從隊(duì)列中移除這個(gè)toast
mQueue.poll();
}
mView = null;
}
}
private static void activeQueue() {
CustomToast miuiToast = mQueue.peek();
if (miuiToast == null) {
// 如果不能從隊(duì)列中獲取到toast的話(huà)奶甘,那么就表示已經(jīng)暫時(shí)完所有的toast了
// 這個(gè)時(shí)候需要標(biāo)記隊(duì)列狀態(tài)為:非激活讀取中
mAtomicInteger.decrementAndGet();
} else {
// 如果還能從隊(duì)列中獲取到toast的話(huà)篷店,那么就表示還有toast沒(méi)有展示
// 1. 展示隊(duì)首的toast
// 2. 設(shè)置一定時(shí)間后主動(dòng)采取toast消失措施
// 3. 設(shè)置展示完畢之后再次執(zhí)行本邏輯,以展示下一個(gè)toast
mHandler.post(miuiToast.mShow);
mHandler.postDelayed(miuiToast.mHide, miuiToast.mDurationMillis);
mHandler.postDelayed(mActivite, miuiToast.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();
}
};
}
四甩十、使用方法
問(wèn)題解決后船庇,想到這是一個(gè)通用性的問(wèn)題吭产,可以搞一個(gè)庫(kù)出來(lái)共享侣监,所以就打成了aar上傳到我們的maven私服,便于復(fù)用臣淤。
compile 'xsl.common:toaster:1.0.1'
Toaster實(shí)現(xiàn)了自定義的IToast接口橄霉,IToast的接口方法基本和原來(lái)的Toast相差無(wú)幾, 所以從系統(tǒng)的Toast轉(zhuǎn)到我們自定義的Toaster的成本極低,其實(shí)就是改個(gè)名字而已邑蒋,其他用法完全一致姓蜂。
//System Toast
Toast.makeText(MainActivity.this, "show System Toast", Toast.LENGTH_SHORT).show();
//Custom Toast
Toaster.makeText(this, "show Custom Toast", Toast.LENGTH_SHORT).show();
五按厘、發(fā)散思維
還有什么別的解決思路?
自己仿照系統(tǒng)的Toast然后用自己的消息隊(duì)列來(lái)維護(hù)钱慢,讓其不受NotificationManagerService影響逮京。(本文采用)
通過(guò)WindowManager自己來(lái)寫(xiě)一個(gè)通知。
通過(guò)Dialog束莫、PopupWindow來(lái)編寫(xiě)一個(gè)自定義通知懒棉。
通過(guò)直接去當(dāng)前頁(yè)面最外層content布局來(lái)添加View。