Android:剖析源碼,隨心所欲控制Toast顯示

前言

 

  Toast相信大傢都不會陌生吧,如果對於Toast不甚瞭解,可以參考我的上一篇博客《Android:談一談安卓應用中的Toast情節》,裡面有關於Toast基礎比較詳細的介紹。但是如果你想要看的是最原汁原味的Toast攻略,我非常建議你:出門右轉,谷歌官網,據說是一個非常給力的地兒,一般人我還不告訴他呢。但是!如果官網的開發者指南都滿足不瞭你的胃口的話,那你還是得準備點西瓜瓜子回來吧,搬個板凳坐前排來一起分析一下Toast的源碼設計。

 

Toast的源代碼世界

 

  這個故事要從哪裡說起呢?話說很久很久以前,程序員菜鳥小明不小心搜索到瞭Toast這個java文件,頓時小明心跳加速、臉紅耳赤的:“這可不是我經常用到的Toast嗎?”。懷揣著程序員固有的好奇心的小明點進瞭這個代碼文件,發現瞭這麼一個函數

 

復制代碼

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;

    }

復制代碼

好眼熟,貌似昨天還剛剛跟它在代碼上打過招呼呢。小明頓時有一種很高大上的感覺,這就是傳說中的android源代碼!

 

小明瞄瞭幾眼代碼,馬上總結出兩個信息:1、android源碼真簡單!2、Toast顯示的佈局文件是transient_notification.xml!

 

懷揣這洋洋得意的心思,小明在源代碼中開始搜索transient_notification.xml,一頓卡死,終於在快放棄的時候給出瞭結果。

 

復制代碼

<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"  

    android:layout_width="match_parent"  

    android:layout_height="match_parent"  

    android:orientation="vertical"  

    android:background="?android:attr/toastFrameBackground">  

  

    <TextView  

        android:id="@android:id/message"  

        android:layout_width="wrap_content"  

        android:layout_height="wrap_content"  

        android:layout_weight="1"  

        android:layout_gravity="center_horizontal"  

        android:textAppearance="@style/TextAppearance.Toast"  

        android:textColor="@color/bright_foreground_dark"  

        android:shadowColor="#BB000000"  

        android:shadowRadius="2.75"  

        />  

  

</LinearLayout>  

復制代碼

這簡單的不像話瞭!!小明憤怒瞭。但是憤怒歸憤怒,小明還是繼續往下看瞭,接下來看什麼呢,肯定是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;

  tn.mNextView = mNextView;

 

  try {

    service.enqueueToast(pkg, tn, mDuration);

  } catch (RemoteException e) {

    // Empty

  }

}

復制代碼

   這裡好像是要先獲取一個服務:INotificationManager,然後調用service.enqueueToast(pkg, tn, mDuration)好像是將Toast放到一個隊列裡面顯示吧;小明這麼底氣不足的理解著。這個TN是個啥子玩意呢?沒見過?那就來個第一次約會咯。代碼搜索出爐:

 

復制代碼

private static class TN extends ITransientNotification.Stub {  

        final Runnable mShow = new Runnable() {  

            @Override  

            public void run() {  

                handleShow();  

            }  

        };  

  

        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;  

            }  

        };  

  

        private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();  

        final Handler mHandler = new Handler();      

  

        int mGravity;  

        int mX, mY;  

        float mHorizontalMargin;  

        float mVerticalMargin;  

  

  

        View mView;  

        View mNextView;  

  

        WindowManager mWM;  

  

        TN() {  

            // XXX This should be changed to use a Dialog, with a Theme.Toast  

            // defined that sets up the layout params appropriately.  

            final WindowManager.LayoutParams params = mParams;  

            params.height = WindowManager.LayoutParams.WRAP_CONTENT;  

            params.width = WindowManager.LayoutParams.WRAP_CONTENT;  

            params.format = PixelFormat.TRANSLUCENT;  

            params.windowAnimations = com.android.internal.R.style.Animation_Toast;  

            params.type = WindowManager.LayoutParams.TYPE_TOAST;  

            params.setTitle("Toast");  

            params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON  

                    | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE  

                    | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;  

        }  

  

        /** 

         * schedule handleShow into the right thread 

         */  

        @Override  

        public void show() {  

            if (localLOGV) Log.v(TAG, "SHOW: " + this);  

            mHandler.post(mShow);  

        }  

  

        /** 

         * schedule handleHide into the right thread 

         */  

        @Override  

        public void hide() {  

            if (localLOGV) Log.v(TAG, "HIDE: " + this);  

            mHandler.post(mHide);  

        }  

  

        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();  

            }  

        }  

  

        private void trySendAccessibilityEvent() {  

            AccessibilityManager accessibilityManager =  

                    AccessibilityManager.getInstance(mView.getContext());  

            if (!accessibilityManager.isEnabled()) {  

                return;  

            }  

            // treat toasts as notifications since they are used to  

            // announce a transient piece of information to the user  

            AccessibilityEvent event = AccessibilityEvent.obtain(  

                    AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);  

            event.setClassName(getClass().getName());  

            event.setPackageName(mView.getContext().getPackageName());  

            mView.dispatchPopulateAccessibilityEvent(event);  

            accessibilityManager.sendAccessibilityEvent(event);  

        }          

  

        public void handleHide() {  

            if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);  

            if (mView != null) {  

                // note: checking parent() just to make sure the view has  

                // been added…  i have seen cases where we get here when  

                // the view isn't yet added, so let's try not to crash.  

                if (mView.getParent() != null) {  

                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);  

                    mWM.removeView(mView);  

                }  

  

                mView = null;  

            }  

        }  

    } 

