1. 程式人生 > >android 反射實現自定義toast 自由設定消失時間

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自定義改變了底層的一些架構對其影響,只是猜測。