前一阵在自己的app里面实现了一个截图功能,实现的方法比较粗暴,直接使用的screenshot指令,效果拔群。
1 | String cmd = "screencap -p /sdcard/Pictures/Screenshots/" + timecurrentTimeMillis + ".png"; |
后来有朋友说这么截图比较生硬,因为看不到截图的结果,后来就想加一个toast,弹出一个提示就好了。1
2
3
4
5
6
7
8String cmd = "screencap -p /sdcard/Pictures/Screenshots/" + timecurrentTimeMillis + ".png";
try {
Process p = Runtime.getRuntime().exec(cmd);
Toast.makeText(context,"screenShot success",Toast.LENGTH_SHORT).show();
} catch (IOException e) {
Toast.makeText(context,"screenShot failed",Toast.LENGTH_SHORT).show();
XposedBridge.log(e.getMessage());
}
再后来发现这种情况虽然能成功弹出toast,但是会有一种比较尴尬的情况,截图的同时,toast也出现在截图中了。
当时处理这个问题的想法也很简单,将toast出现的时间稍微往后延一些1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18String cmd = "screencap -p /sdcard/Pictures/Screenshots/" + timecurrentTimeMillis + ".png";
try {
Process p = Runtime.getRuntime().exec(cmd);
new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(1000);
Toast.makeText(context,"screenShot success",Toast.LENGTH_SHORT).show();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
} catch (IOException e) {
Toast.makeText(context,"screenShot failed",Toast.LENGTH_SHORT).show();
XposedBridge.log(e.getMessage());
}
然而使用这种方法之后,报错了。1
2
3
4
5
6
7
8
9
1007-07 22:51:58.478 2346-3283/com.android.systemui E/AndroidRuntime: FATAL EXCEPTION: Thread-63
Process: com.android.systemui, PID: 2346
java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
at android.os.Handler.<init>(Handler.java:200)
at android.os.Handler.<init>(Handler.java:114)
at android.widget.Toast$TN.<init>(Toast.java:345)
at android.widget.Toast.<init>(Toast.java:101)
at android.widget.Toast.makeText(Toast.java:259)
at com.egguncle.xposednavigationbar.hook.btnFunc.BtnScreenShot$1.run(BtnScreenShot.java:76)
at java.lang.Thread.run(Thread.java:818)
查看报错信息,错误是因为没有Looper.prepare引起的,经过测试发现,Toast不能在子线程里面直接弹出。
来看一下toast的源码
http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/java/android/widget/Toast.java1
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构造方法
99 public Toast(Context context) {
100 mContext = context;
101 mTN = new TN();
102 mTN.mY = context.getResources().getDimensionPixelSize(
103 com.android.internal.R.dimen.toast_y_offset);
104 mTN.mGravity = context.getResources().getInteger(
105 com.android.internal.R.integer.config_toastDefaultGravity);
106 }
107
以及show方法
108 /**
109 * Show the view for the specified duration.
110 */
111 public void show() {
112 if (mNextView == null) {
113 throw new RuntimeException("setView must have been called");
114 }
115
116 INotificationManager service = getService();
117 String pkg = mContext.getOpPackageName();
118 TN tn = mTN;
119 tn.mNextView = mNextView;
120
121 try {
122 service.enqueueToast(pkg, tn, mDuration);
123 } catch (RemoteException e) {
124 // Empty
125 }
126 }
在Toast的构造方法里面发现一个叫TN的类,而且在show方法中,tn被设置到toast队列中,似乎是个很重要的类,来跟进一下看看TN到底是什么。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24358 TN() {
359 // XXX This should be changed to use a Dialog, with a Theme.Toast
360 // defined that sets up the layout params appropriately.
361 final WindowManager.LayoutParams params = mParams;
362 params.height = WindowManager.LayoutParams.WRAP_CONTENT;
363 params.width = WindowManager.LayoutParams.WRAP_CONTENT;
364 params.format = PixelFormat.TRANSLUCENT;
365 params.windowAnimations = com.android.internal.R.style.Animation_Toast;
366 params.type = WindowManager.LayoutParams.TYPE_TOAST;
367 params.setTitle("Toast");
368 params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
369 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
370 | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
371 }
372
373 /**
374 * schedule handleShow into the right thread
375 */
376
377 public void show() {
378 if (localLOGV) Log.v(TAG, "SHOW: " + this);
379 mHandler.post(mShow);
380 }
381
再看一下报错的那一行1
final Handler mHandler = new Handler();
查看这个TN类的构造方法和show方法,发现是用了window,这也说明了toast并不属于view,而是一个window,而且在show中发现了handler,源码中并没有使用looper.prepare方法,报错的原因应该就在这里了。
解决方法也很简单,使用handler通知主线程弹出toast,执意要在子线程弹出的话,使用looper.prepare和looper.loop就好。