Toast信息提示框之所以在显示一定时间后会自动关闭,是因为在系统中有一个Toast队列。系统会依次从队列中取(出队列)一个Toast,并显示 它。在显示一段时间后,再关闭,然后再显示下一个Toast信息提示框。直到Toast队列中所有Toast都显示完为止。那么有些时候需要这个 Toast信息提示框长时间显示,直到需要关闭它时通过代码来控制,而不是让系统自动来关闭Toast信息提示框。不过这个要求对于Toast本身来说有些过分,因为Toast类并没有提供这个功能。虽然如此,但方法总比问题多。通过一些特殊的处理还是可以实现这个功能的,而且并不复杂。
Toast信息提示框需要调用Toast.show方法来显示。下面来看一下show方法的源代码。
他有两个静态的常量Toast.SHORT和Toast.LONG,这个在后面我会在源码中看到这个两个时间其实是2.5s和3s。
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
INotificationManager service = getService();
String pkg = mContext.getPackageName();
TN tn = mTN;
try {
// 将当前Toast加入到Toast队列
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
show方法的代码并不复杂,可以很容易找到如下的代码。
service.enqueueToast(pkg, tn, mDuration);
从上面的代码可以很容易推断出它的功能是将当前的Toast加入到系统的Toast队列中。看到这里,各位读者应该想到。虽然show方法的表面功能是 显示Toast信息提示框,但其实际的功能是将Toast加入到队列中,再由系统根据Toast队列来显示Toast信息提示框。那么我们经过更进一步地 思考,可以大胆地做出一个初步的方案。既然系统的Toast队列可以显示Toast信息提示框,那么我们为什么不可以自己来显示它呢?这样不是可以自己来 控制Toast的信息提示框的显示和关闭了吗!当然,这就不能再调用show方法来显示Toast信息提示框了(因为show方法会将Toast加入队 列,这样我们就控制不了Toast了)。
既然初步方案已拟定,现在就来实施它。先在Toast类找一下还有没有其他的show方法。结果发现了一个TN类,该类是Toast的一个内嵌类。 在TN类中有一个show方法。TN是ITransientNotification.Stub的子类。从ITransientNotification 和TN类中的show方法初步推断(因为Transient的中文意思是“短暂的”)系统是从Toast队列中获得了Toast对象后,利用TN对象的 show方法显示Toast,再利用TN.hide方法来关闭Toast。首先声明,这只是假设,我们还不知道这么做是否可行!当然,这也是科学研究的一 般方法,先推断或假设,然后再证明推断或假设。
现在关键的一步是获得TN对象。遗憾的是TN被声明成private类型,外部无法访问。不过别着急。在Toast类中有一个mTN变量。虽然不是 public变量,但仍然可以通过反射技术访问该变量。mTN变量会在创建Toast对象时初始化。因此,只要获得mTN变量,就获得了TN对象。下面的 代码显示了一个永远不会自动关闭的Toast信息提示框。
// 先创建一个Toast对象
Toast toast = Toast.makeText(this, "永不消失的Toast", Toast.LENGTH_SHORT);
// 设置Toast信息提示框显示的位置(在屏幕顶部水平居中显示)
toast.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL, 0, 0);
try
{
// 从Toast对象中获得mTN变量
Field field = toast.getClass().getDeclaredField("mTN");
field.setAccessible(true);
Object obj = field.get(toast);
// TN对象中获得了show方法
Method method = obj.getClass().getDeclaredMethod("show", null);
// 调用show方法来显示Toast信息提示框
method.invoke(obj, null);
}
catch (Exception e)
{
}
上面的代码中try{…}catch(…){…}语句中的代码是关键。先利用事先创建好的Toast对象获得了mTN变量。然后再利用反射技术获得了TN对象的show方法。
关闭Toast和显示Toast的方法类似,只是需要获得hide方法,代码如下:
try
{
// 需要将前面代码中的obj变量变成类变量。这样在多个地方就都可以访问了
Method method = obj.getClass().getDeclaredMethod("hide", null);
method.invoke(obj, null);
}
catch (Exception e)
{
}
上面的代码已经很完美地实现了通过代码控制Toast信息提示框显示和关闭的功能。但如果想实现得更完美,可以在Android SDK源代码中找一个叫ITransientNotification.aidl的文件(该文件是AIDL服务定义文件,将在后面详细介绍),并在 Android工程的src目录中建一个android.app包,将这个文件放到这个包中。然后ADT会自动在gen目录中生成了一个 android.app包,包中有一个ITransientNotification.java文件。由于Android SDK自带的ItransientNotification接口属于内部资源,外部程序无法访问,因此,只能将从Toast对象中获得的mTN变量转换成 刚才生成的ITransientNotification对象了。这样就不需要使反射技术获得show和hide方法了。经过改良的显示和关闭Toast 信息提示框的代码如下:
ITransientNotification notification = (ITransientNotification) field.get(toast);
// 显示Toast信息提示框
notification.show();
// 关闭Toast信息提示框
notification.hide();
Toast的源代码:
我们平常使用的makeText方法:
[java] view plain copy
1. /**
2. * Make a standard toast that just contains a text view.
3. *
4. * @param context The context to use. Usually your {@link android.app.Application}
5. * or {@link android.app.Activity} object.
6. * @param text The text to show. Can be formatted text.
7. * @param duration How long to display the message. Either {@link #LENGTH_SHORT} or
8. * {@link #LENGTH_LONG}
9. *
10. */
11. public static Toast makeText(Context context, CharSequence text, int duration) {
12. new Toast(context);
13.
14. LayoutInflater inflate = (LayoutInflater)
15. context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
16. null);
17. TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
18. tv.setText(text);
19.
20. result.mNextView = v;
21. result.mDuration = duration;
22.
23. return result;
24. }
从这里面我们可以知道Toast显示的布局文件时transient_notification.xml,关于这个文件,我们可以在源码目录中搜索一下transient_notification.xml:
[html] view plain copy
1. <?xml version="1.0" encoding="utf-8"?>
2. <!--
3. /* //device/apps/common/res/layout/transient_notification.xml
4. **
5. ** Copyright 2006, The Android Open Source Project
6. **
7. ** Licensed under the Apache License, Version 2.0 (the "License");
8. ** you may not use this file except in compliance with the License.
9. ** You may obtain a copy of the License at
10. **
11. ** http://www.apache.org/licenses/LICENSE-2.0
12. **
13. ** Unless required by applicable law or agreed to in writing, software
14. ** distributed under the License is distributed on an "AS IS" BASIS,
15. ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16. ** See the License for the specific language governing permissions and
17. ** limitations under the License.
18. */
19. -->
20.
21. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
22. android:layout_width="match_parent"
23. android:layout_height="match_parent"
24. android:orientation="vertical"
25. android:background="?android:attr/toastFrameBackground">
26.
27. <TextView
28. android:id="@android:id/message"
29. android:layout_width="wrap_content"
30. android:layout_height="wrap_content"
31. android:layout_weight="1"
32. android:layout_gravity="center_horizontal"
33. android:textAppearance="@style/TextAppearance.Toast"
34. android:textColor="@color/bright_foreground_dark"
35. android:shadowColor="#BB000000"
36. android:shadowRadius="2.75"
37. />
38.
39. </LinearLayout>
看到了这个布局是如此的简单,里面显示的内容就是使用TextView来操作的,当然我们也可以修改这个布局的,他提供了一个setView方法,我们可以自定义样式来进行显示的:
[java] view plain copy
1. Toast toast = new Toast(this);
2. View v = LayoutInflater.from(this).inflate(R.layout.activity_main, null);
3. toast.setView(v);
4. toast.show();
R.layout.activity_main是我们自己的布局文件
同时我们也可以看到Toast.makeText方法也会返回一个Toast,在这个方法里我们看到他是使用系统的布局文件,然后在哪个TextView中进行显示内容,同时返回这个Toast,所以如果我们想得到这个系统的显示View可以使用这个方法得到一个Toast,然后再调用getView方法就可以得到了,同时我们也是可以在这个view上继续加一下我们相加的控件,但是这样做是没必要的,这里只是说一下。
下面接着来看一下显示的show方法吧:
[java] view plain copy
1. /**
2. * Show the view for the specified duration.
3. */
4. public void show() {
5. if (mNextView == null) {
6. throw new RuntimeException("setView must have been called");
7. }
8.
9. INotificationManager service = getService();
10. String pkg = mContext.getPackageName();
11. TN tn = mTN;
12. tn.mNextView = mNextView;
13.
14. try {
15. service.enqueueToast(pkg, tn, mDuration);
16. catch (RemoteException e) {
17. // Empty
18. }
19. }
这个方法很简单的,首先获取一个服务,然后将我们需要显示的toast放到这个服务的队列中进行显示,那么这里最主要的方法就是:
[java] view plain copy
1. service.enqueueToast(pkg, tn, mDuration);
首先看一下这个方法的参数是:pkg:包名,mDuration:显示的时间,tn:显示回调的包装类
这里我们可以看到其实最重要的参数是tn了,因为显示的逻辑可能就在这个类里面,找到源代码:
[java] view plain copy
1. private static class TN extends ITransientNotification.Stub {
2. final Runnable mShow = new Runnable() {
3. @Override
4. public void run() {
5. handleShow();
6. }
7. };
8.
9. final Runnable mHide = new Runnable() {
10. @Override
11. public void run() {
12. handleHide();
13. // Don't do this in handleHide() because it is also invoked by handleShow()
14. null;
15. }
16. };
17.
18. private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
19. final Handler mHandler = new Handler();
20.
21. int mGravity;
22. int mX, mY;
23. float mHorizontalMargin;
24. float mVerticalMargin;
25.
26.
27. View mView;
28. View mNextView;
29.
30. WindowManager mWM;
31.
32. TN() {
33. // XXX This should be changed to use a Dialog, with a Theme.Toast
34. // defined that sets up the layout params appropriately.
35. final WindowManager.LayoutParams params = mParams;
36. params.height = WindowManager.LayoutParams.WRAP_CONTENT;
37. params.width = WindowManager.LayoutParams.WRAP_CONTENT;
38. params.format = PixelFormat.TRANSLUCENT;
39. params.windowAnimations = com.android.internal.R.style.Animation_Toast;
40. params.type = WindowManager.LayoutParams.TYPE_TOAST;
41. "Toast");
42. params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
43. | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
44. | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
45. }
46.
47. /**
48. * schedule handleShow into the right thread
49. */
50. @Override
51. public void show() {
52. if (localLOGV) Log.v(TAG, "SHOW: " + this);
53. mHandler.post(mShow);
54. }
55.
56. /**
57. * schedule handleHide into the right thread
58. */
59. @Override
60. public void hide() {
61. if (localLOGV) Log.v(TAG, "HIDE: " + this);
62. mHandler.post(mHide);
63. }
64.
65. public void handleShow() {
66. if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
67. " mNextView=" + mNextView);
68. if (mView != mNextView) {
69. // remove the old view if necessary
70. handleHide();
71. mView = mNextView;
72. Context context = mView.getContext().getApplicationContext();
73. if (context == null) {
74. context = mView.getContext();
75. }
76. mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
77. // We can resolve the Gravity here by using the Locale for getting
78. // the layout direction
79. final Configuration config = mView.getContext().getResources().getConfiguration();
80. final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
81. mParams.gravity = gravity;
82. if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
83. 1.0f;
84. }
85. if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
86. 1.0f;
87. }
88. mParams.x = mX;
89. mParams.y = mY;
90. mParams.verticalMargin = mVerticalMargin;
91. mParams.horizontalMargin = mHorizontalMargin;
92. if (mView.getParent() != null) {
93. if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
94. mWM.removeView(mView);
95. }
96. if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
97. mWM.addView(mView, mParams);
98. trySendAccessibilityEvent();
99. }
100. }
101.
102. private void trySendAccessibilityEvent() {
103. AccessibilityManager accessibilityManager =
104. AccessibilityManager.getInstance(mView.getContext());
105. if (!accessibilityManager.isEnabled()) {
106. return;
107. }
108. // treat toasts as notifications since they are used to
109. // announce a transient piece of information to the user
110. AccessibilityEvent event = AccessibilityEvent.obtain(
111. AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
112. event.setClassName(getClass().getName());
113. event.setPackageName(mView.getContext().getPackageName());
114. mView.dispatchPopulateAccessibilityEvent(event);
115. accessibilityManager.sendAccessibilityEvent(event);
116. }
117.
118. public void handleHide() {
119. if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
120. if (mView != null) {
121. // note: checking parent() just to make sure the view has
122. // been added... i have seen cases where we get here when
123. // the view isn't yet added, so let's try not to crash.
124. if (mView.getParent() != null) {
125. if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
126. mWM.removeView(mView);
127. }
128.
129. null;
130. }
131. }
132. }
这个类也不复杂,我们看到他继承了一个类,这个类的形式不知道大家还熟悉吗?我们在前面介绍远程服务AIDL的时候看到过这种形式的类,所以我们可以看到他使用Binder机制,我们可以在源代码中搜索一下:ITransientNotification
看到了,果然是个aidl文件,我们打开看一下:
[java] view plain copy
1. /* //device/java/android/android/app/ITransientNotification.aidl
2. **
3. ** Copyright 2007, The Android Open Source Project
4. **
5. ** Licensed under the Apache License, Version 2.0 (the "License");
6. ** you may not use this file except in compliance with the License.
7. ** You may obtain a copy of the License at
8. **
9. ** http://www.apache.org/licenses/LICENSE-2.0
10. **
11. ** Unless required by applicable law or agreed to in writing, software
12. ** distributed under the License is distributed on an "AS IS" BASIS,
13. ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14. ** See the License for the specific language governing permissions and
15. ** limitations under the License.
16. */
17.
18. package android.app;
19.
20. /** @hide */
21. oneway interface ITransientNotification {
22. void show();
23. void hide();
24. }
好吧,我们看到就是两个方法,一个是show显示,一个是隐藏hide,那就看他的实现了,回到上面的代码中:
[java] view plain copy
1. /**
2. * schedule handleShow into the right thread
3. */
4. @Override
5. public void show() {
6. if (localLOGV) Log.v(TAG, "SHOW: " + this);
7. mHandler.post(mShow);
8. }
9.
10. /**
11. * schedule handleHide into the right thread
12. */
13. @Override
14. public void hide() {
15. if (localLOGV) Log.v(TAG, "HIDE: " + this);
16. mHandler.post(mHide);
17. }
TN类中的实现这两个方法,内部使用Handler机制:post一个mShow和mHide:
[java] view plain copy
1. final Runnable mShow = new Runnable() {
2. @Override
3. public void run() {
4. handleShow();
5. }
6. };
7.
8. final Runnable mHide = new Runnable() {
9. @Override
10. public void run() {
11. handleHide();
12. // Don't do this in handleHide() because it is also invoked by handleShow()
13. null;
14. }
15. };
再看方法:handleShow
[java] view plain copy
1. public void handleShow() {
2. if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
3. " mNextView=" + mNextView);
4. if (mView != mNextView) {
5. // remove the old view if necessary
6. handleHide();
7. mView = mNextView;
8. Context context = mView.getContext().getApplicationContext();
9. if (context == null) {
10. context = mView.getContext();
11. }
12. mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
13. // We can resolve the Gravity here by using the Locale for getting
14. // the layout direction
15. final Configuration config = mView.getContext().getResources().getConfiguration();
16. final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
17. mParams.gravity = gravity;
18. if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
19. 1.0f;
20. }
21. if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
22. 1.0f;
23. }
24. mParams.x = mX;
25. mParams.y = mY;
26. mParams.verticalMargin = mVerticalMargin;
27. mParams.horizontalMargin = mHorizontalMargin;
28. if (mView.getParent() != null) {
29. if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
30. mWM.removeView(mView);
31. }
32. if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
33. mWM.addView(mView, mParams);
34. trySendAccessibilityEvent();
35. }
36. }
看一下TN的构造方法:
这个方法主要是来调节toast的显示位置,同时我们可以看到这个显示使用的是WindowManager控件,将我们toast的显示的视图view放到WindowManger中的。
[java] view plain copy
1. TN() {
2. // XXX This should be changed to use a Dialog, with a Theme.Toast
3. // defined that sets up the layout params appropriately.
4. final WindowManager.LayoutParams params = mParams;
5. params.height = WindowManager.LayoutParams.WRAP_CONTENT;
6. params.width = WindowManager.LayoutParams.WRAP_CONTENT;
7. params.format = PixelFormat.TRANSLUCENT;
8. params.windowAnimations = com.android.internal.R.style.Animation_Toast;
9. params.type = WindowManager.LayoutParams.TYPE_TOAST;
10. "Toast");
11. params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
12. | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
13. | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
14. }
之所以用WindowManger,我猜原因很简单,因为WindowManager是可以独立于Activity来显示的,我们知道toast在我们推出Activity的时候都还可以进行显示的。这个WindowManger用途也很广泛的,那个360桌面清理小工具就是使用这个控件显示的(后台开启一个service就可以了,不需要借助Activity)。同时toast也提供了setGravity或者setMargin方法进行设置toast的显示位置,其实这些设置就是在设置显示view在WindowManager中的位置
通过上面的知识我们或许稍微理清了思路,就是首先借助TN类,所有的显示逻辑在这个类中的show方法中,然后再实例一个TN类变量,将传递到一个队列中进行显示,所以我们要向解决这个显示的时间问题,那就从入队列这部给截断,因为一旦toast入队列了,我们就控制不了,因为这个队列是系统维护的,所以我们现在的解决思路是:
1、不让toast入队列
2、然后我们自己调用TN类中的show和hide方法
第一个简单,我们不调用toast方法就可以了,但是第二个有点问题了,因为我们看到TN这个类是私有的,所以我们也不能实例化他的对象,但是toast类中有一个实例化对象:tn
[java] view plain copy
1. final TN mTN;
是包访问权限,不是public的,这时候就要借助强大的技术,反射了,我们只需要反射出这个变量,一次即可,得到这个变量我们可以得到这个TN类对象了,然后再使用反射获取他的show和hide方法即可,下面我们就来看一下实际的代码吧:
[java] view plain copy
1. package com.weijia.toast;
2.
3. import java.lang.reflect.Field;
4. import java.lang.reflect.Method;
5.
6. import android.content.Context;
7. import android.view.View;
8. import android.widget.Toast;
9.
10. public class ReflectToast {
11.
12. Context mContext;
13.
14. private Toast mToast;
15. private Field field;
16. private Object obj;
17. private Method showMethod, hideMethod;
18.
19. public ReflectToast(Context c, View v) {
20. this.mContext = c;
21. new Toast(mContext);
22. mToast.setView(v);
23.
24. reflectionTN();
25. }
26.
27. public void show() {
28. try {
29. null);
30. catch (Exception e) {
31. e.printStackTrace();
32. }
33. }
34.
35. public void cancel() {
36. try {
37. null);
38. catch (Exception e) {
39. e.printStackTrace();
40. }
41. }
42.
43. private void reflectionTN() {
44. try {
45. "mTN");
46. true);//
47. obj = field.get(mToast);
48. "show", null);
49. "hide", null);
50. catch (Exception e) {
51. e.printStackTrace();
52. }
53. }
54. }
这里我们实例化一个Toast对象,但是没有调用showf方法,就是不让toast入系统显示队列中,这样就可以控制show方法和hide方法的执行了,下面是测试代码:
[java] view plain copy
1. package com.weijia.toast;
2.
3. import android.app.Activity;
4. import android.os.Bundle;
5. import android.view.View;
6. import android.view.View.OnClickListener;
7. import android.widget.TextView;
8.
9. public class MainActivity extends Activity {
10. ReflectToast toast;
11. boolean isShown = false;
12.
13. @Override
14. public void onCreate(Bundle savedInstanceState) {
15. super.onCreate(savedInstanceState);
16. setContentView(R.layout.activity_main);
17. final TextView tView = new TextView(this);
18. "ReflectToast !!!");
19. new ReflectToast(this, tView);
20.
21. new OnClickListener() {
22. @Override
23. public void onClick(View v) {
24. if(isShown){
25. toast.cancel();
26. false;
27. else{
28. toast.show();
29. true;
30. }
31. }
32. });
33.
34. }
35. }
通过一个按钮可以控制toast的显示了,想显示多长时间就显示多长时间