當使用者在使用Android App時,可以讓App跳出一些浮動式、會自動關閉的的訊息提示使用者應該要做哪些動作,或者是App已經完成了哪些動作,讓使用者清楚知道目前App的執行狀況。要如何在Android實作出氣泡訊息呢?很簡單,在Android SDK中就有內建「Toast」類別可以做到這樣的功能。



Toast的使用方法

簡易作法

最簡單顯示出氣泡訊息的作法,就是直接使用Toast類別下的makeText方法來產生出一個Toast物件,再用show方法將這個Toast物件顯示出來。如以下程式碼:

Toast.makeText(context, text, duration).show();

其中需要注意的是duration這個參數,duration顧名思義就是Toast訊息顯示的持續時間,只有兩個整數數值有作用,分別是短時間的「Toast.LENGTH_SHORT」和長時間的「Toast.LENGTH_LONG」。在預設的情況下,短時間大約是2秒、而長時間大約是3.5秒。

直接使用makeText產生出來的Toast,樣式和位置由系統來決定,如下圖是直接使用Toast的makeText方法和show方法顯示出來的氣泡訊息:

用這種方式來顯示出氣泡訊息的話,會有一個很大的缺點,那就是如果在氣泡訊息尚未消失前,又使用了一次makeText方法和show方法,新的氣泡訊息就會被安排在舊的氣泡訊息消失後才會顯示,而不是立刻將舊的氣泡訊息蓋過(墊在下面),也不是直接改變舊氣泡訊息的文字。當氣泡訊息一多,可是要等很久才可以把所有的訊息顯示完哦!

進階作法

為了避免發生上述使用簡易作法產生的問題,我們必須要管理makeText之後所得到的Toast物件。可以使用一個物件變數或是類別變數來儲存Toast物件的參考,以供重複使用。如以下程式:

...
private static Toast toast;
...
private static void makeTextAndShow(final Context context, final String text, final int duration) {
    if (toast == null) {
        //如果還沒有用過makeText方法,才使用
        toast = android.widget.Toast.makeText(context, text, duration);
    } else {
        toast.setText(text);
        toast.setDuration(duration);
    }
    toast.show();
}
...

有了上面的程式後,只要呼叫makeTextAndShow方法就可以顯示出不會排隊延遲的氣泡訊息了!

自訂Toast的樣式

直接使用makeText產生出來的Toast物件,是使用系統內建的樣式,因此可能會因App執行在不同的Android裝置上而有所改變。如果想要自訂Toast的樣式,就不該使用makeText方法,應該直接用new實體化一個新的Toast物件,接著再指定一個View給他,可以將上面的makeTextAndShow方法改寫如下:

...
private static Toast toast;
private static TextView toastText;
...
private static void makeTextAndShow(final Context context, final String text, final int duration) {
    if (toast == null) {
        //如果還沒有建立過Toast,才建立
        final ViewGroup toastView = new FrameLayout(context); // 用來裝toastText的容器
        final FrameLayout.LayoutParams flp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT);
        final GradientDrawable background = new GradientDrawable();
        toastText = new TextView(context);
        toastText.setLayoutParams(flp);
        toastText.setSingleLine(false);
        toastText.setTextSize(18);
        toastText.setTextColor(Color.argb(0xAA, 0xFF, 0xFF, 0xFF)); // 設定文字顏色為有點透明的白色
        background.setColor(Color.argb(0xAA, 0xFF, 0x00, 0x00)); // 設定氣泡訊息顏色為有點透明的紅色
        background.setCornerRadius(20); // 設定氣泡訊息的圓角程度

        toastView.setPadding(30, 30, 30, 30); // 設定文字和邊界的距離
        toastView.addView(toastText);
        toastView.setBackgroundDrawable(background);

        toast = new Toast(context);
        toast.setView(toastView);
    }
    toastText.setText(text);
    toast.setDuration(duration);
    toast.show();
}
...

執行結果:

自製Toast類別

由於Toast訊息算是Android App常用到的功能,因此可以將它另寫成一個類別來處理:

/**
 * Android 氣泡訊息
 *
 * @author magiclen
 *
 */
public class Toast {

    // -----類別常數-----
    /**
     * 短時間訊息
     */
    public static final int SHORT = android.widget.Toast.LENGTH_SHORT;

    /**
     * 長時間訊息
     */
    public static final int LONG = android.widget.Toast.LENGTH_LONG;

    /**
     * 類別位置
     *
     * @author magiclen
     *
     */
    public static enum Position {
        CENTER, CENTER_BOTTOM, CENTER_TOP, CENTER_LEFT, CENTER_RIGHT, LEFT_TOP, RIGHT_TOP, LEFT_BOTTOM, RIGHT_BOTTOM;
    }

    /**
     * 調整Toast與螢幕邊界的距離,數值愈大距離愈遠
     */
    private static final int offsetFactor = 12;

    // -----類別變數-----
    /**
     * 儲存Context的參考
     */
    private static Context context;

    /**
     * 儲存Toast的參考
     */
    private static android.widget.Toast toast;

    /**
     * 儲存Toast的位置
     */
    private static Position toastPosition = Position.CENTER_BOTTOM;

    /**
     * 儲存X邊界
     */
    private static int offsetX;

    /**
     * 儲存Y邊界
     */
    private static int offsetY;

    /**
     * 儲存使用者使用的ToastView
     */
    private static ToastView toastView;

    /**
     * 設定Context給Toast使用,建議在Activity onCreate的時候都指定一次,<br/>
     * 避免參考到的Context已經被釋放掉,或是變數被清空
     *
     * @param context
     */
    public static void setContext(final Context context) {
        Toast.context = context;
        useDefaultOffset();
    }

    /**
     * 指定ToastView,這個設定只有在重新呼叫showToast方法後才可以看到結果
     *
     * @param toastView 傳入ToastView物件
     */
    public static void setToastView(final ToastView toastView) {
        Toast.toastView = toastView;
        toast = null;
    }

    /**
     * 設定Toast的位置,這個設定只有在重新呼叫showToast方法後才可以看到結果
     *
     * @param position 傳入Toast的位置
     */
    public static void setPosition(final Position position) {
        if (position != null) {
            Toast.toastPosition = position;
        } else {
            Toast.toastPosition = Position.CENTER_BOTTOM;
        }
        useDefaultOffset();
    }

    /**
     * 設定Toast的位置,這個設定只有在重新呼叫showToast方法後才可以看到結果
     *
     * @param position 傳入Toast的位置
     * @param offsetX 傳入X位移量
     * @param offsetY 傳入Y位移量
     */
    public static void setPosition(final Position position, final int offsetX, final int offsetY) {
        if (position != null) {
            Toast.toastPosition = position;
        } else {
            Toast.toastPosition = Position.CENTER_BOTTOM;
        }
        setOffset(offsetX, offsetY);
    }

    /**
     * 使用預設的位移
     */
    private static void useDefaultOffset() {
        final DisplayMetrics dm = context.getResources().getDisplayMetrics();
        setOffset(dm.widthPixels / offsetFactor, dm.heightPixels / offsetFactor);
    }

    /**
     * 設定氣泡訊息的位移
     *
     * @param offsetX 傳入X位移量
     * @param offsetY 傳入Y位移量
     */
    private static void setOffset(final int offsetX, final int offsetY) {
        Toast.offsetX = offsetX;
        Toast.offsetY = offsetY;
    }

    /**
     * 顯示氣泡訊息(短時間)
     *
     * @param text 傳入訊息內容
     */
    public static void showToast(final String text) {
        showToast(text, android.widget.Toast.LENGTH_SHORT);
    }

    /**
     * 建立Toast物件
     *
     * @param text 傳入訊息內容
     * @param duration 傳入訊息維持的時間
     * @return 傳回Toast物件
     */
    private static android.widget.Toast makeText(final String text, final int duration) {
        if (toastView != null) {
            final android.widget.Toast toast = new android.widget.Toast(context);
            toastView.setText(text);
            toast.setDuration(duration);
            toast.setView(toastView);
            return toast;
        } else {
            return android.widget.Toast.makeText(context, text, android.widget.Toast.LENGTH_SHORT);
        }
    }

    /**
     * 顯示氣泡訊息
     *
     * @param text 傳入訊息內容
     * @param duration 傳入訊息維持的時間
     */
    public static void showToast(final String text, final int duration) {
        if (toast == null) {
            // 如果Toast物件不存在,就用makeText方法建立一個新的Toast
            toast = makeText(text, duration);
        } else {
            // 如果Toast物件存在,就重設它的持續時間和訊息文字
            toast.setDuration(duration);
            if (toastView == null) {
                toast.setText(text);
            } else {
                toastView.setText(text);
            }
        }

        // 設定氣泡訊息的位置
        int gravity = 0, offsetX = 0, offsetY = 0;
        switch (toastPosition) {
            case CENTER:
                gravity = Gravity.CENTER;
                break;
            case CENTER_BOTTOM:
                gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
                offsetY = Toast.offsetY;
                break;
            case CENTER_TOP:
                gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
                offsetY = Toast.offsetY;
                break;
            case CENTER_LEFT:
                gravity = Gravity.LEFT | Gravity.CENTER_VERTICAL;
                offsetX = Toast.offsetX;
                break;
            case CENTER_RIGHT:
                gravity = Gravity.RIGHT | Gravity.CENTER_VERTICAL;
                offsetX = Toast.offsetX;
                break;
            case LEFT_TOP:
                gravity = Gravity.LEFT | Gravity.TOP;
                offsetX = Toast.offsetX;
                offsetY = Toast.offsetY;
                break;
            case LEFT_BOTTOM:
                gravity = Gravity.LEFT | Gravity.BOTTOM;
                offsetX = Toast.offsetX;
                offsetY = Toast.offsetY;
                break;
            case RIGHT_TOP:
                gravity = Gravity.RIGHT | Gravity.TOP;
                offsetX = Toast.offsetX;
                offsetY = Toast.offsetY;
                break;
            case RIGHT_BOTTOM:
                gravity = Gravity.RIGHT | Gravity.BOTTOM;
                offsetX = Toast.offsetX;
                offsetY = Toast.offsetY;
                break;
        }
        toast.setGravity(gravity, offsetX, offsetY);
        // 顯示出氣泡訊息
        toast.show();
    }

    /**
     * 顯示氣泡訊息(短時間)
     *
     * @param textId 傳入String Resoure的ID,或是整數數字
     */
    public static void showToast(final int textId) {
        showToast(textId, android.widget.Toast.LENGTH_SHORT);
    }

    /**
     * 顯示氣泡訊息
     *
     * @param textId 傳入String Resoure的ID,或是整數數字
     * @param duration 傳入訊息維持的時間
     */
    public static void showToast(final int textId, final int duration) {
        String text;
        try {
            // 嘗試取得String Resource
            text = context.getResources().getString(textId);
        } catch (final Exception ex) {
            // 若String Resource取得失敗,則直接使用整數作為氣泡訊息
            text = String.valueOf(textId);
        }
        showToast(text, duration);
    }

    /**
     * 顯示氣泡訊息(短時間)
     *
     * @param decimalText 傳入整數或是浮點數數字作為訊息內容
     */
    public static void showToast(final double decimalText) {
        showToast(decimalText, android.widget.Toast.LENGTH_SHORT);
    }

    /**
     * 顯示氣泡訊息
     *
     * @param decimalText 傳入整數或是浮點數數字作為訊息內容
     * @param duration
     */
    public static void showToast(final double decimalText, final int duration) {
        if (decimalText == Math.round(decimalText)) {
            // 如果傳入的數是整數
            showToast(String.valueOf((int) decimalText), duration);
        } else {
            // 如果傳入的數不是整數
            showToast(String.valueOf(decimalText), duration);
        }
    }
}
/**
 * Toast專用的View
 *
 * @author magiclen
 *
 */
public class ToastView extends FrameLayout {

    private static int DEFAULT_TEXT_SIZE = 18;
    private static int DEFAULT_TEXT_COLOR = Color.argb(0xAA, 0xFF, 0xFF, 0xFF);
    private static int DEFAULT_PADDING = 30;
    private static int DEFAULT_BACKGROUND_COLOR = Color.argb(0xAA, 0xFF, 0x00, 0x00);
    private static int DEFAULT_RADIUS = 20;

    private TextView text;
    private GradientDrawable background = new GradientDrawable();

    public ToastView(final Context context) {
        super(context);
        init(context);
    }

    public ToastView(final Context context, final AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public ToastView(final Context context, final AttributeSet attrs, final int defStyle) {
        super(context, attrs, defStyle);
        init(context);
    }

    private final void init(final Context context) {
        text = new TextView(context);
        final FrameLayout.LayoutParams flp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT);
        text.setLayoutParams(flp);
        text.setSingleLine(false);
        text.setId(android.R.id.message);

        text.setTextSize(DEFAULT_TEXT_SIZE);
        text.setTextColor(DEFAULT_TEXT_COLOR);

        addView(text);

        setPadding(DEFAULT_PADDING, DEFAULT_PADDING, DEFAULT_PADDING, DEFAULT_PADDING);

        background.setColor(DEFAULT_BACKGROUND_COLOR);
        background.setCornerRadius(DEFAULT_RADIUS);
        setBackgroundDrawable(background);
    }

    public void setTextSize(final float size) {
        text.setTextSize(size);
    }

    public void setTextSize(final int unit, final float size) {
        text.setTextSize(unit, size);
    }

    public void setTextColor(final int color) {
        text.setTextColor(color);
    }

    public void setBackgroundColor(final int color) {
        background.setColor(color);
    }

    public void setRadius(final float radius) {
        background.setCornerRadius(radius);
    }

    public void setPadding(final int padding) {
        setPadding(padding, padding, padding, padding);
    }

    void setText(final String textString) {
        if (textString != null) {
            text.setText(textString);
        } else {
            text.setText("");
        }
    }
}

用法舉例

Toast.setContext(context);
Toast.setPosition(Toast.Position.RIGHT_BOTTOM);

ToastView tv = new ToastView(this);
tv.setRadius(50);
tv.setTextSize(50);
tv.setBackgroundColor(Color.argb(0xAA, 0x00, 0xAA, 0x00));

Toast.setToastView(tv);

Toast.showToast("氣泡訊息!");

執行結果: