1. 程式人生 > >Toast分析——實現自己的Toast

Toast分析——實現自己的Toast

tla call handle imageview 不出 handler 操作 absolute resources

android 4.0以後,新增了一個功能:關閉某個應用發出的通知、Toast等。詳細操作為:打開應用安裝列表。找到要屏蔽的應用(長按通知,點擊彈出的"應用信息",就可以跳到應用信息界面),把同意推送消息(顯示通知)取消就可以。

產品發現這個功能之後,果斷要求屏蔽。能力有限,不知道怎樣破通知的屏蔽。自己實現一個Toast還是小case的~~

Toast的實現非常快想到兩種方案。Dialog和浮窗(WindowManager)。Dialog懷疑代價可能比較大,因此沒有去嘗試。直接來看浮窗,最後發現Toast也是用浮窗實現的。決定用浮窗,就比較簡單了,拿到WindowManager然後addView/removeView就可以。既然決定又一次實現,就弄的更好用一些——如何調用更方便——最簡單就直接提供個靜態方法吧。然後就實現的細節了。相信有部分朋友跟我一樣,對Toast的了解並非非常清楚,這裏主要指Toast的現實策略。到如今都沒有去細研究它是以一個什麽樣的策略顯示的。

以下是點點總結:

1、同樣的消息。會取消之前的,最後一個會顯示指定時間。不同的消息會串行顯示(or 每一個應用僅僅能顯示一定量的Toast。隊列中有了則僅僅更新顯示時間)。

2、顯示時間僅僅能使用指定的兩種,自己設置了無效。

3、支持自己定義顯示內容。

4、主界面不在前臺了。依舊能夠顯示。在其它線程若使用則須要自己實現消息隊列。

以上緊憑自己觀察推測。不一定對哈,錯誤地方歡迎指正。自己實現就不一定非得依照原始的樣子了,滿足產品要求,怎麽爽。如何美觀就如何寫~~


首先來看看Toast的使用:

Toast.makeText(getApplicationContext(), "hello world", Toast.LENGTH_LONG).show();
最簡單的一個Toast調用,這個應該也是我們最經常使用的。

接著,非常自然的就會有各種其它的想法。比方自己定義顯示內容,不想每次都傳入Context,現實在指定位置,控制顯示時間,等等各種奇葩需求。看看上面這行代碼,Toast.make(...)返回的是一個Toast對象。那麽我們來看看拿到這個對象之後能做些什麽:

mToast.setDuration(Toast.LENGTH_SHORT); // 顯示時間
mToast.setText("hello world"); // may res id
mToast.setGravity(Gravity.LEFT|Gravity.TOP, 50, 300);
mToast.setMargin(0.5f, 0.5f);
mToast.setView(tvMsg); // 指定顯示的view
mToast.cancel(); // 取消顯示
mToast.show(); // 顯示

非getXXX()方法就這些。

除了經常使用的show/cancel外我們來看看剩下的幾個方法是做什麽的。

setText():這個就不用說了。顯示的內容,支持字符串和字符串資源ID

setGravity():這個是顯示的對齊方式。後面兩個參數是針對前面對齊方式的x/y偏移量。

比方。上面代碼設置向屏幕的左上角對齊,並向右偏移50,向下偏移300

setMargin():margin,也就是外邊距。

比方我們通過Gravity設置顯示在左上角,然後設置這兩個值為0.5f,則Toast的左上角會現實在屏幕的中央

setView():設置顯示內容視圖,這個時候我們就能夠自己定義了。

好了。知道了這麽多,我們就寫個自己定義的Toast出來:

