1. 程式人生 > >Android中Toast顯示時間的自定義

Android中Toast顯示時間的自定義

Android中Toast的顯示時間為特定時間且不可更改,但是有時候我們開發設計需要讓Toast顯示更長時間,或者自己完全控制Toast的顯示和關閉。通過檢視Toast類的原始碼,可以看出,這有點難為它了,Toast類本身並沒有提供相應方法。
但是通過原始碼的檢視,還是可以看出點眉頭。原始碼分析思路在這裡轉eoe裡的一篇文章,思路較為清晰:
轉:
  Toast資訊提示框之所以在顯示一定時間後會自動關閉,是因為在系統中有一個Toast佇列。系統會依次從佇列中取(出佇列)一個Toast,並 顯示它。在顯示一段時間後,再關閉,然後再顯示下一個Toast資訊提示框。直到Toast佇列中所有Toast都顯示完為止。那麼有些時候需要這個Toas t資訊提示框長時間顯示,直到需要關閉它時通過程式碼來控制,而不是讓系統自動來關閉Toast資訊提示框。不過這個要求對於Toast本身來說有些過 分,因為Toast類並沒有提供這個功能。雖然如此,但方法總比問題多。通過一些特殊的處理還是可以實現這個功能的,而且並不複雜。


      Toast資訊提示框需要呼叫Toast.show方法來顯示。下面來看一下show方法的原始碼。

 

public void show() {
    if (mNextView == null) {
         throw new RuntimeException("setView must have been called");
    }
    INotificationManager service = getService();
    String pkg = mContext.getPackageName();
    TN tn = mTN;
    try {
         //  將當前Toast加入到Toast佇列
         service.enqueueToast(pkg, tn, mDuration);
    } catch (RemoteException e) {
         // Empty
    }
}



show方法的程式碼並不複雜,可以很容易找到如下的程式碼。
service.enqueueToast(pkg, tn, mDuration);
     從上面的程式碼可以很容易推斷出它的功能是將當前的Toast加入到系統的Toast佇列中。看到這裡,各位讀者應該想到。雖然show方法的表面功能是顯示Toast資訊提示框,但其實際的功能是將Toast加入到佇列中,再由系統根據Toast佇列來顯示Toast資訊提示框。那麼我們經過更進一步地思考,可以大膽地做出一個初步的方案。既然系統的Toast佇列可以顯示Toast資訊提示框,那麼我們為什麼不可以自己來顯示它呢?這樣不

是可以自己來控制Toast的資訊提示框的顯示和關閉了嗎!當然,這就不能再呼叫show方法來顯示Toast資訊提示框了(因為show方法會將Toast加入佇列,這樣我們就控制不了Toast了)。
     既然初步方案已擬定,現在就來實施它。先在Toast類找一下還有沒有其他的show方法。結果發現了一個TN類,該類是Toast的一個內嵌類。在TN類中有一個show方法。TN是ITransientNotification.Stub的子類。從ITransientNotification和TN類中的show方法初步推斷(因為Transient的中文意思是“短暫的”)系統是從Toast佇列中獲得了Toast物件後,利用TN物件的show方法顯示Toast,再利用TN.hide方法來關閉Toast。首先宣告,這只是假設,我們還不知道這麼做是否可行!當然,這也是科學研究的一般方法,先推斷或假設,然後再證明推斷或假設。
     現在關鍵的一步是獲得TN物件。遺憾的是TN被宣告成private型別,外部無法訪問。不過彆著急。在Toast類中有一個mTN變數。雖然不是public變數,但仍然可以通過反射技術訪問該變數。mTN變數會在建立Toast物件時初始化。因此,只要獲得mTN變數,就獲得了TN物件。下面的代碼顯示了一個永遠不會自動關閉的Toast資訊提示框。

 

