2025-05-17

前言
 
在開發Android應用時,加新功能是必不可少的,我們加入瞭新的功能,有的一看界面就可以看出來,但是有的新功能就比較隱蔽,也就是用戶很難知道你添加瞭這個新功能,這個時候就需要用戶在打開我們的應用時給出一些提示,說明我們在哪裡添加瞭新功能,點擊哪裡可以看到這個新功能。這時我們第一時間想到的可能是Toast,因為它用法簡單,又不影響用戶操作,但是它有個缺點,就是不能明確的指示是哪裡添加瞭新功能,除非你用文字描述出來。為此,我基於Toast編寫瞭一個小組件FloatTextToast(下面遇到的這個名字代替我寫的這個組件),他和Toast的用法一樣簡單,並且彌補瞭Toast的缺點,也更顯得更好看。

效果圖

 

 

 
你可以學到

1. Toast的基本用法
2. Android的消息機制,如何創建自己的消息隊列
3. 怎樣在Activity啟動時獲取一個View的width、height、top、left等屬性
基本思路

1. 首先你要有一個處理好的9 PNG的圖片,用於自適應文字顯示,關於9 PNG處理可以參考Android Doc
2. 要顯示在哪個View的下面,就要知道這個目標View的位置
3. 把要顯示的文本放在一個TextView裡,使用Toast的setView方法設置Toast要顯示的View。
4. 根據得到的位置,最後就是使用Toast的setGravity方法把要顯示的內容放到正確的位置顯示出來即可。
總的來說首先就是要知道目標View,根據targetView計算出要顯示提示的位置,然後根據位置使用Toast把提示的文本顯示出來。但是這裡有幾個難點,下面就一一解決
Activity加載完成時獲取targetVIew的寬高和位置屬性

我們加入瞭新的功能提示,自然會在用戶打開這個界面的時候就提示,但是在UI沒有渲染完成綁定倒Window上的時候,是不能獲取倒targetView的width、height和position的,那麼我們怎麼才能知道targetView的這些屬性呢?Activity的onAttachedToWindow回調方法是不能用的,況且它是在API 5加上的,以前的API中並沒有。不過我們還有一種方法,那就是在顯示提示的時候獲取targetView的屬性,如果獲取不到(為0)就一直獲取,直到獲取到為止,這其實是一個輪詢。為瞭達到這一目的,我們在開發者調用FloatTextToast.show()的時候使用Android的Message機制輪詢獲取一個targetView的屬性,如果獲取到,就會顯示提示文字瞭。在此之前先看下FloatTextToast構造函數,可以對它有個大概的瞭解,防止後面的代碼中出現的成員變量不認識。
[java]
1. private FloatTextToast(Context context,View targetView) { 
2.         this.mTargetView = targetView; 
3.         this.mContext= context; 
4.         mToast=new Toast(mContext); 
5.         mContentView=new TextView(mContext); 
6.         mContentView.setBackgroundResource(R.drawable.float_text_toast_bg); 
7.         mContentView.setTextColor(Color.BLACK); 
8.         mContentView.setTextSize(TypedValue.COMPLEX_UNIT_DIP,16); 
9.         mToast.setView(mContentView); 
10.          
11.         //初始化一個Handler線程  
12.         mHandlerThread=new HandlerThread("FloatTextToast"); 
13.         mHandlerThread.start(); 
14.         mHandler=new FloatTextToastHandler(mHandlerThread.getLooper()); 
15.     } 
 
自定義自己的消息循環機制
要想在一個自定義的組件中使用Message機制,一定要有自己的Looper機制,我們不能使用Activity的Looper,因為主Looper可能會有其他的Message需要處理,這就會導致我們的show方法會延遲調用,這樣效果就不好瞭,所以要有一個專門的Looper來處理此Message。要聲明自己的Looper,就需要HandlerThread這個類的配合瞭,這可是個好東西,使用它你會很容易的創建一個自己的線程用於處理你Message。使用方法很簡單,如下代碼:
[java] view plaincopyprint?
1. //初始化一個Handler線程  
2.         mHandlerThread=new HandlerThread("FloatTextToast"); 
3.         mHandlerThread.start(); 
4.         mHandler=new FloatTextToastHandler(mHandlerThread.getLooper()); 
 
這樣就聲明瞭一個HandlerThread並且讓它運行,運行之後我們就可以獲取一個屬於該Thread的Looper,然後把Message發送給這個Looper,那麼這個線程就可以處理你發送的消息瞭。。看看我們的自定義Handler
[java]
1. private class FloatTextToastHandler extends Handler{ 
2.  
3.         public FloatTextToastHandler(Looper looper) { 
4.             super(looper); 
5.         } 
6.  
7.         @Override 
8.         public void handleMessage(Message msg) { 
9.             switch(msg.what){ 
10.             case WHAT_SHOW: 
11.                 showInHandler(); 
12.             } 
13.         } 
14.          
15.          
16.     } 
 
