有关 Android 应用开发中的弹窗式通知
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 的实例。将实例以参数的形式传递给需要使用的地方即可。当然,其实最合理的方式是做成静态类的方式,这就需要同学自行去琢磨实现了。