private void bolgToast() {
	LinearLayout llMain = new LinearLayout(getApplicationContext());
	llMain.setOrientation(LinearLayout.VERTICAL);
	llMain.setBackgroundColor(Color.BLACK);
	llMain.setGravity(Gravity.CENTER);
	ImageView ivIcon = new ImageView(getApplicationContext());
	ivIcon.setImageResource(R.drawable.ic_launcher);
	llMain.addView(ivIcon);
	TextView tvMsg = new TextView(getApplicationContext());
	tvMsg.setText("hello word");
	tvMsg.setTextSize(18);
	llMain.addView(tvMsg);
	Toast mToast = new Toast(getApplicationContext());
	mToast.setView(llMain);
	mToast.show();
}
最後效果例如以下:

技術分享

應該還行吧~~以下簡單說幾個問題:

1、有時候可能不知不覺在非主線程調用了Toast,這個時候系統可能會給你報這樣一個錯誤:

08-09 00:01:36.353: E/AndroidRuntime(24817): java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
這個錯誤非常明顯,可能須要我們自己去實現當前線程的消息系統。

網上的非常多樣例告訴我們能夠這樣寫:

new Thread(new Runnable() {
	
	@Override
	public void run() {
		Looper.prepare();
		Toast.makeText(getApplicationContext(), "hello ttdevs", Toast.LENGTH_LONG).show();
		Looper.loop(); // 不只須要prepare。還須要這句。不然不報錯但Toast也不出來
	}
}).start();

事實上本人一直非常好奇這樣寫的意義在哪裏。當然你也能夠這麽寫:

new Thread(new Runnable() {
	
	@Override
	public void run() {
		new Thread(new Runnable() {
		
			Handler mHandler = new Handler(Looper.getMainLooper()) {
				@Override
				public void dispatchMessage(Message msg) {
					Toast.makeText(getApplicationContext(), "hello ttdevs", Toast.LENGTH_LONG).show();
				}
			};
		
			@Override
			public void run() {
				mHandler.sendEmptyMessage(0);
			}
		}).start();
	}
}).start();
效果是一樣的(activity中寫的一段測試代碼,你能理解為什麽用了Thread的嵌套來模擬問題嗎?(?))。

2、有的時候你也可能會遇到這種問題:

08-09 00:17:28.053: E/AndroidRuntime(26441): Caused by: java.lang.RuntimeException: This Toast was not created with Toast.makeText()
我的測試代碼是這種:

Toast mToast = new Toast(getApplicationContext());
mToast.setText(String.valueOf(Math.random()));
mToast.show();
對於這個問題,我們僅僅能老老實實的用Toast.makeText()來構造一個Toast了。詳細原因我們之後會分析。


3、RuntimeException:setView must have been called , 這個問題常見於在調用show()之前調用過cancel()。

是否還記得這個cancel方法呢?查看源代碼我們看到:

        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;
            }
        };
在運行完隱藏之後會將mNextView = null。



以下我們來分析下Toast的源代碼(最新的4.4)。其它版本號比方2.2可能和這個版本號有較大不同,大家能夠自行分析。首先我們來看下最經常使用的makeText()方法:

    /**
     * Make a standard toast that just contains a text view.
     *
     * @param context  The context to use.  Usually your [email protected] android.app.Application}
     *                 or [email protected] android.app.Activity} object.
     * @param text     The text to show.  Can be formatted text.
     * @param duration How long to display the message.  Either [email protected] #LENGTH_SHORT} or
     *                 [email protected] #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;
    }
這種方法是返回一個標準的只帶有一個TextView的Toast。

先是通過Toast的默認構造方法創建一個Toast對象。

然後給這個Toast設置view和顯示時間。

當中R.layout.transient_notification是一個簡單線型布局裏面嵌套個TextView,大家能夠自行查看源代碼。然後就是設置顯示時間,對於long和short這裏面並非一個時間值,而是一個0/1這樣的靜態flag常量。這也就簡單說明為什麽我們傳入的duration為時間的話無效的原因了。接下來我們在看Toast的構造方法:

    /**
     * Construct an empty Toast object.  You must call [email protected] #setView} before you
     * can call [email protected] #show}.
     *
     * @param context  The context to use.  Usually your [email protected] android.app.Application}
     *                 or [email protected] android.app.Activity} object.
     */
    public Toast(Context context) {
        mContext = context;
        mTN = new TN();
        mTN.mY = context.getResources().getDimensionPixelSize(
                com.android.internal.R.dimen.toast_y_offset);
        mTN.mGravity = context.getResources().getInteger(
                com.android.internal.R.integer.config_toastDefaultGravity);
    }
