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方法:
-
-
-
-
-
-
-
-
-
-
- public static Toast makeText(Context context, CharSequence text, int duration) {
- Toast result = new Toast(context);
-
- LayoutInflater inflate = (LayoutInflater)
- context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
- TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
- tv.setText(text);
-
- result.mNextView = v;
- result.mDuration = duration;
-
- return result;
- }
从这里面我们可以知道Toast显示的布局文件时transient_notification.xml,关于这个文件,我们可以在源码目录中搜索一下transient_notification.xml:
- <?xml version="1.0" encoding="utf-8"?>
- <!--
- /* //device/apps/common/res/layout/transient_notification.xml
- **
- ** Copyright 2006, The Android Open Source Project
- **
- ** Licensed under the Apache License, Version 2.0 (the "License");
- ** you may not use this file except in compliance with the License.
- ** You may obtain a copy of the License at
- **
- ** http://www.apache.org/licenses/LICENSE-2.0
- **
- ** Unless required by applicable law or agreed to in writing, software
- ** distributed under the License is distributed on an "AS IS" BASIS,
- ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ** See the License for the specific language governing permissions and
- ** limitations under the License.
- */
- -->
-
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical"
- android:background="?android:attr/toastFrameBackground">
-
- <TextView
- android:id="@android:id/message"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:layout_gravity="center_horizontal"
- android:textAppearance="@style/TextAppearance.Toast"
- android:textColor="@color/bright_foreground_dark"
- android:shadowColor="#BB000000"
- android:shadowRadius="2.75"
- />
-
- </LinearLayout>
看到了这个布局是如此的简单,里面显示的内容就是使用TextView来操作的,当然我们也可以修改这个布局的,他提供了一个setView方法,我们可以自定义样式来进行显示的:
- Toast toast = new Toast(this);
- View v = LayoutInflater.from(this).inflate(R.layout.activity_main, null);
- toast.setView(v);
- toast.show();
R.layout.activity_main是我们自己的布局文件同时我们也可以看到Toast.makeText方法也会返回一个Toast,在这个方法里我们看到他是使用系统的布局文件,然后在哪个TextView中进行显示内容,同时返回这个Toast,所以如果我们想得到这个系统的显示View可以使用这个方法得到一个Toast,然后再调用getView方法就可以得到了,同时我们也是可以在这个view上继续加一下我们相加的控件,但是这样做是没必要的,这里只是说一下。
下面接着来看一下显示的show方法吧:
-
-
-
- public void show() {
- if (mNextView == null) {
- throw new RuntimeException("setView must have been called");
- }
-
- INotificationManager service = getService();
- String pkg = mContext.getPackageName();
- TN tn = mTN;
- tn.mNextView = mNextView;
-
- try {
- service.enqueueToast(pkg, tn, mDuration);
- } catch (RemoteException e) {
-
- }
- }
这个方法很简单的,首先获取一个服务,然后将我们需要显示的toast放到这个服务的队列中进行显示,那么这里最主要的方法就是:
- service.enqueueToast(pkg, tn, mDuration);
首先看一下这个方法的参数是:pkg:包名,mDuration:显示的时间,tn:显示回调的包装类
这里我们可以看到其实最重要的参数是tn了,因为显示的逻辑可能就在这个类里面,找到源代码:
- private static class TN extends ITransientNotification.Stub {
- final Runnable mShow = new Runnable() {
- @Override
- public void run() {
- handleShow();
- }
- };
-
- final Runnable mHide = new Runnable() {
- @Override
- public void run() {
- handleHide();
-
- mNextView = null;
- }
- };
-
- private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
- final Handler mHandler = new Handler();
-
- int mGravity;
- int mX, mY;
- float mHorizontalMargin;
- float mVerticalMargin;
-
-
- View mView;
- View mNextView;
-
- WindowManager mWM;
-
- TN() {
-
-
- final WindowManager.LayoutParams params = mParams;
- params.height = WindowManager.LayoutParams.WRAP_CONTENT;
- params.width = WindowManager.LayoutParams.WRAP_CONTENT;
- params.format = PixelFormat.TRANSLUCENT;
- params.windowAnimations = com.android.internal.R.style.Animation_Toast;
- params.type = WindowManager.LayoutParams.TYPE_TOAST;
- params.setTitle("Toast");
- params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
- | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
- | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
- }
-
-
-
-
- @Override
- public void show() {
- if (localLOGV) Log.v(TAG, "SHOW: " + this);
- mHandler.post(mShow);
- }
-
-
-
-
- @Override
- public void hide() {
- if (localLOGV) Log.v(TAG, "HIDE: " + this);
- mHandler.post(mHide);
- }
-
- public void handleShow() {
- if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
- + " mNextView=" + mNextView);
- if (mView != mNextView) {
-
- handleHide();
- mView = mNextView;
- Context context = mView.getContext().getApplicationContext();
- if (context == null) {
- context = mView.getContext();
- }
- mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
-
-
- 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;
- 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();
- }
- }
-
- private void trySendAccessibilityEvent() {
- AccessibilityManager accessibilityManager =
- AccessibilityManager.getInstance(mView.getContext());
- if (!accessibilityManager.isEnabled()) {
- return;
- }
-
-
- AccessibilityEvent event = AccessibilityEvent.obtain(
- AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
- event.setClassName(getClass().getName());
- event.setPackageName(mView.getContext().getPackageName());
- mView.dispatchPopulateAccessibilityEvent(event);
- accessibilityManager.sendAccessibilityEvent(event);
- }
-
- public void handleHide() {
- if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
- if (mView != null) {
-
-
-
- if (mView.getParent() != null) {
- if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
- mWM.removeView(mView);
- }
-
- mView = null;
- }
- }
- }
这个类也不复杂,我们看到他继承了一个类,这个类的形式不知道大家还熟悉吗?我们在前面介绍远程服务AIDL的时候看到过这种形式的类,所以我们可以看到他使用Binder机制,我们可以在源代码中搜索一下:ITransientNotification
看到了,果然是个aidl文件,我们打开看一下:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- package android.app;
-
-
- oneway interface ITransientNotification {
- void show();
- void hide();
- }
好吧,我们看到就是两个方法,一个是show显示,一个是隐藏hide,那就看他的实现了,回到上面的代码中:-
-
-
- @Override
- public void show() {
- if (localLOGV) Log.v(TAG, "SHOW: " + this);
- mHandler.post(mShow);
- }
-
-
-
-
- @Override
- public void hide() {
- if (localLOGV) Log.v(TAG, "HIDE: " + this);
- mHandler.post(mHide);
- }
TN类中的实现这两个方法,内部使用Handler机制:post一个mShow和mHide:
- final Runnable mShow = new Runnable() {
- @Override
- public void run() {
- handleShow();
- }
- };
-
- final Runnable mHide = new Runnable() {
- @Override
- public void run() {
- handleHide();
-
- mNextView = null;
- }
- };
再看方法:handleShow- public void handleShow() {
- if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
- + " mNextView=" + mNextView);
- if (mView != mNextView) {
-
- handleHide();
- mView = mNextView;
- Context context = mView.getContext().getApplicationContext();
- if (context == null) {
- context = mView.getContext();
- }
- mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
-
-
- 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;
- 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();
- }
- }
看一下TN的构造方法:
这个方法主要是来调节toast的显示位置,同时我们可以看到这个显示使用的是WindowManager控件,将我们toast的显示的视图view放到WindowManger中的。
- TN() {
-
-
- final WindowManager.LayoutParams params = mParams;
- params.height = WindowManager.LayoutParams.WRAP_CONTENT;
- params.width = WindowManager.LayoutParams.WRAP_CONTENT;
- params.format = PixelFormat.TRANSLUCENT;
- params.windowAnimations = com.android.internal.R.style.Animation_Toast;
- params.type = WindowManager.LayoutParams.TYPE_TOAST;
- params.setTitle("Toast");
- params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
- | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
- | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
- }
之所以用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
擦,是包访问权限,不是public的,这时候就要借助强大的技术,反射了,我们只需要反射出这个变量,然后强暴她一次即可,得到这个变量我们可以得到这个TN类对象了,然后再使用反射获取他的show和hide方法即可,下面我们就来看一下实际的代码吧:- package com.weijia.toast;
-
- import java.lang.reflect.Field;
- import java.lang.reflect.Method;
-
- import android.content.Context;
- import android.view.View;
- import android.widget.Toast;
-
- public class ReflectToast {
-
- Context mContext;
-
- private Toast mToast;
- private Field field;
- private Object obj;
- private Method showMethod, hideMethod;
-
- public ReflectToast(Context c, View v) {
- this.mContext = c;
- mToast = new Toast(mContext);
- mToast.setView(v);
-
- reflectionTN();
- }
-
- public void show() {
- try {
- showMethod.invoke(obj, null);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
-
- public void cancel() {
- try {
- hideMethod.invoke(obj, null);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
-
- private void reflectionTN() {
- try {
- field = mToast.getClass().getDeclaredField("mTN");
- field.setAccessible(true);
- obj = field.get(mToast);
- showMethod = obj.getClass().getDeclaredMethod("show", null);
- hideMethod = obj.getClass().getDeclaredMethod("hide", null);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
这里我们实例化一个Toast对象,但是没有调用showf方法,就是不让toast入系统显示队列中,这样就可以控制show方法和hide方法的执行了,下面是测试代码:- package com.weijia.toast;
-
- import android.app.Activity;
- import android.os.Bundle;
- import android.view.View;
- import android.view.View.OnClickListener;
- import android.widget.TextView;
-
- public class MainActivity extends Activity {
- ReflectToast toast;
- boolean isShown = false;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- final TextView tView = new TextView(this);
- tView.setText("ReflectToast !!!");
- toast = new ReflectToast(this, tView);
-
- findViewById(R.id.show_toast).setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- if(isShown){
- toast.cancel();
- isShown = false;
- }else{
- toast.show();
- isShown = true;
- }
- }
- });
-
- }
- }
通过一个按钮可以控制toast的显示了,想显示多长时间就显示多长时间
0 0