//  先建立一個Toast物件
Toast toast = Toast.makeText(this, "永不消失的Toast", Toast.LENGTH_SHORT);
//  設定Toast資訊提示框顯示的位置(在螢幕頂部水平居中顯示)
toast.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL, 0, 0);
try
{
   //  從Toast物件中獲得mTN變數
 Field field = toast.getClass().getDeclaredField("mTN");
   field.setAccessible(true);
            Object obj = field.get(toast);
   //  TN物件中獲得了show方法
            Method method =  obj.getClass().getDeclaredMethod("show", null);
   //  呼叫show方法來顯示Toast資訊提示框
            method.invoke(obj, null);
}
catch (Exception e)
{
}



     上面的程式碼中try{…}catch(…){…}語句中的程式碼是關鍵。先利用事先建立好的Toast物件獲得了mTN變數。然後再利用反射技術獲得了TN物件的show方法。
關閉Toast和顯示Toast的方法類似,只是需要獲得hide方法,程式碼如下:

 

try
{
//  需要將前面程式碼中的obj變數變成類變數。這樣在多個地方就都可以訪問了
Method method =  obj.getClass().getDeclaredMethod("hide", null);
   method.invoke(obj, null);
}
catch (Exception e)
{
}



     上面的程式碼已經很完美地實現了通過程式碼控制Toast資訊提示框顯示和關閉的功能。但如果想實現得更完美,可以在Android SDK原始碼中找一個叫ITransientNotification.aidl的檔案(該檔案是AIDL服務定義檔案,將在後面詳細介紹),並在Android工程的src目錄中建一個android.app包,將這個檔案放到這個包中。然後ADT會自動在gen目錄中生成了一個android.app包,包中有一個ITransientNotification.java檔案。由於Android SDK自帶的ItransientNotification介面屬於內部資源,外部程式無法訪問,因此,只能將從Toast物件中獲得的mTN變數轉換成剛才生成的ITransientNotification物件了。這樣就不需要使反射技術獲得show和hide方法了。經過改良的顯示和關閉Toast資訊提示框的程式碼如下:
ITransientNotification  notification = (ITransientNotification) field.get(toast);
//  顯示Toast資訊提示框
notification.show();
//  關閉Toast資訊提示框
notification.hide();
最後整理程式碼如下:

 

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import android.app.Activity;
import android.os.Bundle;
import android.view.Gravity;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;


public class TestToastActivity extends Activity {
private Button showtoast,closetoast;
private Toast toast;
private Field field;
private Object obj;
private Method showMethod,hideMethod;

    @Override
    public void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        //初始化按鈕元件
        showtoast = (Button)this.findViewById(R.id.showtoast);
        closetoast = (Button)this.findViewById(R.id.closetoast);
        
        //設定元件監聽
        showtoast.setOnClickListener(new MyOnClickListener());
        closetoast.setOnClickListener(new MyOnClickListener());
        
        //建立Toast物件
        toast = Toast.makeText(this, "Toast自定義顯示時間測試", 1);
        toast.setGravity(Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL, 0, 0);
       
        //利用反射技術拿到mTN物件    
        reflectionTN();
    }
    
    
class MyOnClickListener implements View.OnClickListener{
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.showtoast:
try {
showMethod.invoke(obj, null);//呼叫TN物件的show()方法,顯示toast
} catch (Exception e) {
e.printStackTrace();
}
break;
case R.id.closetoast:
try {
hideMethod.invoke(obj, null);//呼叫TN物件的hide()方法,關閉toast
} catch (Exception e) {
e.printStackTrace();
}
break;
default:
break;
} 
}  
    }

private void reflectionTN() {
     try {
field = toast.getClass().getDeclaredField("mTN");
field.setAccessible(true);
obj = field.get(toast);
showMethod = obj.getClass().getDeclaredMethod("show", null);
hideMethod = obj.getClass().getDeclaredMethod("hide", null);
} catch (Exception e) {
e.printStackTrace();
}     
}
}