2020-02-03
關鍵字:ToastManager、應用消息通知
Android 應用往往少不了要與用戶交互的場景。
所謂與用戶交互,就是指用戶需要主動或者被動接受來自應用的消息、狀態提示的場景。
這種消息、狀態的展示形式往往多種多樣。但常見的也是比較合適的是彈窗式交互。
彈窗式交互是在應用內展示的。即在應用運行過程中以1、Toast 式彈窗通知;2、對話框式彈窗通知;兩種形式來與用戶交互。
其中,第 1 種交互筆者稱之為“弱交互式通知”,它彈出來以后過一段時間即會自行消隱。用戶只需要看,完全不用去處理,甚至可以連看都不看。
而第 2 種筆者則稱之為“強交互式通知”,它會彈出一個對話框,用戶只能手動點擊對話框上的相應按鈕才能關掉對話框。
這兩種交互彈窗的實現可就太容易了。第一個就是 Toast,而第二個則是 Dialog。堪稱是小學生都能做出來。
但今天這篇博文,不聊實現方式。來聊聊在一款應用中應如何對待各種各樣的彈窗式消息通知。
根據筆者的經驗,在整個應用中統一管理彈窗式通知是最合理的。如何統一管理呢?
即嚴禁私自創建 Toast 或 Dialog 來展示,這樣可能會導致同時彈出多個彈窗的情況從而引發通知混亂。
取而代之的是所有需要彈出的通知都交由同一個通知管理類來彈出。
有了這個統一的入口,我們就可以很方便地管控通知了。是即時彈出、是過濾、是排隊彈出或是其它各種需求,都可以在這個統一的通知管理類中很方便的實現。
筆者今天就在這里記錄一下自己撰寫的這么一個通知管理類 ToastManager。當然,筆者的這個類僅僅是根據自己的實際需求來實現的,並沒有做到絕對的完善與完美,在此記錄的主要目的是為了給自己備一下忘。
筆者的這個 ToastManager 目前有三種彈窗:
1、弱交互式彈窗;
2、強交互式彈窗;
3、強交互式選擇彈窗;
強交互式彈窗的變種版,對話框上具有“確定”與“否定”兩個按鈕,可以通過回調方法來通知創建者用戶的選擇結果。
筆者這個 ToastManager 在本質上就是簡單地對 Toast 與 AlertDialog 作一下封裝而已。甚至連排隊機制都還沒有實現,如果你有興趣,可以嘗試着自己去實現。
對了,還有一個很重要的。因為這個通知管理類理論上允許在任意位置調用。而 Toast 和 Dialog 是不允許在子線程中彈出的,但這種情況筆者僅僅是做了打印提示處理。正常來講應該是將所有的通知彈出請求都轉換成在主線程來彈的,但很遺憾,筆者沒有去實現,實在是因為懶~
話不多說,以下是 ToastManager 的源碼:

package com.jarwen.scanner.util; import android.app.AlertDialog; import android.content.Context; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import android.view.Gravity; import android.view.View; import android.widget.Button; import android.widget.GridLayout; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import com.jarwen.scanner.R; import com.jarwen.scanner.ScannerApplication; public class ToastManager { private static final boolean IS_WEAK_TOAST = true; private Context context; private Toast toast; private int txtColor; private int txtSize; private Drawable bgDrawable; private AlertDialog dialog; private OnMakeChoiceResult onMakeChoiceResult; private OnStrongToastListener onStrongToastListener; public ToastManager(Context context){ this.context = context; toast = new Toast(context); toast.setDuration(Toast.LENGTH_SHORT); toast.setGravity(Gravity.CENTER, 0, UnitManager.px2dp(80)); txtSize = 13; txtColor = context.getResources().getColor(R.color.gray_dark_1); bgDrawable = context.getResources().getDrawable(R.drawable.round_corner_gray_r5); } public void toast(String msg) { toast(IS_WEAK_TOAST, msg); } public void toast(boolean isWeakToast, String msg){ if(Thread.currentThread().getId() != 1){ Logger.e("Cannot toast on sub-thread."); return; } dismissDialog(); if(isWeakToast) { weakToast(msg); }else{ strongToast(msg); } } public void makeChoice(String content, OnMakeChoiceResult callback){ Logger.v("makeChoice()"); if(Thread.currentThread().getId() != 1){ Logger.e("Cannot toast on sub-thread."); return; } dismissDialog(); if(onMakeChoiceResult != null) { Logger.e("Cannot popup the make choice dialog cause current already shown a 'mc' dialog."); return; } int windowWidth = (int) (ScannerApplication.getInstance().getHardware().getAppWidth() * 0.618f); int windowHeight = UnitManager.px2dp(123); Logger.d("dimension:" + windowWidth + "*" + windowHeight); // 1. make layout. GridLayout layout = new GridLayout(context); layout.setColumnCount(2); layout.setRowCount(2); layout.setBackground(context.getResources().getDrawable(R.drawable.round_corner_makechoice_dialog_bg)); TextView tvContent = new TextView(context); tvContent.setText(content); tvContent.setTextSize(15); tvContent.setTextColor(context.getResources().getColor(R.color.gray_text_333)); tvContent.setGravity(Gravity.CENTER); tvContent.setBackground(context.getResources().getDrawable(R.drawable.round_corner_makechoice_dialog_content_bg)); GridLayout.LayoutParams glp = new GridLayout.LayoutParams(GridLayout.spec(0), GridLayout.spec(0, 2)); glp.width = -1; glp.height = (int) (windowHeight * 0.6f); tvContent.setLayoutParams(glp); TextView tvCancel = new TextView(context); tvCancel.setTextColor(context.getResources().getColor(R.color.gray_text_888)); tvCancel.setTextSize(15); tvCancel.setText(context.getText(R.string.no)); tvCancel.setBackground(context.getResources().getDrawable(R.drawable.round_corner_makechoice_dialog_cancel_bg)); tvCancel.setGravity(Gravity.CENTER); glp = new GridLayout.LayoutParams(GridLayout.spec(1), GridLayout.spec(0, 1.0f)); if(Build.VERSION.SDK_INT <= 22){ glp.width = (int) ((float) windowWidth / 2.0f); } glp.height = (int) (windowHeight * 0.4f); glp.topMargin = UnitManager.px2dp(1); tvCancel.setLayoutParams(glp); tvCancel.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Logger.d("cancel the make choice dialog"); if(onMakeChoiceResult != null) { onMakeChoiceResult.onMakeChoice(false); onMakeChoiceResult = null; } dismissDialog(); notifyStrongToastListener(false); } }); TextView tvOk = new TextView(context); tvOk.setTextColor(context.getResources().getColor(R.color.toast_makechoice_txt_ok)); tvOk.setTextSize(15); tvOk.setText(context.getText(R.string.yes)); tvOk.setGravity(Gravity.CENTER); tvOk.setBackground(context.getResources().getDrawable(R.drawable.round_corner_makechoice_dialog_ok_bg)); glp = new GridLayout.LayoutParams(GridLayout.spec(1), GridLayout.spec(1, 1.0f)); if(Build.VERSION.SDK_INT <= 22){ glp.width = (int) ((float) windowWidth / 2.0f) - UnitManager.px2dp(1); } glp.height = (int) (windowHeight * 0.4f); glp.topMargin = UnitManager.px2dp(1); glp.leftMargin = glp.topMargin; tvOk.setLayoutParams(glp); tvOk.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Logger.d("ok the make choice dialog"); if(onMakeChoiceResult != null) { onMakeChoiceResult.onMakeChoice(true); onMakeChoiceResult = null; } dismissDialog(); notifyStrongToastListener(false); } }); layout.addView(tvContent); layout.addView(tvCancel); layout.addView(tvOk); // 2. decorate dialog and show it. if(dialog != null) { dialog.dismiss(); } dialog = new AlertDialog.Builder(context).create(); dialog.setCancelable(false); dialog.setCanceledOnTouchOutside(false); dialog.show(); dialog.setContentView(layout); //Must behind on 'dialog.show()'. if(dialog.getWindow() != null) { dialog.getWindow().setLayout(windowWidth, windowHeight); dialog.getWindow().setBackgroundDrawable(new ColorDrawable(0)); } onMakeChoiceResult = callback; notifyStrongToastListener(true); } // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ private void weakToast(String msg){ if(Build.VERSION.SDK_INT > 25){ toast = null; Toast toast = new Toast(context); toast.setDuration(Toast.LENGTH_SHORT); toast.setGravity(Gravity.CENTER, 0, UnitManager.px2dp(80)); toast.setView(getTextView(msg)); toast.show(); }else{ if(toast.getView() != null){ ((TextView)toast.getView()).setText(msg); }else{ toast.setView(getTextView(msg)); } toast.show(); } } private void strongToast(String msg){ dismissDialog(); dialog = new AlertDialog.Builder(context).create(); dialog.setCanceledOnTouchOutside(false); dialog.setCancelable(false); dialog.show(); dialog.setContentView(getDialogView(msg)); if(dialog.getWindow() != null) { Logger.d("poping strong toast,screen:" + ScannerApplication.getInstance().getHardware().getAppWidth() + "*" + ScannerApplication.getInstance().getHardware().getAppHeight()); dialog.getWindow().setLayout((int) (ScannerApplication.getInstance().getHardware().getAppWidth() * 0.618f), -2); } notifyStrongToastListener(true); } private void dismissDialog(){ if(dialog != null) { dialog.dismiss(); dialog = null; } } private TextView getTextView(String txt){ TextView tv = new TextView(context); int padding = UnitManager.pix10(); tv.setPadding(padding, padding, padding, padding); tv.setBackground(bgDrawable); tv.setGravity(Gravity.CENTER); tv.setTextColor(txtColor); tv.setTextSize(txtSize); tv.setText(txt); return tv; } private View getDialogView(String txt){ final LinearLayout dialogLayout = new LinearLayout(context); dialogLayout.setGravity(Gravity.CENTER); dialogLayout.setBackground(context.getResources().getDrawable(R.drawable.round_corner_white_r5)); dialogLayout.setOrientation(LinearLayout.VERTICAL); dialogLayout.setLayoutParams(new LinearLayout.LayoutParams(-1, -1)); // 1. Information view. TextView tv = new TextView(context); tv.setPadding(UnitManager.pix10(), UnitManager.pix10(), UnitManager.pix10(), UnitManager.pix10()); LinearLayout.LayoutParams llp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); llp.topMargin = UnitManager.px2dp(20); llp.bottomMargin = UnitManager.px2dp(20); tv.setLayoutParams(llp); tv.setGravity(Gravity.CENTER); tv.setTextColor(context.getResources().getColor(R.color.gray_textview_original)); tv.setTextSize(12); tv.setText(txt); // 2. Divider line. View divider = new View(context); divider.setBackgroundColor(context.getResources().getColor(R.color.gray_background)); llp = new LinearLayout.LayoutParams(-1, UnitManager.px2dp(2)); divider.setLayoutParams(llp); // 3. Button. Button btn = new Button(context); btn.setText(R.string.ok); btn.setTextColor(context.getResources().getColor(R.color.basically_color)); btn.setTextSize(16); btn.setBackground(context.getResources().getDrawable(R.drawable.round_corner_white_r5)); btn.setLayoutParams(new LinearLayout.LayoutParams(-1, UnitManager.px2dp(40))); btn.setGravity(Gravity.CENTER); btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { dismissDialog(); notifyStrongToastListener(false); } }); dialogLayout.addView(tv); dialogLayout.addView(divider); dialogLayout.addView(btn); return dialogLayout; } public void showWaitingDialog(String info){ dismissDialog(); dialog = new AlertDialog.Builder(context).create(); dialog.setCancelable(false); dialog.setCanceledOnTouchOutside(false); dialog.show(); dialog.setContentView(getTextView(info)); if(dialog.getWindow() != null) { dialog.getWindow().setLayout((int) (ScannerApplication.getInstance().getHardware().getAppWidth() * 0.382f), -2); } } public void dismissWaitingDialog(){ dismissDialog(); } private void notifyStrongToastListener(boolean isShown){ if(onStrongToastListener != null) { onStrongToastListener.onStrongToastEvent(isShown); if(!isShown) { onStrongToastListener = null; //一次性通知。 } } } public void setOnStrongToastListener(OnStrongToastListener listener){ onStrongToastListener = listener; } public interface OnMakeChoiceResult{ void onMakeChoice(boolean yes); } public interface OnStrongToastListener { void onStrongToastEvent(boolean isShown); } }
它的使用方式也很簡單,因為 Android 應用開發中不建議把 Context 靜態保存(實際上對於 ToastManager 來說完全可以),而筆者不喜歡看到 Android Studio 的警告提示,就將 ToastManager 做成普通類的形式。同時,因為彈出 Dialog 需要 Activity 的 Context,因此,建議各位同學在 Activity 的初始化時創建 ToastManager 的實例。將實例以參數的形式傳遞給需要使用的地方即可。當然,其實最合理的方式是做成靜態類的方式,這就需要同學自行去琢磨實現了。