關鍵部分還是創建一個TN對象,繼續跟進:

技術分享技術分享

上面是TN的結構和構造方法。這個構造方法還是比較簡單的。

改動WindowManager.LayoutParams的參數,假設你弄過浮窗。這裏就比較簡單了。

比較好奇的的LayoutParams.TYOE_TOAST,居然有這個type。然後立刻就想到,屏蔽Toast是不是就依據這個Type。只是通過測試,發現不是。 最讓我感覺五雷轟頂的還是這個凝視:This should be changed to use a Dialog, with a Theme.Toast defined that sets up the layout params appropriately.為什麽會有這個凝視呢?

寫了這個凝視又為什麽不用dialog來實現呢?做了以下這個嘗試:

技術分享

發現並沒有Theme.Toast,不知道是不是系統內部的,不給用戶使用。

只是。這也說明,用dialog實現可能是沒有問題的。好了。就到這吧,能力有限再繼續往下跟就困難了。

回到上面的Toast來看看我們經常使用的show()方法:

    /**
     * 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
        }
    }
關鍵部分跟不進去,臨時就不看了。

值得註意的一點是這個RuntimeException:setView must have been called。假設我們遇到這個錯誤提示就應該知道是顯示Toast的View為null。結合TN的代碼。可能非常自然的想到真正顯示的時候應該是TN.show(),通過handler終於運行的是以下的方法:

        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();
            }
        }
整個思路還是比較簡答的。準備條件,終於調用WindowManager的addView方法將我們的View顯示在界面上。handleHide()類似。將我們的View從界面上remove掉。值得註意的是hide的時候,我們的View會被置為null。
最後,我們來總結下Toast的。首先,我們須要通過new Toast或者Toast.makeText的方式構造一個Toast對象,貯備好要所需數據。當運行show的時候,會將這些數據傳遞給Android的Notification系統。然後由其負責處理相應的邏輯並終於通過WindowManager顯示在界面上。通過上面的分析我們也應該知道Toast和Notification用的同一個Notification系統。沒有去細致的分析各個版本號在實現的上的差異。假設有問題大家能夠去看相應的Toast源代碼來找詳細的解決的方法。以下提供兩個參考:

1、http://blog.csdn.net/ameyume/article/details/7714491

2、http://www.imooo.com/yidongkaifa/android/1009042.htm

最後我們來談談自己實現的Toast:通過一個show方法。將自己的Toast 增加一個消息隊列中,然後循環取出消息隊列中的內容顯示。

隱藏也是通過一個特殊的消息來完畢。當消息隊列達到一定大小,我們採取最簡單的邏輯即清空隊列來處理。

另外作為一個全局的東西。我們須要初始化和回收,初始化建議在application中完畢。

代碼例如以下(BaseThread請到我的置頂的那篇文章中查看):

public class ToastUtil extends BaseThread {
	private static final int SHOW_TIME = 2000; // 顯示時間
	private static final int QUEUE_SIZE = 120; // 隊列大小
	private static final int QUEUE_SIZE_LIMIT = 100; // 限制隊列大小
	private static final int FLAG_SHOW = 1000; // 顯示
	private static final int FLAG_HIDE = 1001; // 隱藏
	private static final int FLAG_CLEAR = 1002; // 清理消息隊列
	private static final String QUITMSG = "@bian_#feng_$market_%toast_&quit_*flag"; // 退出的標記

	private static BlockingQueue<String> mMsgQueue = new ArrayBlockingQueue<String>(QUEUE_SIZE); // 消息緩存隊列

	private static ToastUtil mToast;

	private WindowManager mWindowManager;
	private WindowManager.LayoutParams mParams;
	private View toastView;
	private TextView tvAlert;

	@SuppressLint("InflateParams")
	private ToastUtil(Context context) {
		mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);

		mParams = new WindowManager.LayoutParams();
		mParams.type = WindowManager.LayoutParams.TYPE_TOAST; //TYPE_SYSTEM_OVERLAY
		mParams.windowAnimations = android.R.style.Animation_Toast;
		mParams.format = PixelFormat.TRANSLUCENT;
		mParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
		mParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
		mParams.gravity = Gravity.CENTER_HORIZONTAL|Gravity.TOP;
		mParams.alpha = 1f;// 透明度,0全透 ,1不透
		mParams.verticalMargin = 0.75f;
		mParams.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
                | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;

		toastView = LayoutInflater.from(context).inflate(R.layout.layout_toast, null);
		tvAlert = (TextView) toastView.findViewById(R.id.tvAlert);

		start();
	}

	/**
	 * 初始化消息顯示
	 * 
	 * @param context
	 */
	public static void init(Context context) {
		if (null == mToast) {
			mToast = new ToastUtil(context);
		}
	}

	private Handler mHandler = new Handler(Looper.getMainLooper()) {

		public void handleMessage(android.os.Message msg) {
			int what = msg.what;
			switch (what) {
			case FLAG_SHOW:
				String str = msg.obj.toString();
				if (!TextUtils.isEmpty(str)) {
					showMsg(str);
				}
				break;
			case FLAG_HIDE:
				hideMsg();
				break;
			case FLAG_CLEAR:
				showMsg("操作異常。消息太多");
				break;

			default:
				break;
			}
		};
	};

	private void showMsg(String msg) {
		try {
			tvAlert.setText(msg);
			if (null == toastView.getParent()) {
				mWindowManager.addView(toastView, mParams);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	private void hideMsg() {
		try {
			if (null != toastView.getParent()) {
				mWindowManager.removeView(toastView);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * 顯示消息
	 * 
	 * @param msg
	 *            顯示的內容
	 */
	public static void show(String msg) {
		try {
			mMsgQueue.put(msg); // block
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	public static void show(Context context, int id) {
		try {
			mMsgQueue.put(context.getResources().getString(id)); // block
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * 退出
	 */
	public static void eixt() {
		try {
			mMsgQueue.put(QUITMSG);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	@Override
	public void execute() {
		try {
			String msgStr = mMsgQueue.take();
			
			if (QUITMSG.equals(msgStr)) {
				exitToast();
				return;
			}

			Message msg = mHandler.obtainMessage();
			if (null == msg) {
				msg = new Message();
			}
			msg.what = FLAG_SHOW;
			msg.obj = msgStr;
			mHandler.sendMessage(msg);

			Thread.sleep(SHOW_TIME);

			if (mMsgQueue.size() == 0) {
				mHandler.sendEmptyMessage(FLAG_HIDE);
			}

			if (mMsgQueue.size() > QUEUE_SIZE_LIMIT) {
				mMsgQueue.clear();

				mHandler.sendEmptyMessage(FLAG_CLEAR);
				Thread.sleep(SHOW_TIME);
				mHandler.sendEmptyMessage(FLAG_HIDE);
			}

			System.out.println(">>>>>" + mMsgQueue.size());
		} catch (Exception e) {
			e.printStackTrace();
			mHandler.sendEmptyMessage(FLAG_HIDE);
		}
	}

	/**
	 * 退出。清理內存
	 */
	private void exitToast() {
		try {
			hideMsg();

			quit();
			mMsgQueue.clear();
			mMsgQueue = null;
			mToast = null;
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}


Toast分析——實現自己的Toast