復制代碼

乍一看,把小明給虛的,急忙找來大牛程序員幫忙講解一下。大牛認真過瞭幾眼,咦~其實也不是那麼復雜的。這時大牛註意到瞭這個TN繼承瞭ITransientNotification.Stub,這個類的形式不知道大傢還熟悉嗎?連小明好像在博客園裡面介紹AIDL的文章時懵懵懂懂看到過這種形式的類,可是沒等小明反應過來,大牛順手就在源代碼中搜索瞭一下:ITransientNotification

 

 

 

“果斷是AIDL!!”小明驚嘆。果然大神跟菜鳥就是不一樣,大牛這時打開ITransientNotification瞄一瞄,發現瞭show()和hide()這兩個方法。

 

復制代碼

package android.app;  

  

/** @hide */  

oneway interface ITransientNotification {  

    void show();  

    void hide();  

復制代碼

“那麼應該回去TN看看他的實現瞭”,大牛跟小明說。

 

復制代碼

@Override  

public void show() {  

    if (localLOGV) Log.v(TAG, "SHOW: " + this);  

    mHandler.post(mShow);  

}  

 

@Override  

public void hide() {  

    if (localLOGV) Log.v(TAG, "HIDE: " + this);  

    mHandler.post(mHide);  

}  

復制代碼

原來是使用handler機制,分別post一個nShow和一個mHide,再接再厲,追蹤源碼

 

復制代碼

final Runnable mShow = new Runnable() {  

  @Override  

  public void run() {  

    handleShow();  

  }  

};  

  

final Runnable mHide = new Runnable() {  

  @Override  

  public void run() {  

    handleHide();  

    mNextView = null;  

  }  

};

復制代碼

小明這次學聰明瞭,畢竟跟大牛學習比小明整天啃得那些《七天精通Android編程》之類的坑爹書靠譜多瞭,所以小明跟大牛說,我們應該看看handleShow()的實現,正解!

 

復制代碼

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();  

  }  

復制代碼

原來是Toast的視圖是通過WindowManager的addView來加載的,小明突然感覺自己向高級程序員邁進瞭一大步—–“怎麼說哥現在也是瞭解實現原理的人瞭!”

 

他們接下來又把邪惡的目光定位在TN()這個構造方法上面

 

復制代碼

TN() {  

  final WindowManager.LayoutParams params = mParams;  

  params.height = WindowManager.LayoutParams.WRAP_CONTENT;  

  params.width = WindowManager.LayoutParams.WRAP_CONTENT;  

  params.format = PixelFormat.TRANSLUCENT;  

  params.windowAnimations = com.android.internal.R.style.Animation_Toast;  

  params.type = WindowManager.LayoutParams.TYPE_TOAST;  

  params.setTitle("Toast");  

  params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON  

    | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE  

    | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;  

}  

復制代碼

這就是設置Toast中的View的各種位置參數params。

 

但是小明還是有點不明白,大牛看到小明神遊的樣子,就給他解釋道:

 

  其實Toast的原理是這樣的,先通過makeText()實例化出一個Toast,然後調用toast.Show()方法,這時並不會馬上顯示Toast,而是會實例化一個TN變量,然後通過service.enqueueToast()將其加到服務隊列裡面去等待顯示。在TN中進行調控Toast的顯示格式以及裡面的hide()、show()方法來控制Toast的出現以及消失,強調一下的是這個隊列是系統維護的,我們並不能幹涉。

 

