使用WindowManager自定义toast

Android 原生的 Toast 十分好用,功能也很强大,扩展性也十分 OK,但是有一个致命的缺点:一旦用户屏蔽了 App 的通知权限,则 Toast 也不会显示了!

对于一个上线的产品,这个当然是不能忍的。于是我们会用一些方式来规避这个问题。

首先可能会想到的,是实现一个自定义的 ToastView,在合适的时间将 View 显示在 android.R.id.content 这个 ViewGroup 里面。但是这样并不是十分管用,因为这样的话, ToastView 会和 Acitivity 绑定在一起,当 Activity 不可见时,相应的 Toast 也不可见了。

此时,我们想到另外一种方式,是通过 WindowManager 来处理 ToastView. WindowManager 管理着每个窗口的前后顺序,如果我们把 ToastView 添加到 WindowManager 里面,则就可以一直显示在屏幕上啦。

知道了方法,我们就开发做。其实代码也很简单,直接贴出来了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class Toast {
public static final int LENGTH_SHORT = 0;
public static final int LENGTH_LONG = 1;
private static final int LONG_DELAY = 3500;
private static final int SHORT_DELAY = 2000;
private static Context context;
private static WindowManager windowManager;

public static void init(Context context) {
Toast.context = context.getApplicationContext();
windowManager = (WindowManager) context.getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
}

public static void show(String text) {
show(text, LENGTH_SHORT);
}

public static void show(final String text, int length) {
final TextView textView = new TextView(context);
textView.setBackgroundResource(android.R.drawable.toast_frame); //设置成官方原生的 Toast 背景
textView.setText(text);

WindowManager.LayoutParams lp = new WindowManager.LayoutParams();
lp.type = WindowManager.LayoutParams.TYPE_TOAST;
lp.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; //设置这个 window 不可点击,不会获取焦点,这样可以不干扰背后的 Activity 的交互。
lp.width = WindowManager.LayoutParams.WRAP_CONTENT;
lp.height = WindowManager.LayoutParams.WRAP_CONTENT;
lp.format = PixelFormat.TRANSLUCENT; //这样可以保证 Window 的背景是透明的,不然背景可能是黑色或者白色。
lp.windowAnimations = android.R.style.Animation_Toast; //使用官方原生的 Toast 动画效果

windowManager.addView(textView, lp);
textView.postDelayed(new Runnable() { // 指定时间后,取消 Toast 显示
@Override
public void run() {
windowManager.removeView(textView);
}
}, length == LENGTH_SHORT ? SHORT_DELAY : LONG_DELAY);
}

}

因为这里的 context 和 WindowManager 都是整个 Application 生命周期的,这里就直接用 static 引用了。其余的见注释好了。

其实如果仔细看官方 android.weidget.Toast 的源码,也可以看到,官方的 Toast 也是通过 WindowManager#addView(View) 来实现的。相关代码我也贴出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public void handleShow() {
if (localLOGV) {
Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ " mNextView=" + mNextView);
}
if (mView != mNextView) {
// remove the old view if necessary
handleHide();
mView = mNextView;
Context context = mView.getContext().getApplicationContext();
String packageName = mView.getContext().getOpPackageName();
if (context == null) {
context = mView.getContext();
}
mWM = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
// We can resolve the Gravity here by using the Locale for getting
// the layout direction
final Configuration config = mView.getContext().getResources().getConfiguration();
final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
mParams.gravity = gravity;
if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
mParams.horizontalWeight = 1.0f;
}
if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
mParams.verticalWeight = 1.0f;
}
mParams.x = mX;
mParams.y = mY;
mParams.verticalMargin = mVerticalMargin;
mParams.horizontalMargin = mHorizontalMargin;
mParams.packageName = packageName;
if (mView.getParent() != null) {
if (localLOGV) {
Log.v(TAG, "REMOVE! " + mView + " in " + this);
}
mWM.removeView(mView);
}
if (localLOGV) {
Log.v(TAG, "ADD! " + mView + " in " + this);
}
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
}
}