美文网首页
第二十四章 对话框

第二十四章 对话框

作者: 唔笛plk | 来源:发表于2019-07-20 11:52 被阅读0次

    一、系统对话框&自定义对话框

    1.系统对话框 AlertDialog

    • 普通对话框
    • 多按钮普通对话框
    • 列表对话框
    • 单选/多选对话框
    • 带输入对话框
    public class CustomLoginDialog extends AlertDialog {
        private Context context;
        private TextView tvLoginProgress;
        private TextView tvLoginTitle;
    
        public CustomLoginDialog(Context context) {
            super(context);
            this.context = context;
        }
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.custom_login);
            tvLoginProgress = this.findViewById(R.id.tvLoginProgress);
            tvLoginTitle = this.findViewById(R.id.tvLoginTitle);
            setCanceledOnTouchOutside(false);
            getWindow().setLayout(DensityUtils.dp2px(context, 270), DensityUtils.dp2px(context, 136));
            getWindow().setBackgroundDrawable(ContextCompat.getDrawable(context, R.drawable.shape_dialog_bg));
        }
    
        public void setProgress(String progress) {
            tvLoginProgress.setText("数据正在同步:" + progress + "%");
        }
    
        public void setLoginTitle(String loginTitle) {
            tvLoginTitle.setText(loginTitle);
        }
    }
    
    

    2.自定义对话框

    • DialogFragment
      Android官方推荐,自定义方便
    public abstract class BaseDialogFragment extends DialogFragment {
        protected String TAG = this.getClass().getSimpleName();
        private static final String SAVED_DIALOG_STATE_TAG = "android:savedDialogState";
        Unbinder unbinder;
    
        @Override
        public void onActivityCreated(@Nullable Bundle savedInstanceState) {
            if (getShowsDialog()) {
                setShowsDialog(false);
            }
            super.onActivityCreated(savedInstanceState);
            setShowsDialog(true);
    
            View view = getView();
            if (view != null) {
                if (view.getParent() != null) {
                    throw new IllegalStateException(
                            "DialogFragment can not be attached to a container view");
                }
                getDialog().setContentView(view);
            }
            final Activity activity = getActivity();
            if (activity != null) {
                getDialog().setOwnerActivity(activity);
                getDialog().getWindow().getDecorView().setFocusableInTouchMode(false);
            }
            if (savedInstanceState != null) {
                Bundle dialogState = savedInstanceState.getBundle(SAVED_DIALOG_STATE_TAG);
                if (dialogState != null) {
                    getDialog().onRestoreInstanceState(dialogState);
                }
            }
        }
    
        @Override
        public void onStart() {
            super.onStart();
            // 全屏显示Dialog,重新测绘宽高
            if (getDialog() != null) {
                DisplayMetrics dm = new DisplayMetrics();
                getActivity().getWindowManager().getDefaultDisplay().getMetrics(dm);
                getDialog().getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
    
            }
        }
    
        @Override
        public void onPause() {
            super.onPause();
        }
    
        @Override
        public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
            View rootView = inflater.inflate(getLayoutResId(), container, false);
            AutoUtils.auto(rootView);
            unbinder = ButterKnife.bind(this, rootView);
            initView();
            return rootView;
        }
    
        /**
         * 获取布局文件
         *
         * @return
         */
        protected abstract int getLayoutResId();
    
        /**
         * 初始化界面
         */
        protected void initView() {
        }
    
        @Override
        public void onDestroyView() {
            super.onDestroyView();
            unbinder.unbind();
        }
    
    
        /**
         * 全屏显示Dialog
         *
         * @param savedInstanceState
         * @return
         */
        @NonNull
        @Override
        public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
            Dialog dialog = new Dialog(getActivity());
            dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
            dialog.getWindow().setBackgroundDrawableResource(R.color.transparent);
            dialog.setCanceledOnTouchOutside(false);
            dialog.setCancelable(false);
            return dialog;
        }
        /**
         * 是否在弹窗外面
         *
         * @param context
         * @param event
         * @return
         */
        protected boolean isOutOfBounds(Context context, MotionEvent event) {
            final int x = (int) event.getX();
            final int y = (int) event.getY();
            final int slop = ViewConfiguration.get(context).getScaledWindowTouchSlop();
            final View decorView = getDialog().getWindow().getDecorView();
            return (x < -slop) || (y < -slop)
                    || (x > (decorView.getWidth() + slop))
                    || (y > (decorView.getHeight() + slop));
        }
    }
    
    
    • PopupWindow(弹框)
      简单易用,但bug较多,适配不方便
    public void showOther() {
            View view = LayoutInflater.from(context).inflate(R.layout.popwin_show_other, null);
            popupWindow = new PopupWindow(view, DensityUtil.dp2px(context, 132), ViewGroup.LayoutParams.WRAP_CONTENT);
            popupWindow.setFocusable(true);
            popupWindow.setOutsideTouchable(true);
            popupWindow.setBackgroundDrawable(new ColorDrawable(ContextCompat.getColor(context, R.color.transparent)));
            popupWindow.setAnimationStyle(R.anim.anim_pop);
            final TextView tvShowOtherScan = view.findViewById(R.id.tvShowOtherScan);
            final TextView tvShowOtherDevice = view.findViewById(R.id.tvShowOtherDevice);
            TextView tvShowOtherSet = view.findViewById(R.id.tvShowOtherSet);
            TextView tvShowOpenHistory = view.findViewById(R.id.tvShowOpenHistory);
    
            Drawable scanDrawable = ContextCompat.getDrawable(context, R.drawable.set_icon_scan);
            Drawable openDrawable = ContextCompat.getDrawable(context, R.drawable.set_icon_lock);
            Drawable setDrawable = ContextCompat.getDrawable(context, R.drawable.set_icon_set);
    
            scanDrawable.setBounds(0, 0, DensityUtil.dp2px(context, 15), DensityUtil.dp2px(context, 15));
            openDrawable.setBounds(0, 0, DensityUtil.dp2px(context, 15), DensityUtil.dp2px(context, 15));
            setDrawable.setBounds(0, 0, DensityUtil.dp2px(context, 15), DensityUtil.dp2px(context, 15));
    
            tvShowOtherDevice.setCompoundDrawables(scanDrawable, null, null, null);
            tvShowOtherScan.setCompoundDrawables(scanDrawable, null, null, null);
            tvShowOpenHistory.setCompoundDrawables(openDrawable, null, null, null);
            tvShowOtherSet.setCompoundDrawables(setDrawable, null, null, null);
    
            View.OnClickListener onClickListener = v -> {
                if (v.getId() == R.id.tvShowOtherScan) {
                    App.getInstance().setType("0");
                    new IntentIntegrator(getActivity())
                            .setOrientationLocked(false)
                            .setCaptureActivity(DeviceScanActivity.class)
                            .initiateScan();
    
                } else if (v.getId() == R.id.tvShowOtherSet) {
                    startActivity(new Intent(context, SetDeviceActivity.class));
    
                } else if (v.getId() == R.id.tvShowOpenHistory) {
                    Intent intent = new Intent(context, DeviceOpenHistoryActivity.class);
                    intent.putExtra(DeviceOpenHistoryActivity.class.getSimpleName(), true);
                    startActivity(intent);
                } else if (v.getId() == R.id.tvShowOtherDevice) {
                    App.getInstance().setType("1");
                    new IntentIntegrator(getActivity())
                            .setOrientationLocked(false)
                            .setCaptureActivity(DeviceScanActivity.class)
                            .initiateScan();
    
                }
                popupWindow.dismiss();
                popupWindow = null;
    
            };
            tvShowOtherScan.setOnClickListener(onClickListener);
            tvShowOtherDevice.setOnClickListener(onClickListener);
            tvShowOtherSet.setOnClickListener(onClickListener);
            tvShowOpenHistory.setOnClickListener(onClickListener);
            popupWindow.showAtLocation(rlSmartMore, Gravity.BOTTOM | Gravity.RIGHT, 0, rlSmartMore.getHeight() + SystemUtil.getVirtualBarHeight(context));
        }
    
    

    二、对话框实现机制

    AlertDialog时Dialog的子类,因此我们只分析Dialog,在Dialog的源码中我们可以看出,Dialog的父容器时Activity;

    1.对话框的创建

     Dialog dialog = new Dialog(MainActivity.this);
     dialog.setContentView(R.layout.dialog);
     dialog.show();
    //取消对话框
    dialog.cancel();
    

    2.Dialog的构造

    public class Dialog implements DialogInterface, Window.Callback,
            KeyEvent.Callback, OnCreateContextMenuListener, Window.OnWindowDismissedCallback {
        .................
        //使用默认主题的构造方法
        public Dialog(Context context) {
            this(context, 0, true);
        }
        //指定主题的构造方法
        Dialog(Context context, int theme, boolean createContextThemeWrapper) {
            if (createContextThemeWrapper) {
                if (theme == 0) {
                    TypedValue outValue = new TypedValue();
                    //使用默认的对话框主题
                    context.getTheme().resolveAttribute(com.android.internal.R.attr.dialogTheme,
                            outValue, true);
                    theme = outValue.resourceId;
                }
                //创建属于该对话框的Context
                mContext = new ContextThemeWrapper(context, theme);
            } else {
                mContext = context;
            }
            //获得Activity的窗口管理服务
            mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
            //创建对话框的窗口
            final Window w = new PhoneWindow(mContext);
            mWindow = w;
            //设置窗口回调监听
            w.setCallback(this);
            //设置窗口消失回调监听事件
            w.setOnWindowDismissedCallback(this);
            //给窗口设置窗口管理器
            w.setWindowManager(mWindowManager, null, null);
            //设置当前对话框窗口的位置
            w.setGravity(Gravity.CENTER);
            mListenersHandler = new ListenersHandler(this);
        }
    
    }
    

    分析:
    在Dilaog的构造方法中主要做了如下工作:

    • 根据参数createContextThemeWrapper的值来决定是使用参数theme指定的主题还是使用其父窗口Activity的主题。
      调用Context#getSystemService方法获得当前应用的窗口管理器WindowManager对象,一个应用不管有多少个Activity都只有一个WindowManager对象用于管理当前应用中的所有窗口。
    • 为Dialog对话框创建一个窗口Window对象,Window是个抽象类,其实现指向PhoneWindow类。
      给窗口设置事件回调监听,因为在Dialog类中实现了Window#Callback接口类,该接口类目的是让Dialog对话框的窗口具有处理响应按键触摸事件的能力,这也就是为什么用户默认创建的Dialog对话框可以响应“Back”回退按键事件和点击对话框窗口以外的地方Dialog对话框会自动消失隐藏。由此可知,Dialog和Activity都实现了消息处理。
    • 设置Window类的内部成员变量值WindowManager,由此知道Window的WindowManager和Dilaog的WindowManager指向同一个对象。
    • 设置当前Dialog窗口的对齐方式为居中,这就是为什么我们默认的对话框都是居中显示了吧。
      创建对话框的事件监听对象,用于对话框显示,消失,取消时的一些监听操作。
      Dialog内部创建了一个Window对象,窗口是一个抽象的东西,和Activity应用窗口一样,需要往窗口Window中添加视图View来显示内容。因此调用setContentView方法来加载对话框的布局视图。

    2.Dialog加载布局

    Dialog#setContentView源码如下:
    public void setContentView(int layoutResID) {
            mWindow.setContentView(layoutResID);
    }
    

    分析:
    该方法将操作转发给Window类中的setContentView方法,然而mWindow对象是指向PhoneWindow类的,也就是调用PhoneWindow类中的setContentView方法。到此处我们发现Dialog加载布局的流程和Activity加载布局的流程是一样的。因此这里就不仔细分析了,可以参考上一篇博客。到此,Dialog对话框窗口Window内部就已经添加了视图DecorView了。那么剩下的事就是Dilaog对话框怎么显示在手机屏幕上了。

    3.Dialog的显示

    在创建完Dialog对话框之后我们仅仅调用Dialog#show方法就可以让该对话框显示在当前Activity上。
    Dilaog#show源码如下:

    public void show() {
            //如果当前对话框正在显示时仅仅做一些简单可见度设置操作
            if (mShowing) {
                if (mDecor != null) {
                    if (mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
                        mWindow.invalidatePanelMenu(Window.FEATURE_ACTION_BAR);
                    }
                    mDecor.setVisibility(View.VISIBLE);
                }
                return;
            }
            //设置dialog是否已经取消标志
            mCanceled = false;
    
            if (!mCreated) {
                dispatchOnCreate(null);
            }
            //是个空方法,可以在创建Dialog时重写该方法
            onStart();
            //得到Dialog对话框窗口的顶层视图DecorView
            mDecor = mWindow.getDecorView();
            //设置窗口actionbar
            if (mActionBar == null && mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
                final ApplicationInfo info = mContext.getApplicationInfo();
                mWindow.setDefaultIcon(info.icon);
                mWindow.setDefaultLogo(info.logo);
                mActionBar = new WindowDecorActionBar(this);
            }
    
            WindowManager.LayoutParams l = mWindow.getAttributes();
            //设置当前窗口输入法模式
            if ((l.softInputMode
                    & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) == 0) {
                WindowManager.LayoutParams nl = new WindowManager.LayoutParams();
                nl.copyFrom(l);
                nl.softInputMode |=
                        WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
                l = nl;
            }
    
            try {
                //重点 添加对话框窗口的顶层视图到Activity上
                mWindowManager.addView(mDecor, l);
                //重置对话框状态
                mShowing = true;
                //异步消息处理机制来处理Dialog对话框显示时候的一个回调监听
                sendShowMessage();
            } finally {
            }
        }
    

    分析:
    在show方法里主要做了如下几件工作:

    • 判断当前Dialog对话框窗口是存在,如果存在直接让其显示即可;如果当前窗口不存在,则调用Dialog的回调方法onCreate方法,用户可以在onCreate回调方法中创建一个新的Dialog对话框。
    • 获得Dialog对话框的顶层视图DecorView对象赋值给成员变量mDecor用于addView方法的参数。
      根据条件为当前对话框窗口设置导航栏logo图标等。
    • 获得当前窗口的参数属性赋值给l,用于addView方法的参数。
    • 调用WindowManager#addView方法添加Dialog对话框窗口。
      自此Dialog对话框的添加过程已经完成了,回过头来会发现,其实Dialog对话框窗口的创建添加过程和Activity应用窗口过程是一样一样的。

    4.移除Dialog对话框

    移除或者隐藏对话框的代码也很简单。用户仅仅调用Dialog#cancel方法就可以移除当前Activity之上的对话框了。

    public void cancel() {
            if (!mCanceled && mCancelMessage != null) {
                mCanceled = true;
                // Obtain a new message so this dialog can be re-used
                Message.obtain(mCancelMessage).sendToTarget();
            }
            dismiss();
    }
    

    该方法也很简单,先发送移除Dialog时的监听事件,之后将操作转发到dismiss方法中。

    /**
         * Dismiss this dialog, removing it from the screen. This method can be
         * invoked safely from any thread.  Note that you should not override this
         * method to do cleanup when the dialog is dismissed, instead implement
         * that in {@link #onStop}.
         */
        @Override
        public void dismiss() {
            if (Looper.myLooper() == mHandler.getLooper()) {//主线程
                dismissDialog();
            } else {//子线程
                mHandler.post(mDismissAction);
            }
        }
    

    分析:
    注释解释的很清楚了:该方法可以安全的在任何线程中调用,也就是说可以在子线程中移除对话框而不报错。Looper.myLooper()方法获得的Looper对象是当前线程的Looper,而mHandler.getLooper()方法获得的Looper对象是mHandler所在线程的Looper。由于Android系统规定只要有关UI操作都必须在主线程中,而我们在创建Dialog是在主线程中,mHandler对象是在主线程中创建的,因此mHandler.getLooper()就是主线程的Looper。

    以上代码:如果当前线程为主线程,则调用dismissDialog方法,如果是子线程,则利用Handler将此操作发送到UI线程中操作。

    1.在主线程中移除对话框

    void dismissDialog() {
            //如果对话框的顶层视图不存在或者dialog没有正在显示则不做任何处理
            if (mDecor == null || !mShowing) {
                return;
            }
            //如果对话框窗口已经销毁也不做任何处理
            if (mWindow.isDestroyed()) {
                Log.e(TAG, "Tried to dismissDialog() but the Dialog's window was already destroyed!");
                return;
            }
    
            try {
                //移除对话框
                mWindowManager.removeViewImmediate(mDecor);
            } finally {
                if (mActionMode != null) {
                    mActionMode.finish();
                }
                mDecor = null;
                mWindow.closeAllPanels();
                //空方法,可以在创建dialog的时候重写该方法
                onStop();
                //重置标志位
                mShowing = false;
                //处理对话框移除的监听事件
                sendDismissMessage();
            }
        }
    
    

    分析:

    如果当前Dialog窗口的视图DecorView为空或者当前窗口不存在,则不做任何处理,直接退出当前方法即可。
    如果当前Dialog窗口已经被销毁了也不做任何处理。
    调用WindowManager#removeView方法来移除当前对话框窗口。
    该方法主要作用就是从Activity的窗口管理器mWindowManager中移除对话框窗口的视图,也就是完成了该对话框的移除操作。

    2.在子线程中调用Dialog#cancel

    当子线程调用时就会执行 mHandler.post(mDismissAction)代码。该代码的作用就是将操作转发到主线程中。我们看看mDismissAction的实现如下:

    private final Runnable mDismissAction = new Runnable() {
            public void run() {
                dismissDialog();
            }
    };
    

    我们知道Dialog默认是响应“Back”返回键当前对话框消失事件以及点击Dialog对话框视图以外的地方当前对话框也会消失,而默认的PopupWindow对话框是不支持以上两种事件操作的。那么为什么会是这样呢?此处先分析Dialog对触摸事件的处理,下一节分PopupWindow不支持事件处理的原因。

    public class Dialog implements DialogInterface, Window.Callback,
            KeyEvent.Callback, OnCreateContextMenuListener, Window.OnWindowDismissedCallback {
        ........
               public boolean onKeyUp(int keyCode, KeyEvent event) {
            if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking()
                    && !event.isCanceled()) {
                onBackPressed();
                return true;
            }
            return false;
        }
        ........
        public void onBackPressed() {
            if (mCancelable) {
                cancel();
            }
        }
    
    }
    

    在Dialog类中实现了按键事件KeyEvent.Callback接口类,因此当有用户按键输入事件发生时就会调用KeyEvent.Callback接口类中的相应方法。当按键操作有“抬起”的操作行为时,系统会调用onKeyUp方法。而Dialog类中的onKeyUp方法中会检查当前按键事件是否为“KeyEvent.KEYCODE_BACK”事件,且当前输入事件没有被取消,那么会调用onBackPressed,而该方法中判断如果当前对话框可以被取消则调用cancel方法来取消或者隐藏当前对话框。因此Dialog也就响应了“Back”按键事件之后对话框消失。
    Dialog点击对话框视图以外的地方消失:

    public class Dialog implements DialogInterface, Window.Callback,
            KeyEvent.Callback, OnCreateContextMenuListener, Window.OnWindowDismissedCallback {
        ........
         public boolean dispatchTouchEvent(MotionEvent ev) {
             //响应窗口的触摸事件分发
            if (mWindow.superDispatchTouchEvent(ev)) {
                return true;
            }
            //响应Dialog的触摸事件
            return onTouchEvent(ev);
        }
        ........
    }
    

    分析:
    Dialog类同样也实现了Window.Callback接口事件,同时调用Window#setCallback方法设置了该事件的回调,因此Dialog也同样具有响应触摸事件的功能。当用户点击手机屏幕时,就系统就会自动调用dispatchTouchEvent方法来分发当前窗口的触摸事件。该方法先后做了两件事情:

    先调用Dialog的窗口Window对象的方法Window#superDispatchTouchEvent来处理触摸按键事件。
    如果Window窗口的触摸按键事件处理返回为false,则调用Dialog#onTouchEvent方法来继续处理触摸按键事件。
    有关触摸事件传递机制请参考这篇博客:Android事件分发机制完全解析,带你从源码的角度彻底理解(上)。

    当用户点击Dialog窗口视图以外的地方时,最后时会执行Dialog#onTouchEvent方法的,感兴趣的同学可以自行研究下!那么我们来看看Dialog#onTouchEvent方法源码如下:

     public boolean onTouchEvent(MotionEvent event) {
            if (mCancelable && mShowing && mWindow.shouldCloseOnTouch(mContext, event)) {
                cancel();
                return true;
            }
    
            return false;
        }
    

    分析:
    该方法也很简单,如果if条件满足,则直接调用cancel方法来取消当前对话框,if条件不满足时不做任何处理直接返回。那么我们来看看什么情况下if添加满足导致了调用cancel方法取消对话框。必须满足三个条件:当前对话框可以被取消,对话框正在显示,以及Window.shouldCloseOnTouch方法返回true。前两个条件默认都满足,那么来看看第三个条件什么情况下满足吧!
    Window.shouldCloseOnTouch源码如下:

    /** @hide */
        public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
            if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
                    && isOutOfBounds(context, event) && peekDecorView() != null) {
                return true;
            }
            return false;
        }
    

    分析:
    该方法需要满足四个条件才会返回true。

    • 布尔变量mCloseOnTouchOutside:表示是否支持点击窗口以外的地方窗口可消失。Dialog对话框的窗口默认支持,也就是该条件满足。如果想修改该条件,你可以调用Dialog#setCanceledOnTouchOutside(false)方法来达到点击窗口以外的地方Dialog消失,其实最终是设置mCloseOnTouchOutside变量为false,然后导致shouldCloseOnTouch方法返回false。
    • 当前触摸事件是否为“MotionEvent.ACTION_DOWN”手指按下事件,自然满足。
      调用isOutOfBounds方法判断当前手指点击的坐标是否在Dialog对话框窗口视图之外?
    • 当前Dialog对话框窗口是否添加了视图DecorView?如果对话框显示出来了,自然窗口DecorView对象不为空。
      因此有上面四个条件分析我们得知:只有当isOutOfBounds方法返回true时,条件才成立,shouldCloseOnTouch方法返回值才为true,手指点击Dialog窗口之外的地方Dialog才会消失。所以主要看isOutOfBounds方法的实现了。

    Window#isOutOfBounds源码如下:

     private boolean isOutOfBounds(Context context, MotionEvent event) {
            final int x = (int) event.getX();
            final int y = (int) event.getY();
            final int slop = ViewConfiguration.get(context).getScaledWindowTouchSlop();
            final View decorView = getDecorView();
            return (x < -slop) || (y < -slop)
                    || (x > (decorView.getWidth()+slop))
                    || (y > (decorView.getHeight()+slop));
        }
    

    此方法实现也很简单,判断当前手指按下点击屏幕的坐标x,y是否在Window窗口的视图DecorView宽度高度之外,如果是,则返回true,否则返回false。

    至此:有关Dialog响应“Back”返回按键事件和点击Dialog窗口之外的地方Dialog自动消失事件分析完成了。其实这一块的原理和Activity处理“Back”返回键当前Activity会调用finish方法一样。

    Dialog总结:
    Dialog对话框窗口Window的实现机制和Activity一样。Dialog有一个Window对象,该对象属于PhoneWindow类型用于描述Dialog对话框窗口;PhoneWindow类有一个内部类DecorView,用于描述当前窗口的顶层视图。同样Dialog也实现了Window.Callback接口回调,以便Dialog也可以处理用户的触摸和按键事件。
    转载自:https://blog.csdn.net/feiduclear_up/article/details/49080587

    相关文章

      网友评论

          本文标题:第二十四章 对话框

          本文链接:https://www.haomeiwen.com/subject/urpylctx.html