它需要傳遞一個Looper作為構造參數聲明,意思就是使用這個Looper處理我發send的Message的意思。上面的代碼
[java]
1.mHandler=new FloatTextToastHandler(mHandlerThread.getLooper()); 
 
正是我們使用自己開啟的線程處理我們的Message的意思。下面看下我們的showInHandler()方法是怎麼處理的。
[java]
1. /**在Handler調用的show方法,主要為瞭等待{@link #mTargetView}的位置*/ 
2.     private void showInHandler(){ 
3.         int[] targetPos=getTargetViewPos(); 
4.         if(targetPos[0]==0&&targetPos[1]==0){ 
5.             mHandler.sendEmptyMessageDelayed(WHAT_SHOW, 100); 
6.         }else{ 
7.             final Rect contentPos=getContentViewPos(targetPos); 
8.             mToast.setGravity(Gravity.LEFT|Gravity.TOP, contentPos.left, contentPos.top); 
9.             mToast.show(); 
10.         } 
11.     } 
 
該方法其實就是在獲取targetVIew的位置,如果獲取不到,則向自定義的Looper裡發送一個Message重新調用該函數,如果得到瞭位置,那麼就調用Toast的setGravity方法設置好要顯示文本的位置,然後顯示即可。
獲取要顯示文本的位置
要獲取顯示的位置,就要知道targetVIew的位置以及它的寬、高,這樣就能計算要顯示文本的位置瞭。View組件都有一個函數,可以把自己在Window裡的坐標轉換為一個數組。
[java]
1. private int[] getTargetViewPos(){ 
2.         final int[] targetPos=new int[2]; 
3.         mTargetView.getLocationInWindow(targetPos); 
4.         return targetPos; 
5.     } 
 這樣,就返回瞭targetView的xy坐標瞭。光有targetView的坐標還不夠,還要有contentView最終要顯示的位置。
[java]
1. /**
2.      * 計算獲取浮動文本顯示的位置,把浮動文本放在targetView的中心處
3.      * @return 一個包含top和left的Rect
4.      */ 
5.     private  Rect getContentViewPos(int[] targetPos){ 
6.         final Rect windowVisibleRect=new Rect(); 
7.         final View targetView=mTargetView; 
8.         final TextView contentView=mContentView; 
9.         //狀態欄高度  
10.         targetView.getWindowVisibleDisplayFrame(windowVisibleRect); 
11.         int statusBarHeight=windowVisibleRect.top; 
12.          
13.         //背景圖那個三角箭頭的位置  
14.         final TextPaint textPaint=contentView.getPaint(); 
15.         int contentW=(int)textPaint.measureText((String)contentView.getText()); 
16.         int arrowPos=(int)(contentW*(30.0/160)); 
17.          
18.         final Rect rect = new Rect(); 
19.         rect.left = targetPos[0]+targetView.getWidth()/2-arrowPos; 
20.         rect.top = targetPos[1]-statusBarHeight + targetView.getHeight(); 
21.         return rect; 
22.     } 
 
這個函數的功能就是讓文本顯示在targetView的下方的橫向中間的位置,也就是文本的背景尖角三角要指向targetView橫向中間的位置,這樣才好看些。為瞭這個才需要使用Paint測量文本的寬度,所以這也是該組件的一個缺陷,不能顯示String格式之外的字符,比如SpannableString。

完整的組件代碼

上面是對組件代碼的拆分講解,是為瞭說明我們當時實現這個組件的想法以及步驟,下面就整體把代碼列出來,明瞭的看一下。
[java]
1. /**
2.  * 浮動的文本顯示。根據一個提供的View,可以把文本顯示到該View的下面.
3.  * 可以設置顯示的時間,多瞭該時間後自動消失。目前隻支持純文本{@link String}類型的顯示
4.  * 因為要計算顯示文本的寬度。
5.  * @author michael_li(飛雪無情)
6.  * @since 2011-12-10 下午04:57:36
7.  */ 
8. public class FloatTextToast { 
9.     public static final int LENGTH_LONG=Toast.LENGTH_LONG; 
10.     public static final int LENGTH_SHORT=Toast.LENGTH_SHORT; 
11.     private static final int WHAT_SHOW=1; 
12.      
13.     private Context mContext; 
14.     private View mTargetView; 
15.     private Toast mToast; 
16.     private  TextView mContentView; 
17.      
18.     private HandlerThread mHandlerThread; 
19.     private FloatTextToastHandler mHandler; 
20.     private FloatTextToast(Context context,View targetView) { 
21.         this.mTargetView = targetView; 
22.         this.mContext= context; 
23.         mToast=new Toast(mContext); 
24.         mContentView=new TextView(mContext); 
25.         mContentView.setBackgroundResource(R.drawable.float_text_toast_bg); 
26.         mContentView.setTextColor(Color.BLACK); 
27.         mContentView.setTextSize(TypedValue.COMPLEX_UNIT_DIP,16); 
28.         mToast.setView(mContentView); 
29.          
30.         //初始化一個Handler線程  
31.         mHandlerThread=new HandlerThread("FloatTextToast"); 
32.         mHandlerThread.start(); 
33.         mHandler=new FloatTextToastHandler(mHandlerThread.getLooper()); 
34.     } 
35.     /**
36.      * 生成一個FloatTextToast
37.      * @param context Activity 上下文
38.      * @param targetView  目標View,浮動文本要顯示在哪個View下面
39.      * @param text 要顯示的文本
40.      * @param duration 浮動文本顯示的時間 {@link #LENGTH_LONG} {@link #LENGTH_SHORT}
41.      * @return 一個FloatTextToast,可以調用{@link #show()}顯示
42.      */ 
43.     public static FloatTextToast makeText(Context context,View targetView, String text, int duration) { 
44.         final FloatTextToast floatToast=new FloatTextToast(context,targetView); 
45.         final TextView contentView=floatToast.mContentView; 
46.         contentView.setText(text); 
47.         floatToast.mToast.setDuration(duration); 
48.         return floatToast; 
49.     } 
50.     /**
51.      * 顯示浮動文本
52.      */ 
53.     public void show(){ 
54.         mHandler.sendEmptyMessage(WHAT_SHOW); 
55.     } 
56.     /**在Handler調用的show方法,主要為瞭等待{@link #mTargetView}的位置*/ 
57.     private void showInHandler(){ 
58.         int[] targetPos=getTargetViewPos(); 
59.         if(targetPos[0]==0&&targetPos[1]==0){ 
60.             mHandler.sendEmptyMessageDelayed(WHAT_SHOW, 100); 
61.         }else{ 
62.             final Rect contentPos=getContentViewPos(targetPos); 
63.             mToast.setGravity(Gravity.LEFT|Gravity.TOP, contentPos.left, contentPos.top); 
64.             mToast.show(); 
65.         } 
66.     } 
67.     private int[] getTargetViewPos(){ 
68.         final int[] targetPos=new int[2]; 
69.         mTargetView.getLocationInWindow(targetPos); 
70.         return targetPos; 
71.     } 
72.     /**
73.      * 計算獲取浮動文本顯示的位置,把浮動文本放在targetView的中心處
74.      * @return 一個包含top和left的Rect
75.      */ 
76.     private  Rect getContentViewPos(int[] targetPos){ 
77.         final Rect windowVisibleRect=new Rect(); 
78.         final View targetView=mTargetView; 
79.         final TextView contentView=mContentView; 
80.         //狀態欄高度  
81.         targetView.getWindowVisibleDisplayFrame(windowVisibleRect); 
82.         int statusBarHeight=windowVisibleRect.top; 
83.          
84.         //背景圖那個三角箭頭的位置  
85.         final TextPaint textPaint=contentView.getPaint(); 
86.         int contentW=(int)textPaint.measureText((String)contentView.getText()); 
87.         int arrowPos=(int)(contentW*(30.0/160)); 
88.          
89.         final Rect rect = new Rect(); 
90.         rect.left = targetPos[0]+targetView.getWidth()/2-arrowPos; 
91.         rect.top = targetPos[1]-statusBarHeight + targetView.getHeight(); 
92.         return rect; 
93.     } 
94.     private class FloatTextToastHandler extends Handler{ 
95.  
96.         public FloatTextToastHandler(Looper looper) { 
97.             super(looper); 
98.         } 
99.  
100.         @Override 
101.         public void handleMessage(Message msg) { 
102.             switch(msg.what){ 
103.             case WHAT_SHOW: 
104.                 showInHandler(); 
105.             } 
106.         } 
107.          
108.          
109.     } 
110. } 
 
此組件和Toast的實現方法一樣,所以上手不難,隻需使用makeText靜態方法生成一個即可
[java]
1.FloatTextToast.makeText(Context context, View targetView, String text, int duration).show() 
 就這麼簡單,傳進去幾個參數,show出即可,和Toast一樣好用。

小結

這裡主要是通過類之間的組合編寫一個一個FloatTextToast組件,便於在應用中提示一些信息,不光局限於新功能的提示,還有其他的點擊查看個人信息等等,就如上面的效果圖一樣。這裡主要的難點就在於Activity啟動獲取targetView的狀態,這裡采用瞭不受影響的自定義的消息機制,能及時的獲取targetView的狀態。這裡也采用的Toast的隊列機制,這樣就能夠更好的一個個的提示,讓用戶看完一個再顯示另外一個,不至於一下子全顯示出來,而用戶沒有時間看。這裡還采用瞭Paint用於測量文本的真實寬度,所以也有瞭一些缺陷,如果哪位有更好的方法,也可以留言告知我,不勝感激。
 
    

附上組件源代碼和效果圖的Demo下載http://up.aiwalls.com/2012/0317/20120317094437418.zip
 

摘自  采菊東籬下
 

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *