1. 程式人生 > >Toast拓展--自定義顯示時間和動畫

Toast拓展--自定義顯示時間和動畫

Toast拓展–自定義顯示時間和動畫

我們在Android應用開發中經常會需要在介面上彈出一個對介面操作無影響的小提示框來提示使用者一些資訊,這時候一般都會使用Android原生的Toast類

Toast.makeText(mContext, "訊息內容", Toast.LENGTH_SHORT).show();

一開始覺得,挺好用的,就有點什麼訊息都用Toast顯示了。
但是用久了就發現,Toast的顯示和消失動畫不符合自己的要求,顯示時間也只有SHORT和LONG兩種選擇,好像不太夠用。

於是,在閱讀了Toast的原始碼後對Toast進行了拓展,原生Toast包含了以下方法給使用者修改顯示內容:

setView(View):void
setDuration(int):void
setMargin(float,float):void
setGravity(int,int,int):void
setText(int):void
setText(CharSequence):void

分別是直接替換檢視、設定顯示時長、設定邊距屬性、設定顯示位置、設定顯示文字內容。

基於原有的Toast上對其進行拓展,修改及增加以下兩個方法:

setDuration(int):void
setAnimations(int):void

設定顯示時長方法拓展為可以自定義顯示時間,引數單位秒,提供三個預設值:LENGTH_SHORT

,LENGTH_LONG,LENGTH_ALWAYS,分別對應原生Toast的LENGTH_SHORT,LENGTH_LONG,以及總是顯示。要注意的是總是顯示需要在合適的時候自己呼叫hide()方法隱藏,否則會影響其他視窗的正常顯示。

下圖是使用自定義動畫和自定義顯示時間的Toast示例

演示效果

廢話不多說,先上工具類原始碼跟example:

ExToast.java

import android.content.Context;
import android.content.res.Resources;
import android.os.Handler;
import android.view.View;
import
android.view.WindowManager; import android.widget.Toast; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /** * Created by kj on 16-06-32. */ public class ExToast { private static final String TAG = "ExToast"; public static final int LENGTH_ALWAYS = 0; public static final int LENGTH_SHORT = 2; public static final int LENGTH_LONG = 4; private Toast toast; private Context mContext; private int mDuration = LENGTH_SHORT; private int animations = -1; private boolean isShow = false; private Object mTN; private Method show; private Method hide; private Handler handler = new Handler(); public ExToast(Context context){ this.mContext = context; if (toast == null) { toast = new Toast(mContext); } } private Runnable hideRunnable = new Runnable() { @Override public void run() { hide(); } }; /** * Show the view for the specified duration. */ public void show(){ if (isShow) return; initTN(); try { show.invoke(mTN); } catch (InvocationTargetException | IllegalAccessException e) { e.printStackTrace(); } isShow = true; //判斷duration,如果大於#LENGTH_ALWAYS 則設定消失時間 if (mDuration > LENGTH_ALWAYS) { handler.postDelayed(hideRunnable, mDuration * 1000); } } /** * Close the view if it's showing, or don't show it if it isn't showing yet. * You do not normally have to call this. Normally view will disappear on its own * after the appropriate duration. */ public void hide(){ if(!isShow) return; try { hide.invoke(mTN); } catch (InvocationTargetException | IllegalAccessException e) { e.printStackTrace(); } isShow = false; } public void setView(View view) { toast.setView(view); } public View getView() { return toast.getView(); } /** * Set how long to show the view for. * @see #LENGTH_SHORT * @see #LENGTH_LONG * @see #LENGTH_ALWAYS */ public void setDuration(int duration) { mDuration = duration; } public int getDuration() { return mDuration; } public void setMargin(float horizontalMargin, float verticalMargin) { toast.setMargin(horizontalMargin,verticalMargin); } public float getHorizontalMargin() { return toast.getHorizontalMargin(); } public float getVerticalMargin() { return toast.getVerticalMargin(); } public void setGravity(int gravity, int xOffset, int yOffset) { toast.setGravity(gravity,xOffset,yOffset); } public int getGravity() { return toast.getGravity(); } public int getXOffset() { return toast.getXOffset(); } public int getYOffset() { return toast.getYOffset(); } public static ExToast makeText(Context context, CharSequence text, int duration) { Toast toast = Toast.makeText(context,text,Toast.LENGTH_SHORT); ExToast exToast = new ExToast(context); exToast.toast = toast; exToast.mDuration = duration; return exToast; } public static ExToast makeText(Context context, int resId, int duration) throws Resources.NotFoundException { return makeText(context, context.getResources().getText(resId), duration); } public void setText(int resId) { setText(mContext.getText(resId)); } public void setText(CharSequence s) { toast.setText(s); } public int getAnimations() { return animations; } public void setAnimations(int animations) { this.animations = animations; } private void initTN() { try { Field tnField = toast.getClass().getDeclaredField("mTN"); tnField.setAccessible(true); mTN = tnField.get(toast); show = mTN.getClass().getMethod("show"); hide = mTN.getClass().getMethod("hide"); /**設定動畫*/ if (animations != -1) { Field tnParamsField = mTN.getClass().getDeclaredField("mParams"); tnParamsField.setAccessible(true); WindowManager.LayoutParams params = (WindowManager.LayoutParams) tnParamsField.get(mTN); params.windowAnimations = animations; } /**呼叫tn.show()之前一定要先設定mNextView*/ Field tnNextViewField = mTN.getClass().getDeclaredField("mNextView"); tnNextViewField.setAccessible(true); tnNextViewField.set(mTN, toast.getView()); } catch (Exception e) { e.printStackTrace(); } } }

ExToast example

ExToast exToast = ExToast.makeText(context,"message",ExToast.LENGTH_ALWAYS);
exToast.setAnimations(R.style.anim_view);
exToast.show();
//使用LENGTH_ALWAYS注意在合適的時候呼叫hide()
exToast.hide();
//顯示5秒的Toast
ExToast exToast = ExToast.makeText(context,"message",5);
exToast.show();

上面的程式碼可以實現自定義xml視窗動畫,以及長時間顯示Toast的功能。
下面看一下R.style.anim_view的內容,視窗動畫可以通過@android:windowEnterAnimation@android:windowExitAnimation定義視窗進場及退場效果

style.xml(放置在res/values/style.xml檔案)

<style name="anim_view">
    <item name="@android:windowEnterAnimation">@anim/anim_in</item>
    <item name="@android:windowExitAnimation">@anim/anim_out</item>
</style>

anim_in.xml(放置在res/anim目錄下)

<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:fromXDelta="0"
        android:fromYDelta="0"
        android:toXDelta="0"
        android:toYDelta="85"
        android:duration="1"
        />
    <translate
        android:fromXDelta="0"
        android:fromYDelta="0"
        android:toXDelta="0"
        android:toYDelta="-105"
        android:duration="350"
        android:fillAfter="true"
        android:interpolator="@android:anim/decelerate_interpolator"
        />
    <alpha
        android:fromAlpha="0"
        android:toAlpha="1"
        android:duration="100"
        />
    <translate
        android:fromXDelta="0"
        android:fromYDelta="0"
        android:toXDelta="0"
        android:toYDelta="20"
        android:duration="80"
        android:fillAfter="true"
        android:startOffset="350"
        />
</set>

anim_out.xml(放置在res/anim目錄下)

<set xmlns:android="http://schemas.android.com/apk/res/android">
    <alpha
        android:fromAlpha="1"
        android:toAlpha="0"
        android:duration="800"/>
</set>

以上動畫是模仿小米Toast彈出動畫的示例,具體動畫可以根據個人喜好自定義。

拓展Toast的工具類及使用方式已經介紹完畢,下面的內容是對於該工具類的設計原理解析,不趕時間並且有興趣的同學可以繼續往下看。

ExToast原理解析

剛才講到,Toast的使用,有很多限制,其中包括系統原生的Toast是呈佇列顯示出來的,必須要等到前一條Toast消失才會顯示下一條。

相信很多同學都遇到過這個問題,比如我做一個按鈕,點選的時候顯示一個toast,然後做了個小小的壓力測試:狂按儲存按鈕!於是toast佇列排了好長一條,一直在顯示,等到一兩分鐘才結束。

通過閱讀Toast原始碼,可以看到裡面的Toast.show()和Toast.cancel()方法:

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
    }
}

public void cancel() {
    mTN.hide();

    try {
        getService().cancelToast(mContext.getPackageName(), mTN);
    } catch (RemoteException e) {
        // Empty
    }
}

可以看到Toast的核心顯示和隱藏是封裝在INotificationManagerenqueueToast方法中,看到enqueue這個詞就知道這是一個佇列處理的函式,它的引數分別是packageName,tn物件,持續時間。結合Toast的顯示效果我們可以猜測這個方法內部實現是佇列顯示和隱藏每一個傳入的Toast。packageName和持續時間我們都很清楚是什麼,剩下的重點就在這個tn物件上了。那tn物件到底是什麼?

繼續閱讀Toast原始碼,可以知道Toast其實是系統虛浮窗的一種具體表現形式,它的核心在於它的一個私有靜態內部類class TN,它處理了Toast的顯示以及隱藏。所以,我們可以通過反射獲取這個TN物件,主動處理Toast的顯示和隱藏,而不經過系統Service

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;
        }
    };
    ...
    final Handler mHandler = new Handler();
    ...
    View mView;
    View mNextView;
    WindowManager mWM;
    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;
     }
    /**
     * 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 (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);
            ...
            if (mView.getParent() != null) {
                if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                mWM.removeView(mView);
            }
            ...
            mWM.addView(mView, mParams);
            ...
        }
    }
    private void trySendAccessibilityEvent() {...}
    public void handleHide() {
        ...
        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) {
                ...
                mWM.removeView(mView);
            }
            mView = null;
        }
    }
}

好吧,上面的程式碼太長不想看,那就把核心的程式碼挑出來

public void show(){
    ...
    WindowManager mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
    mWN.addView(mView, mParams);
}

public void hide(){
    ...
    WindowManager mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
    mWN.removeView(mView);
}

核心程式碼可以明顯看出,Toast的機制就是往WindowManager新增以及移除view,那隻要獲得TN物件,重新封裝一次show()和hide()方法就可以實現自定義顯示時間。

private void initTN() {
    try {
        Field tnField = toast.getClass().getDeclaredField("mTN");
        tnField.setAccessible(true);
        mTN = (ITransientNotification) tnField.get(toast);

        /**呼叫tn.show()之前一定要先設定mNextView*/
        Field tnNextViewField = mTN.getClass().getDeclaredField("mNextView");
        tnNextViewField.setAccessible(true);
        tnNextViewField.set(mTN, toast.getView());

    } catch (Exception e) {
        e.printStackTrace();
    }
}

public show(){
    initTN();
    mTN.show();
}

程式碼中mTN就是從Toast中利用反射獲取的物件,型別是ITransientNotification,這是從android原始碼中拿出來的aidl介面,匹配TN的型別。主動呼叫mTN.show()方法後就會神奇的發現,Toast長時間存在螢幕中,即使離開了app它依然存在,直到呼叫mTN.hide()後才消失。

Toast顯示時間拓展的問題已經解決了,剩下一個自定義動畫的問題。現在回過頭再看TN類的初始化方法程式碼,裡面初始化了一個WindowManager.LayoutParams物件,做過懸浮窗功能的同學應該都接觸過它,下面這一句程式碼就是定義視窗動畫的關鍵,如果能修改params.windowAnimations就能夠修改視窗動畫。

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

很不幸的是,params並不是一個公有的屬性,那就暴力點繼續用反射獲取並且修改視窗動畫

