先上一张图,再说话:
在android中,Toast是用来实现简要信息展示,与界面无关的一种无可点击操作的悬浮层,它和PopupWindow还有dialog不同,popupWindow和dialog是基于activity,detcorView来展示的,它们显示的时候是不可以操作底层view的,所以对于一些提示类的需求,同时还要操作activity的时候最好还是用Toast来完成。
现在举一个列子,一个需求:自定义一个toast,固定位置,而且消失时间需要自定义,该怎么实现呢,Toast有个方法setDuration().但是由于使用了注解,这个方法只支持Toast.LENGTH_LONG和Toast.LENGTH_SHORT 2个同样添加了注解的参数,所以我们无法自定义toast显示的具体时间,当然一般的toast肯定是在8s以内,超过了8s,就可以跟产品说做弹窗了,这个时候就用dialog和popupwindow了,上面是问题,下面给出解决方案以及方案中存在的问题:
基本原理是用到反射来调用show和hide方法,Toast原代码中有一个对象TN,toast的实际展示都是由它来完成的,TN使用了AIDI实现与底层native的交互,使用binder与WindowManager进行通信,最后由WindowManager将toast显示在窗口,下面就通过反射拿到TN对象进行操作.
package com.douyu.module.wheellottery.util;
import android.content.Context;
import android.os.Handler;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.LinearLayout.LayoutParams;
import android.widget.TextView;
import android.widget.Toast;
import com.douyu.module.wheellottery.R;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
/**
* 作者: chengwenchi
* 创建于: 2017/7/27
*/
public class WLToast {
/**
* 获取当前Android系统版本
*/
static int currentapiVersion = android.os.Build.VERSION.SDK_INT;
/**
* 离场动画持续时间
*/
private static final int TIME_END_ANIM = 5000;
/**
* UI线程句柄
*/
Handler mHandler;
/**
* 内容对象
*/
Context mContext;
/**
* 顶层布局
*/
LinearLayout mTopView;
/**
* 布局属性
*/
LayoutParams lp_MM;
/**
* 反射过程中是否出现异常的标志
*/
boolean hasReflectException = false;
/**
* 单例
*/
private static WLToast instance;
private TextView tvContext;
/**
* 获得单例
*
* @param context
* @return
*/
public static WLToast getInstance(Context context) {
if (instance == null) {
instance = new WLToast(context);
}
return instance;
}
private WLToast(Context context) {
if (context == null || context.getApplicationContext() == null) {
throw new NullPointerException("context can't be null");
}
mContext = context.getApplicationContext();
initView();
initTN();
}
/**
* 初始化视图控件
*/
public void initView() {
mHandler = new Handler(mContext.getMainLooper());
View view = LayoutInflater.from(mContext).inflate(R.layout.wl_toast, null);
tvContext = (TextView) view.findViewById(R.id.wl_toast_context);
lp_MM = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
mTopView = new LinearLayout(mContext);
mTopView.setLayoutParams(lp_MM);
mTopView.setOrientation(LinearLayout.VERTICAL);
mTopView.setGravity(Gravity.TOP);
mTopView.addView(view);
}
public final void show(String msg) {
tvContext.setText(msg);
showToast();
hide();
}
public final void hide() {
//动画结束后移除控件
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
hideToast();
}
}, TIME_END_ANIM);
}
/* 以下为反射相关内容 */
Toast mToast;
Field mTN;
Object mObj;
Method showMethod, hideMethod;
/**
* 通过反射获得mTN下的show和hide方法
*/
private void initTN() {
mToast = new Toast(mContext);
mToast.setView(mTopView);
Class<Toast> clazz = Toast.class;
try {
mTN = clazz.getDeclaredField("mTN");
mTN.setAccessible(true);
mObj = mTN.get(mToast);
showMethod = mObj.getClass().getDeclaredMethod("show", new Class<?>[0]);
hideMethod = mObj.getClass().getDeclaredMethod("hide", new Class<?>[0]);
hasReflectException = false;
} catch (NoSuchFieldException e) {
hasReflectException = true;
System.out.println(e.getMessage());
} catch (IllegalAccessException e) {
hasReflectException = true;
System.out.println(e.getMessage());
} catch (IllegalArgumentException e) {
hasReflectException = true;
System.out.println(e.getMessage());
} catch (NoSuchMethodException e) {
hasReflectException = true;
System.out.println(e.getMessage());
}
}
/**
* 通过反射获得的show方法显示指定View
*/
private void showToast() {
try {
//高版本需要再次手动设置mNextView属性,2系列版本不需要
if (currentapiVersion > 10) {
Field mNextView = mObj.getClass().getDeclaredField("mNextView");
mNextView.setAccessible(true);
mNextView.set(mObj, mTopView);
}
if (showMethod != null) {
showMethod.invoke(mObj, new Object[0]);
} else {
mToast.setDuration(Toast.LENGTH_LONG);
mToast.show();
}
hasReflectException = false;
} catch (Exception e) {
hasReflectException = true;
System.out.println(e.getMessage());
}
}
/**
* 通过反射获得的hide方法隐藏指定View
*/
public void hideToast() {
try {
if (mToast != null) {
mToast.cancel();
}
hasReflectException = false;
} catch (Exception e) {
hasReflectException = true;
System.out.println(e.getMessage());
}
}
public void removeAll() {
if (mHandler != null) {
mHandler.removeCallbacksAndMessages(null);
}
}
/**
* 是否是三星手机
*
* @return
*/
private boolean isSamSung() {
String brand = android.os.Build.BRAND;
if (!TextUtils.isEmpty(brand)) {
if (brand.equalsIgnoreCase("samsung")) {//适配三星手机且版本为7.0以上的系统
return true;
}
}
return false;
}
}
利用TN对象调用show方法是不会自动隐藏Toast的,这时,我们手动调用TN的hide方法,就可以进行延时操作了,然后有一个需要注意的点,利用LayoutInflater自定义的view需要一个容器,所以添加了一个container来显示自己加载的xml文件,并控制其位置。
PS:在红米和小米手机上利用反射机制,无法获取到show和hide 两个Method方法,这里暂时不知道具体原因,也许是OEM自定义改变了底层的一些架构对其影响,只是猜测。