小明若有所思的點點頭。。。。。。

 

 自由控制Toast的顯示時間

 

  時間就像水,幹著幹著就幹瞭,擼著擼著就沒瞭,吸著吸著就癟瞭。兩三天又過去瞭,突然有一天頭兒給小明吩咐瞭一個活:給應用設置一個較長時間的Toast。這還不簡單,小明偷偷在工位上打著瞌睡揉揉眼睛,Toast.setDuration()不就解決瞭嘛~要幾秒就設幾秒咯,這還是事兒?但是,谷歌又一次坑瞭他:因為小明不管怎麼設置,Toast隻能有顯示2s和3.5s這兩個情況,這時為啥呢?小明突然想起前些天翻瞭翻Toast的源碼,趕緊去裡面找答案

 

復制代碼

private void scheduleTimeoutLocked(ToastRecord r)  {  

  mHandler.removeCallbacksAndMessages(r);  

  Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);  

  long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;  

  mHandler.sendMessageDelayed(m, delay);  

}

復制代碼

private static final int LONG_DELAY = 3500; // 3.5 seconds  

private static final int SHORT_DELAY = 2000; // 2 seconds  

  我們呢看到這裡是使用瞭handler中的延遲發信息來顯示toast的,這裡我們也看到瞭,延遲時間是duration,但是隻有兩個值:2s和3.5s這兩個值,所以我們在之前說過我們設置toast的顯示時間是沒有任何效果的,所以小明又得去請教大牛瞭,果然活都不會是那麼簡單的。。。。。。。

 

大牛早有研究,他分析道:你還記得我們前些天分析的Toast源代碼嗎?Toast的顯示是首先借助TN類,所有的顯示邏輯在這個類中的show方法中,然後再實例一個TN類變量,將傳遞到一個隊列中進行顯示,所以我們要向解決這個顯示的時間問題,那就從入隊列這部給截斷,說白瞭就兩點:

 

1、不讓Toast進入隊列

 

2、調用TN類中的hide和show的方法自己控制Toast

 

但是第一點好實現,第二點讓人抓狂瞭,因為我們看到TN這個類是私有的,所以我們也不能實例化他的對象,但是toast類中有一個實例化對象:tn

 

final TN mTN;  

竟然是包訪問權限,大牛一臉淫笑的說,咱們得借助無比強大的反射技術,我們隻需要反射出這個變量,然後強暴她一次即可,得到這個變量我們可以得到這個TN類對象瞭,然後再使用反射獲取他的show和hide方法即可,代碼如下:

 

方法一:

 

復制代碼

public class ToastReflect {

    

    private Toast mToast;

    private Field field;

    private Object obj;

    private Method showMethod, hideMethod;

    private double time;

    

    private ToastReflect(Context context, String text, double time){

        this.time = time;

        mToast = Toast.makeText(context, text, Toast.LENGTH_LONG);

        reflectionTN();

    }

    

    private void reflectionTN() {

        try{

            field = mToast.getClass().getDeclaredField("mTN");

            field.setAccessible(true);

            obj = field.get(mToast);

            showMethod = obj.getClass().getDeclaredMethod("show", null);

            hideMethod = obj.getClass().getDeclaredMethod("hide", null);

        }catch(Exception e){

            e.printStackTrace();

        }

    }

 

    public static ToastReflect makeText(Context context, String text, double time){

        ToastReflect toastReflect = new ToastReflect(context, text, time);

        return toastReflect;

    }

    

    private void showToast(){

        try{

            showMethod.invoke(obj, null);

        }catch(Exception e){

            e.printStackTrace();

        }

    }

    

    private void hideToast(){

        try{

            hideMethod.invoke(obj, null);

        }catch(Exception e){

            e.printStackTrace();

        }

    }

    

    public void show(){

        showToast();

        new Timer().schedule(new TimerTask() {

            @Override

            public void run() {

                hideToast();

            }

        }, (long)(time * 1000));

    }

}

復制代碼

ps:利用反射來控制Toast的顯示時間在高版本會有bug,Android 2.2實測實可以用的,Android 4.0則無法使用。具體原因大牛還在分析。。。。。。

 

方法二:

 

  但是作為一個通用性軟件,對於任何版本都需要支持,所以小明還是隻能采取其他辦法,說實話,還真發現瞭一個比較傻瓜的實現。

 

就是可以利用handler.post結合timer來實現效果,兼容性較好。。利用定時重復show一個Toast就能達到根據特定時間來顯示的功能。

 

復制代碼

public class ToastSimple {

    

    private double time;

    private static Handler handler;

    private Timer showTimer;

    private Timer cancelTimer;

    

    private Toast toast;

    

    private ToastSimple(){

        showTimer = new Timer();

        cancelTimer = new Timer();

    }

    