private void initTN() {
    try {
        Field tnField = toast.getClass().getDeclaredField("mTN");
        tnField.setAccessible(true);
        mTN = (ITransientNotification) tnField.get(toast);

        /**呼叫tn.show()之前一定要先設定mNextView*/
        Field tnNextViewField = mTN.getClass().getDeclaredField("mNextView");
        tnNextViewField.setAccessible(true);
        tnNextViewField.set(mTN, toast.getView());

        /**獲取params後重新定義視窗動畫*/
        Field tnParamsField = mTN.getClass().getDeclaredField("mParams");
        tnParamsField.setAccessible(true);
        WindowManager.LayoutParams params = (WindowManager.LayoutParams) tnParamsField.get(mTN);
        params.windowAnimations = R.style.anim_view;
    } catch (Exception e) {
        e.printStackTrace();
    }
}

至此,ExToast的工作原理已經基本解釋完畢。對於本篇反覆講到的利用Java反射獲取類裡面的私有屬性以及方法,是一個很實用的技能,本篇不詳細解釋Java反射知識,如果不熟悉的同學可以自行查詢Java反射相關資料瞭解。瞭解完後應該會對ExToast工具類的設計原理很清楚。

對於Toast的更多應用,請期待下一篇文章。轉載請註明出處,謝謝!

相關推薦

Toast拓展--定義顯示時間動畫

