android 反射實現自定義toast 自由設定消失時間
先上一張圖,再說話:
在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自定義改變了底層的一些架構對其影響,只是猜測。