    public void setTime(double time) {

        this.time = time;

    }

    

    public void setToast(Toast toast){

        this.toast = toast;

    }

    

    public static ToastSimple makeText(Context context, String text, double time){

        ToastSimple toast1= new ToastSimple();

        toast1.setTime(time);

        toast1.setToast(Toast.makeText(context, text, Toast.LENGTH_SHORT));

        handler = new Handler(context.getMainLooper());

        return toast1;

    }

    

    public void show(){

        toast.show();

        if(time > 2){

            showTimer.schedule(new TimerTask() {

                @Override

                public void run() {

                    handler.post(new ShowRunnable());

                }

            }, 0, 1900);

        }

        cancelTimer.schedule(new TimerTask() {

            @Override

            public void run() {

                handler.post(new CancelRunnable());

            }

        }, (long)(time * 1000));

    }

    

    private class CancelRunnable implements Runnable{

        @Override

        public void run() {

            showTimer.cancel();

            toast.cancel();

        }

    }

    

    private class ShowRunnable implements Runnable{

        @Override

        public void run() {

            toast.show();

        }

    }

}

復制代碼

方法三:  

 

這時,大牛也琢磨出一個辦法,因為Toast是基於windowManager來顯示的,所以完全可以自己寫一個自定義的Toast,代碼如下

 

復制代碼

package com.net168.toast;

 

import java.util.Timer;

import java.util.TimerTask;

 

import android.content.Context;

import android.graphics.PixelFormat;

import android.view.Gravity;

import android.view.View;

import android.view.WindowManager;

import android.widget.Toast;

 

public class ToastCustom {

    

    private WindowManager wdm;

    private double time;

    private View mView;

    private WindowManager.LayoutParams params;

    private Timer timer;

    

    private ToastCustom(Context context, String text, double time){

        wdm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);

        timer = new Timer();

        

        Toast toast = Toast.makeText(context, text, Toast.LENGTH_SHORT);

        mView = toast.getView();

        

        params = new WindowManager.LayoutParams();

        params.height = WindowManager.LayoutParams.WRAP_CONTENT;  

        params.width = WindowManager.LayoutParams.WRAP_CONTENT;  

        params.format = PixelFormat.TRANSLUCENT;  

        params.windowAnimations = toast.getView().getAnimation().INFINITE;  

        params.type = WindowManager.LayoutParams.TYPE_TOAST;  

        params.setTitle("Toast");  

        params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON  

                | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE  

                | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;

        params.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;

        params.y = -30;

        

        this.time = time;

    }

    

    public static ToastCustom makeText(Context context, String text, double time){

        ToastCustom toastCustom = new ToastCustom(context, text, time);

        return toastCustom;

    }

    

    public void show(){

        wdm.addView(mView, params);

        timer.schedule(new TimerTask() {

            @Override

            public void run() {

                wdm.removeView(mView);

            }

        }, (long)(time * 1000));

    }

    

    public void cancel(){

        wdm.removeView(mView);

        timer.cancel();

    }

    

    

}

復制代碼

PS:上面自定義Toast代碼隻實現瞭基本功能,其餘功能由於時間關系沒有全部實現。

 

測試代碼如下:

 

復制代碼

public class MainActivity extends ActionBarActivity implements View.OnClickListener{

    

    private EditText edt_duration;

    private Button btn_toast_simple;

    private Button btn_toast_reflect;

    private Button btn_toast_custom;

    

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        

        edt_duration = (EditText) findViewById(R.id.edt_duration);

        btn_toast_simple = (Button) findViewById(R.id.btn_toast_simple);

        btn_toast_reflect = (Button) findViewById(R.id.btn_toast_reflect);

        btn_toast_custom = (Button) findViewById(R.id.btn_toast_custom);

        

        btn_toast_simple.setOnClickListener(this);

        btn_toast_reflect.setOnClickListener(this);

        btn_toast_custom.setOnClickListener(this);

    }

 

    @Override

    public void onClick(View v) {

        double time = Double.parseDouble((edt_duration.getText().toString()));

        switch (v.getId()){

        case R.id.btn_toast_simple:

            ToastSimple.makeText(MainActivity.this, "簡單Toast,執行時間為:" + time, time).show();

            break;

        case R.id.btn_toast_reflect:

            ToastReflect.makeText(MainActivity.this, "反射Toast,執行時間為" + time, time).show();

            break;

        case R.id.btn_toast_custom:

            ToastCustom.makeText(MainActivity.this, "反射Toast,執行時間為" + time, time).show();

            break;

        }

    }

}

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。