Toast拓展–自定義顯示時間和動畫 我們在Android應用開發中經常會需要在介面上彈出一個對介面操作無影響的小提示框來提示使用者一些資訊,這時候一般都會使用Android原生的Toast類 Toast.makeText(mContext, "訊息

ToastCustom【定義顯示風格的Toast

結構圖 iyu chan .cn 崩潰 頂部 組織 繼承 guid 版權聲明:本文為博主原創文章,未經博主允許不得轉載。 前言 基於系統Toast的自定義顯示風格的Toast。 效果圖 代碼分析 ToastCustom類基於系統Toast,不是繼承Toas

非常實用的定義佈局,定義顯示時長的頂部toast

最近在工作中需要彈出頂部toast且顯示時間不固定。從而寫了下面的一個模擬toast的動畫: 先看動畫: public void isShowToast(final boolean isShow,View mToastV) { final int marinTop = 0;//距離頂

【我的Android進階之旅】定義控制元件之使用ViewPager實現可以預覽的畫廊效果,並且定義畫面切換的動畫效果的切換時間

我們來看下效果 在這裡,我們實現的是,一個ViewPager來顯示圖片列表。這裡一個頁面,ViewPage展示了前後的預覽,我們讓預覽頁進行Y軸的壓縮,並設定透明度為0.5f,所有我們看到gif最後,左右兩邊的圖片有點朦朧感。讓預覽頁和主頁面有主從感。我們用分

Toast 定義顯示時長

當然你還可以這麼寫,利用 Timer 去控制顯示時長。 (以下內容來自網路): 設定Toast顯示時間 public void showMyToast(final Toast toast,

C# 中,ListView的定義顯示,可用於顯示不同的顏色字型等

VS2010下的ListView控制元件,想用它來顯示不同的顏色單元格,在網上找了listView1.Items[i].UseItemStyleForSubItems = false;的方法,但是不知道為何不起作用, 所以又搜尋了OwnDraw的方法,本來以為很複雜,但後來

android-Ultra-Pull-To-Refresh-下拉定義顯示動畫1

android-Ultra-Pull-To-Refresh的下拉動畫很炫酷,特別是storehouse style型別的 貼一個自定義的動畫效果-audi車標的logo 效果 具體修改程式碼如下: <string-array name="stor

用DatePickerTimePicker定義一個時間拾取器

原始碼下載:點選下載 很多情況下我們都需要這樣一種簡單的效果:點選一個Textview或者是EditText等可以彈出一個選擇時間的對話視窗,選好後再將時間設定為前者的文字,這就是所謂的時間拾取器,這種效果在設定鬧鈴時我們時常見到,接下來一起看看它的實現方法。 1、xml檔

selector定義顯示狀態

[java] view plaincopyprint? public void setBackgroundDrawable(Drawable d) {           boolean requestLayout = false;              mBackgroundResource = 0;

定義服務 factory service

控制器 ctr div ava json對象 html src com 沒有 1、使用factory 方法 必須有返回值 即return ,factory只是調用普通的function,可以返回任何東西。 HTML 代碼 <body ng-app="myApp

php curl如何設置定義請求頭打印請求頭信息

設置 發出 cnblogs lose 請求 clas false 自定義 col $header = [ ‘client:h5‘, ‘token:test‘, ]; curlRequest($url, $params, true, 10, $header

iOS開發-AFNetworking封裝Get(定義HTTP Header)Post請求及文件下載

謝謝 filepath pos cat style -type poe repo eth 前面提到AFNetworking是一個很強大的網絡三方庫,首先你需要引入AFNetworking三方庫;如封裝的有誤還請指出,謝謝! 1.Get請求 /**Get請求 url 服務器

ASP.NET MVC下定義錯誤頁展示錯誤頁的幾種方式

提供服務 one url attribute 運行 16px execute 釋放 namespace 在網站運行中,錯誤是不可避免的,錯誤頁的產生也是不可缺少的。 這幾天看了博友的很多文章,自己想總結下我從中學到的和實際中配置的。 首先,需要知道產生錯誤頁的來源,一種

定義事件.trigger().triggerHandler()

microsoft ati 普通 影響 submit 返回值 程序 sof efi 自定義事件.trigger()和.triggerHandler()1 .trigger()根據綁定到匹配元素的給定的事件類型執行所有的處理程序和行為,除了能夠觸發瀏覽器事件,

Django 【第十篇】定義驗證規則中間件

表達式 主動 mixin tex choice direct request lock test 一、Form基本使用 類 字段 is_valid() cleaned_data errors 字段參數: m

Confluence 6 升級定義的站點空間布局

功能 也會 隨著 for cti 修改 頁面 nal 需要 隨著 Confluence 的演變。默認的站點和空間布局也會隨著 Confluence 升級而讓使用的所有頁面進行改變。隨著一些新功能的加入和一些老功能的修改,默認的布局也需要進行修改來支持這些改變。 如

Confluence 6 升級定義的站點空間應用你的定義布局

Confluence當你升級你的 Confluence 到其他一個主要的 Confluence 發行版本的時候,你需要手動應用你修改過的任何全局或者空間級別的布局。除非有特殊的聲明,針對一些非主要的 Confluence 升級,你的自定義修改應該是不需要重新應用上去的。什麽是主要版本,什麽是非主要版本?主要版

Confluence 6 升級定義的站點空間獲得你的定義布局

Confluence我們建議你在對站點進行布局修改的時候,你需要為你修改的 Confluence 站點或空間布局保留所有的修改記錄。如果沒有的話,你應該可以通過下面的辦法找到你的自定義修改。這個方法將會把你對全部網站和空間級別的修改以一個單一的輸出而表現出來。從這個輸出文件,你應該能找到你修改過的自定義文件。

Confluence 6 升級定義的站點空間關閉緩存

class step cit 編輯 code onf 定義 重新 isp Velocity 被配置在內存中使用緩存模板。當你在 Confluence 中編輯了頁面的模板文件,Confluence 知道文件進行了編輯,將會重新從磁盤中載入模板文件。如果你直接在 Conflue

Confluence 6 升級定義的站點空間仔細測試你的修改

空間 radi 應該 所有 自定義 ace enc 得到 display 修改可能對 Confluence 的後續版本不兼容,當你對 Confluence 進行升級的時候,你應該總是對你自定義修改的模板文件進行仔細的測試來確定所有的修改對新版本的 Confluence 兼容