您的位置:首页 > 其它

Toast显示和关闭自个控制的方法

2016-03-24 10:23 483 查看
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







/**

* Make a standard toast that just contains a text view.

*

* @param context The context to use. Usually your {@link android.app.Application}

* or {@link android.app.Activity} object.

* @param text The text to show. Can be formatted text.

* @param duration How long to display the message. Either {@link #LENGTH_SHORT} or

* {@link #LENGTH_LONG}

*

*/

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:

[html] view
plain copy







<?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方法,我们可以自定义样式来进行显示的:

[java] view
plain copy







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方法吧:

[java] view
plain copy







/**

* Show the view for the specified duration.

*/

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) {

// Empty

}

}

这个方法很简单的,首先获取一个服务,然后将我们需要显示的toast放到这个服务的队列中进行显示,那么这里最主要的方法就是:

[java] view
plain copy







service.enqueueToast(pkg, tn, mDuration);

首先看一下这个方法的参数是:pkg:包名,mDuration:显示的时间,tn:显示回调的包装类

这里我们可以看到其实最重要的参数是tn了,因为显示的逻辑可能就在这个类里面,找到源代码:

[java] view
plain copy







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();

// Don't do this in handleHide() because it is also invoked by handleShow()

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() {

// XXX This should be changed to use a Dialog, with a Theme.Toast

// defined that sets up the layout params appropriately.

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;

}

/**

* schedule handleShow into the right thread

*/

@Override

public void show() {

if (localLOGV) Log.v(TAG, "SHOW: " + this);

mHandler.post(mShow);

}

/**

* schedule handleHide into the right thread

*/

@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) {

// remove the old view if necessary

handleHide();

mView = mNextView;

Context context = mView.getContext().getApplicationContext();

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;

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;

}

// treat toasts as notifications since they are used to

// announce a transient piece of information to the user

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) {

// 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) {

if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);

mWM.removeView(mView);

}

mView = null;

}

}

}

这个类也不复杂,我们看到他继承了一个类,这个类的形式不知道大家还熟悉吗?我们在前面介绍远程服务AIDL的时候看到过这种形式的类,所以我们可以看到他使用Binder机制,我们可以在源代码中搜索一下:ITransientNotification



看到了,果然是个aidl文件,我们打开看一下:

[java] view
plain copy







/* //device/java/android/android/app/ITransientNotification.aidl

**

** Copyright 2007, 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.

*/

package android.app;

/** @hide */

oneway interface ITransientNotification {

void show();

void hide();

}

好吧,我们看到就是两个方法,一个是show显示,一个是隐藏hide,那就看他的实现了,回到上面的代码中:

[java] view
plain copy







/**

* schedule handleShow into the right thread

*/

@Override

public void show() {

if (localLOGV) Log.v(TAG, "SHOW: " + this);

mHandler.post(mShow);

}

/**

* schedule handleHide into the right thread

*/

@Override

public void hide() {

if (localLOGV) Log.v(TAG, "HIDE: " + this);

mHandler.post(mHide);

}

TN类中的实现这两个方法,内部使用Handler机制:post一个mShow和mHide:

[java] view
plain copy







final Runnable mShow = new Runnable() {

@Override

public void run() {

handleShow();

}

};

final Runnable mHide = new Runnable() {

@Override

public void run() {

handleHide();

// Don't do this in handleHide() because it is also invoked by handleShow()

mNextView = null;

}

};

再看方法:handleShow

[java] view
plain copy







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();

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;

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中的。

[java] view
plain copy







TN() {

// XXX This should be changed to use a Dialog, with a Theme.Toast

// defined that sets up the layout params appropriately.

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

[java] view
plain copy







final TN mTN;

擦,是包访问权限,不是public的,这时候就要借助强大的技术,反射了,我们只需要反射出这个变量,然后强暴她一次即可,得到这个变量我们可以得到这个TN类对象了,然后再使用反射获取他的show和hide方法即可,下面我们就来看一下实际的代码吧:

[java] view
plain copy







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方法的执行了,下面是测试代码:

[java] view
plain copy







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的显示了,想显示多长时间就显示多长时间
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: