Dialog PopupWindow Toast 你还有疑惑吗

作者: 小鱼人爱编程 | 来源:发表于2020-07-25 15:28 被阅读0次

    前言

    显示页面除了Activity,使用最多的可能就是Dialog、PopupWindow、Toast了。这三者有相似之处也有不一样的地方,本篇文章旨在厘清三者关系,阐明各自的优缺点,并探讨哪种场合使用它们。
    本篇文章涉及到WindowManager相关知识,如有需要请移步:Window/WindowManager 不可不知之事

    通过本篇文章,你将了解到:

    1、Dialog/PopupWindow/Toast 生命周期
    2、Dialog/PopupWindow/Toast 异同之处
    3、Dialog/PopupWindow/Toast 使用场合

    Dialog/PopupWindow/Toast 生命周期

    在之前的文章有提过:任何View都需要添加到Window上才能展示,这个过程大致分为四个步骤:

    1、构造显示的目标View
    2、获取WindowManager 实例
    2、构造约束Window的WindowManager.LayoutParams
    3、WindowManager.addView(View, LayoutParams)

    Dialog/PopupWindow/Toast 实际上就是封装了上述四个步骤,并提供更进一步的功能及其更丰富的接口使用,接下来我们逐步分析。

    Dialog 生命周期

    先来看看简单demo

            //自定义View
            MyGroup myGroup = new MyGroup(v.getContext());
            //Dialog 实例
            Dialog dialog = new Dialog(v.getContext());
            //添加View
            dialog.setContentView(myGroup);
            //最终展示
            dialog.show();
    

    先看看Dialog构造函数:

        Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
            //themeResId 指定Dialog样式
            if (createContextThemeWrapper) {
                if (themeResId == Resources.ID_NULL) {
                    //若不指定,则使用默认的样式
                    final TypedValue outValue = new TypedValue();
                    context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
                    themeResId = outValue.resourceId;
                }
                mContext = new ContextThemeWrapper(context, themeResId);
            } else {
                mContext = context;
            }
    
            //获取WindowManager,context是Activity类型,因此此时获取的WindowManager
            //即是Activity的WindowManager
            mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
            //构造Window对象
            final Window w = new PhoneWindow(mContext);
            mWindow = w;
            //监听touch/key event等事件
            w.setCallback(this);
            //省略
            w.setWindowManager(mWindowManager, null, null);
            //Window默认居中
            w.setGravity(Gravity.CENTER);
        }
    

    构造Window对象时:

    #Window.java
    //构造LayoutParams
        private final WindowManager.LayoutParams mWindowAttributes =
                new WindowManager.LayoutParams();
    
    //WindowManager.java
        public static final int TYPE_APPLICATION        = 2;
        public LayoutParams() {
            super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
            type = TYPE_APPLICATION;
            format = PixelFormat.OPAQUE;
        }
    
    

    可以看出,Dialog构造方法主要做了两件事:

    1、构造WindowManager
    2、构造Window对象,同时在Window里会初始化WindowManager.LayoutParams 变量

    完成了四个步骤的第二、三步:构造WindowManager/LayoutParams对象。

    再看看setContentView(XX)

    #Dialog.java
        public void setContentView(@android.annotation.NonNull View view) {
            //Window 方法,实例是PhoneWindow
            mWindow.setContentView(view);
        }
    
    #PhoneWindow.java
        public void setContentView(View view) {
            setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        }
    
        @Override
        public void setContentView(View view, ViewGroup.LayoutParams params) {
            if (mContentParent == null) {
                //构造DecorView
                installDecor();
            } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
                mContentParent.removeAllViews();
            }
            if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
                //省略
            } else {
                //mContentParent 为 DecorView 子View
                //将自定义View添加到mContentParent里,最终也是挂到了DecorView Tree里
                mContentParent.addView(view, params);
            }
            //省略
        }
    

    其中有关DecorView的创建过程请移步:Android DecorView 一窥全貌(上)

    setContentView(XX)构造了DecorView,并将自定义View添加到DecorView里

    最后看看dialog.show()

        public void show() {
            if (mShowing) {
                //Dialog 正在展示,则退出
                return;
            }
            if (!mCreated) {
                //最终调用onCreate(xx)
                dispatchOnCreate(null);
            } else {
                //省略
            }
            onStart();
            //获取DecorView,在setContentView(XX)时已经构造好DecorView
            mDecor = mWindow.getDecorView();
            //在创建Window时已经构造好
            WindowManager.LayoutParams l = mWindow.getAttributes();
    
            //添加DecorView
            mWindowManager.addView(mDecor, l);
            mShowing = true;
        }
    

    dialog.show() 完成了四个步骤中的最后一步:addView(xx)
    至此,Dialog创建完毕并显示,通过上述分析可知,Dialog将四个步骤封装了。

    如何关闭Dialog

    既然是通过WindowManager.addView(xx)添加的View,那么Dialog关闭相应的也需要调用WindowManager.removeView(xx),此处调用的是WindowManager.removeViewImmediate(xx),表示立即执行销毁动作。

    #Dialog.java
        @Override
        public void dismiss() {
            if (Looper.myLooper() == mHandler.getLooper()) {
                //主线程直接执行
                dismissDialog();
            } else {
                //子线程切换到主线程执行
                mHandler.post(mDismissAction);
            }
        }
    
        @UnsupportedAppUsage
        void dismissDialog() {
            if (mDecor == null || !mShowing) {
                return;
            }
            try {
                //移除DecorView
                mWindowManager.removeViewImmediate(mDecor);
            } finally {
                //调用onStop
                onStop();
                mShowing = false;
                sendDismissMessage();
            }
        }
    

    Dialog 生命周期如下:


    image.png

    PopupWindow 生命周期

    同样的简单demo

            //PopupWindow 宽、高
            popupWindow = new PopupWindow(400, 400);
            MyGroup myGroup = new MyGroup(v.getContext());
            popupWindow.setContentView(myGroup);
            //展示popupWindow
            popupWindow.showAsDropDown(button);
    

    看得出来PopupWindow创建与Dialog类似。

    先看看构造函数:

        public PopupWindow(View contentView, int width, int height, boolean focusable) {
            //contentView 为自定义View
            if (contentView != null) {
                mContext = contentView.getContext();
                //获取WindowManager mContext 属于Activity类型
                //与Dialog 一样,WindowManager 就是Activity WindowManager
                mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
            }
            //设置 mContentView = contentView;
            setContentView(contentView);
            //设置Window宽、高
            setWidth(width);
            setHeight(height);
            //设置获取焦点与否
            setFocusable(focusable);
        }
    

    注意,PopupWindow 默认宽高为0,因此需要外部设置宽高值

    setContentView(XX)

        public void setContentView(View contentView) {
            if (isShowing()) {
                return;
            }
            //赋值
            mContentView = contentView;
            if (mContext == null && mContentView != null) {
                //获取Context
                mContext = mContentView.getContext();
            }
            if (mWindowManager == null && mContentView != null) {
                //根据Context获取WindowManager
                mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
            }
        }
    

    popupWindow.showAsDropDown(View anchor)

    View anchor 指的是先锚定一个View,PopupWindow根据这个View的位置来确定自己的位置。

        public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
            if (isShowing() || !hasContentView()) {
                //正在展示,则不处理后续
                return;
            }
            //一系列监听锚定的View
            attachToAnchor(anchor, xoff, yoff, gravity);
            //构造 LayoutParams,并设置其一些参数
            final WindowManager.LayoutParams p =
                    createPopupLayoutParams(anchor.getApplicationWindowToken());
            
            //构造"DecorView",该DecorView不是我们常见的DecorView,而是PopupWindow里的内部类
            //该View作为Window的根View
            preparePopup(p);
            
            //根据anchor确认Window的起始位置
            final boolean aboveAnchor = findDropDownPosition(anchor, p, xoff, yoff,
                    p.width, p.height, gravity, mAllowScrollingAnchorParent);
            updateAboveAnchor(aboveAnchor);
            //添加到Window里。WindowManager.addView(xx)
            invokePopup(p);
        }
    

    至此,PopupWindow创建完毕,可以看出以上步骤包括了Window显示的四个步骤。

    如何关闭PopupWindow

    与Dialog 类似,PopupWindow 有个方法:

    public void dismiss();
    

    该方法最后调用了WindowManager.removeViewImmediate(xx)方法移除Window。

    Toast 生命周期

    还是一个小demo:

    Toast.makeText(App.getApplication(), "hello toast", Toast.LENGTH_LONG).show();
    

    makeText(XX)是个静态方法:

        public static Toast makeText(@android.annotation.NonNull Context context, @android.annotation.Nullable Looper looper,
                                     @android.annotation.NonNull CharSequence text, @Duration int duration) {
            //构造 Toast对象
            Toast result = new Toast(context, looper);
            //加载View
            LayoutInflater inflate = (LayoutInflater)
                    context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
            TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
            //tv是v的子View 设置显示的内容
            tv.setText(text);
            //记录到Toast里
            result.mNextView = v;
            result.mDuration = duration;
            return result;
        }
    

    Toast.show()方法

        public void show() {
            INotificationManager service = getService();
            String pkg = mContext.getOpPackageName();
            //构造TN对象
            TN tn = mTN;
            tn.mNextView = mNextView;
            final int displayId = mContext.getDisplayId();
            
            try {
                //加入到队列里
                service.enqueueToast(pkg, tn, mDuration, displayId);
            } catch (RemoteException e) {
                // Empty
            }
        }
    

    到此Toast创建并显示出来,但是我们并没有看到熟悉的WindowManager.addView(xx),继续来看看。
    show()方法里构造了TN对象,最后该对象被加入到了INotificationManager里。该类是底层服务类,其实现类是:NotificationManagerService.java。既然传给了底层,那么势必要有传回来的动作,查看TN类发现:

        public void show(IBinder windowToken) {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            //发送到handler执行
            mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
        }
    
        public void handleShow(IBinder windowToken) {
            if (mView != mNextView) {
                // remove the old view if necessary
                handleHide();
                mView = mNextView;
                Context context = mView.getContext().getApplicationContext();
                String packageName = mView.getContext().getOpPackageName();
                if (context == null) {
                    context = mView.getContext();
                }
                
                //获取 WindowManager 对象
                mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
                final Configuration config = mView.getContext().getResources().getConfiguration();
                final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
                
                //WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
                mParams.gravity = gravity;
                if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
                    mParams.horizontalWeight = 1.0f;
                }
                if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
                    mParams.verticalWeight = 1.0f;
                }
                //设置Toast 坐标等属性
                mParams.x = mX;
                mParams.y = mY;
                mParams.verticalMargin = mVerticalMargin;
                mParams.horizontalMargin = mHorizontalMargin;
                mParams.packageName = packageName;
                mParams.hideTimeoutMilliseconds = mDuration ==
                        Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
                mParams.token = windowToken;
                if (mView.getParent() != null) {
                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                    mWM.removeView(mView);
                }
                try {
                    //添加到Window
                    mWM.addView(mView, mParams);
                    trySendAccessibilityEvent();
                } catch (WindowManager.BadTokenException e) {
                    /* ignore */
                }
            }
        }
    

    又看到了熟悉的addView(xx)流程。总结来说:

    make() 方法构造Toast
    show() 方法 将要显示的内容加入到service
    service根据时间长短通过handler通知UI进行展示

    如何关闭Toast

    既然Toast显示策略都在service里完成,那么当时间到了之后让Toast消失也是service通知上层销毁Window

        public void cancel() {
            if (localLOGV) Log.v(TAG, "CANCEL: " + this);
            mHandler.obtainMessage(CANCEL).sendToTarget();
        }
    
        public void handleHide() {
            if (mView != null) {
                if (mView.getParent() != null) {
                    //销毁Window
                    mWM.removeViewImmediate(mView);
                }
                try {
                    getService().finishToken(mPackageName, this);
                } catch (RemoteException e) {
                }
                mView = null;
            }
        }
    

    Dialog/PopupWindow/Toast 异同之处

    上边分析了三者的生命周期,了解到他们都是通过addView(xx)添加View到Window进行展示的,那么他们各自的特点以及侧重点是体现在哪些方面呢?接下来分析。
    当我们分别运行上边的三个demo,发现:
    Dialog 表现:

    居中展示、外部有蒙层、点击屏幕外Dialog消失、点击返回键Dialog消失、Dialog 拦截了屏幕上所有的touch/key 事件。
    Dialog需要Activity类型的Context启动。
    有动画。

    PopupWindow 表现

    基于某个锚点显示,可以偏移任何距离。点击屏幕外PopupWindow不消失,PopupWindow 仅仅拦截自身区域内的touch/key 事件。
    PopupWindow需要Activity类型的Context启动。
    有动画。

    Toast 表现

    Toast 在屏幕底部弹出一段文本,该文本在展示指定的时间后消失。
    Toast 不强制需要Activity类型的Context启动。
    有动画。

    接下来看看造成以上差异之处的原因:

    Window 位置确定

    WindowManager.LayoutParams.gravity
    指定Window方位,如居中、居左、居右、居底、居顶。

    WindowManager.LayoutParams.x
    WindowManager.LayoutParams.y

    这俩参数确定Window 距离"gravity"指定方位的偏移。
    如当gravity=Gravity.LEFT 那么layoutParams.x = 200(正数),表示X轴向右偏移的距离,负数反之。
    当gravity=Gravity.RIGHT 那么layoutParams.x = 200,表示X轴向左偏移的距离,负数反之。
    同理垂直方向也是一样道理。
    因此Window 位置确定是通过gravity 和x/y属性结合判断的。
    Dialog 位置确定

        Dialog(@android.annotation.NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
            //省略
            final Window w = new PhoneWindow(mContext);
            //设置gravity
            w.setGravity(Gravity.CENTER);
        }
    

    Dialog 构造函数里设置Window居中,因此demo里表现出来的Dialog居中展示。
    因此改变"gravity"默认值:

    dialog.getWindow().getAttributes().gravity = Gravity.XX

    PopupWindow 位置确定

        public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
            //省略...
            //确定layoutParams.x/layoutParams.y 的值
            //xoff/yoff 表示的是window 距离锚点anchor的偏移,默认是anchor的左下角
            //gravity指的是window与anchor的对齐方式,比如Gravity.RIGHT,表示Window与anchor右对齐
            //当xoff/yoff、gravity同时设置时,先按照anchor的左下角偏移xoff/yoff,得出当前的layoutParams.x/layoutParams.y值
            //再根据gravity调整layoutParams.x/layoutParams.y值
            final boolean aboveAnchor = findDropDownPosition(anchor, p, xoff, yoff,
                    p.width, p.height, gravity, mAllowScrollingAnchorParent);
            //省略...
        }
    

    findDropDownPosition(xx) 该方法确定了PopupWindow 的WindowManager.LayoutParams.x/WindowManager.LayoutParams.y值。
    再来看看WindowManager.LayoutParams.gravity如何确定的:

        protected final WindowManager.LayoutParams createPopupLayoutParams(IBinder token) {
            final WindowManager.LayoutParams p = new WindowManager.LayoutParams();
            //计算出LayoutParams.gravity
            p.gravity = computeGravity();
            //省略
            return p;
        }
    
        private int computeGravity() {
            //根据mGravity来确定gravity
            int gravity = mGravity == Gravity.NO_GRAVITY ?  Gravity.START | Gravity.TOP : mGravity;
            if (mIsDropdown && (mClipToScreen || mClippingEnabled)) {
                gravity |= Gravity.DISPLAY_CLIP_VERTICAL;
            }
            return gravity;
        }
    

    而mGravity是可以在外部设置的:

        public void showAtLocation(View parent, int gravity, int x, int y) {d
            mParentRootView = new WeakReference<>(parent.getRootView());
            showAtLocation(parent.getWindowToken(), gravity, x, y);
        }
    
        public void showAtLocation(IBinder token, int gravity, int x, int y) {
            //省略...
            mGravity = gravity;
            //省略
        }
    

    因此,可以通过showAtLocation(xx)设置PopupWindow的Gravity。
    此处需要注意的是:
    showAsDropDown(xx)参数里的gravity指的是PopupWindow与锚点View的对齐方式。
    而showAtLocation(xx)参数里的gravity才是PopupWindow的Gravity。

    Toast 位置确定
    Toast 默认底部水平居中。在Toast.TN 类里,当展示Toast时调用handleShow(xx)方法:

        public void handleShow(IBinder windowToken) {
            //省略
            if (mView != mNextView) {
                // 省略
                //通过mGravity计算
                final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
                mParams.gravity = gravity;
                //x、y的值
                mParams.x = mX;
                mParams.y = mY;
            }
        }
    

    而mGravity、mX、mY可以在外部设置:

        public void setGravity(int gravity, int xOffset, int yOffset) {
            mTN.mGravity = gravity;
            mTN.mX = xOffset;
            mTN.mY = yOffset;
        }
    

    因此调用setGravity(xx)可以改变Toast展示的位置

    Window外部区域变暗

    Dialog弹出时外部区域会变暗,该效果由以下字段控制

    WindowManager.LayoutParams.dimAmount
    取值float类型
    范围[0-1]
    值越大表示不透明度越高
    0表示不变暗,1表示完全变暗
    该值需要生效,需要配合另外字段使用:
    layoutParams.flags |= WindowManager.LayoutParams.FLAG_DIM_BEHIND;

    Dialog 外部变暗

        protected ViewGroup generateLayout(DecorView decor) {
            if (a.getBoolean(R.styleable.Window_backgroundDimEnabled,
                    mIsFloating)) {
                if ((getForcedWindowFlags()&WindowManager.LayoutParams.FLAG_DIM_BEHIND) == 0) {
                    //设置标记,表示支持变暗
                    params.flags |= WindowManager.LayoutParams.FLAG_DIM_BEHIND;
                }
                if (!haveDimAmount()) {
                    //设置变暗的具体值
                    params.dimAmount = a.getFloat(
                            android.R.styleable.Window_backgroundDimAmount, 0.5f);
                }
            }
        }
    

    可以看出Dialog dimAmount值从style里获取,该style里的默认值是0.6。当然我们可以在外部修改dimAmount值。

                    dialog.setContentView(myGroup);
                    dialog.getWindow().getAttributes().dimAmount = 0.3f;
                    dialog.show();
    

    需要注意的是,dimAmount赋值操作需要在setContentView(xx)之后进行,否则设置的值会被setContentView(xx)重置。

    PopupWindow和Toast 没有对此设置相应的值,因此就没有外部区域变暗的说法。

    Window touch/key 事件

    Dialog 事件接收
    点击Dialog 外部时(touch),Dialog消失;点击物理返回键时(key),Dialog消失。因此我们可以猜测出Dialog是接收到了touch/key事件,并判断如果touch事件在Window外部,那么关闭Dialog。
    涉及到两个步骤:

    1、能接收到外部touch/key 事件
    2、对事件进行相应的处理(是否关闭Dialog)

    1、设置Dialog能否接收touch/key 事件
    Window 默认接收外部点击事件和key事件,Dialog没有更改此默认值,因此能接收到touch/key 事件。
    2、对接收的事件做处理
    Dialog 实现了Window.Callback 接口,重写方法里对touch事件做处理

    #Dialog.java
        public boolean dispatchTouchEvent(@android.annotation.NonNull MotionEvent ev) {
            //先交给Dialog可见区域处理
            if (mWindow.superDispatchTouchEvent(ev)) {
                return true;
            }
            //事件没消费,继续处理
            return onTouchEvent(ev);
        }
    
        public boolean onTouchEvent(@android.annotation.NonNull MotionEvent event) {
            //shouldCloseOnTouch(xx)
            //该方法判断是否是up事件且是否点击在Dialog外部区域且是否设置了可以关闭Dialog的标记
            //都满足,则返回true
            if (mCancelable && mShowing && mWindow.shouldCloseOnTouch(mContext, event)) {
                //符合条件,则关闭Dialog
                cancel();
                return true;
            }
    
            return false;
        }
    

    同样的,Dialog 实现了KeyEvent.Callback,重写方法里对key事件做处理

    #Dialog.java
        public boolean dispatchKeyEvent(@android.annotation.NonNull KeyEvent event) {
            if ((mOnKeyListener != null) && (mOnKeyListener.onKey(this, event.getKeyCode(), event))) {
                return true;
            }
            //可见区域做处理
            if (mWindow.superDispatchKeyEvent(event)) {
                return true;
            }
            //继续分发
            return event.dispatch(this, mDecor != null
                    ? mDecor.getKeyDispatcherState() : null, this);
        }
    
        public boolean onKeyUp(int keyCode, @android.annotation.NonNull KeyEvent event) {
            if ((keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE)
                    && event.isTracking()
                    && !event.isCanceled()) {
                onBackPressed();
                return true;
            }
            return false;
        }
    
        public void onBackPressed() {
            //标记生效,则移除Dialog
            if (mCancelable) {
                cancel();
            }
        }
    

    从上面可以看出,Dialog点击外部和点击物理返回键消失需要同时满足两个条件,那么想要Dialog不消失,只要不满足其中某个条件即可。实际上Dialog是根据第二个条件设置标记位,已经为我们封装好了方法:
    点击外部不消失:

    dialog.setCanceledOnTouchOutside(false);

    点击物理返回键不消失:

    dialog.setCancelable(false);

    值得注意的是:调用了上述方法,Dialog还是接收了事件,只是不关闭Dialog而已。事件并没有分发到其底下的Window。

    PopupWindow 事件接收
    与Dialog类似,看其是否满足两个条件。
    先来看看PopupWindow 调用栈:

    showAsDropDown(xx)->createPopupLayoutParams(xx)->computeFlags(xx)

    #PopupWindow.java
        private int computeFlags(int curFlags) {
            //省略
            if (!mFocusable) {
                //焦点功能没开启,则标记FLAG_NOT_FOCUSABLE
                //该标记下,Window不接收其外部区域的touch事件
                //也不接收key事件
                curFlags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
                if (mInputMethodMode == INPUT_METHOD_NEEDED) {
                    //键盘相关
                    curFlags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
                }
            } else if (mInputMethodMode == INPUT_METHOD_NOT_NEEDED) {
                curFlags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
            }
            //省略
        }
    

    computeFlags(xx)计算WindowManager.LayoutParams.flags的值。PopupWindow是否接收事件取决于"mFocusable",在我们的demo里并没有对该值进行设置,默认为false,因此PopupWindow不能接收外部点击事件与key事件,当然也就不能处理是否关闭PopupWindow的逻辑了。
    而"mFocusable"字段的赋值可以在PopupWindow构造函数里指定或者调用

    public void setFocusable(boolean focusable)

    当指定focusable=true时,PopupWindow就能接收touch/key事件了,PopupDecorView 负责接收事件处理:

        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            //onTouch 优先执行
            if (mTouchInterceptor != null && mTouchInterceptor.onTouch(this, ev)) {
                return true;
            }
            return super.dispatchTouchEvent(ev);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            final int x = (int) event.getX();
            final int y = (int) event.getY();
    
            //接收Down事件关闭
            if ((event.getAction() == MotionEvent.ACTION_DOWN)
                    && ((x < 0) || (x >= getWidth()) || (y < 0) || (y >= getHeight()))) {
                dismiss();
                return true;
            } else if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
                //另一类事件
                dismiss();
                return true;
            } else {
                return super.onTouchEvent(event);
            }
        }
    

    key事件差不多,此处略过。
    总结来说:

    设置focusable为true即可点击外部消失PopupWindow,反之则不消失

    网上一些文章说的是PopupWindow 会阻塞程序,这种观点是错误的。实际上是下一层的Window(Activity)没有接收到事件,当然不会做任何处理了

    Toast 事件接收
    Toast 一般用来定时展示一个文本,因此一般无需接收事件。
    在Toast 构造函数里,会构造TN对象,该对象里初始化WindowManager.LayoutParams.flags参数:

        TN(String packageName, @android.annotation.Nullable Looper looper) {
            final WindowManager.LayoutParams params = mParams;
            //省略
            params.setTitle("Toast");
            //设置不接收外部的touch事件和key事件
            params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
                    | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                    | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
            //省略
        }
    

    关于Window touch/key 事件详细字段内容请移步:Window/WindowManager 不可不知之事
    本篇只说明设置了哪些参数。

    启动Dialog/PopupWindow/Toast 所需的Context限制

    请移步:Android各种Context的前世今生

    Window 动画

    控制Window 动画的字段是:

    WindowManager.LayoutParams.windowAnimations

    Dialog 动画
    Dialog 默认动画:

        <style name="Animation.Dialog">
            <item name="windowEnterAnimation">@anim/dialog_enter</item>
            <item name="windowExitAnimation">@anim/dialog_exit</item>
        </style>
    

    替换Dialog默认动画,定义Style

        <style name="myAnim">
            <item name="android:windowEnterAnimation">@anim/myanim</item>
        </style>
    
        <style name="myDialog" parent="myTheme">
            <item name="android:windowAnimationStyle">@style/myAnim</item>
        </style>
    

    Dialog 构造函数引用该Style。
    当然也可以单独设置

    dialog.getWindow().getAttributes().windowAnimations = R.style.myAnim;

    PopupWindow 动画
    PopupWindow 默认没有动画,其加载动画时机:

    createPopupLayoutParams(xx)->computeAnimationResource(xx)

    在外部指定其动画:

        public void setAnimationStyle(int animationStyle) {
            mAnimationStyle = animationStyle;
        }
    

    popupWindow.setAnimationStyle(R.style.myAnim);

    Toast 动画
    在Toast.TN的构造函数里,有默认动画:

    params.windowAnimations = com.android.internal.R.style.Animation_Toast;
    
        <style name="Animation.Toast">
            <item name="windowEnterAnimation">@anim/toast_enter</item>
            <item name="windowExitAnimation">@anim/toast_exit</item>
        </style>
    

    Toast 没有提供对外接口设置Window动画。

    Dialog/PopupWindow/Toast 使用场合

    从上边分析可以看出,造成Window表现差异的实际上就是WindowManager.LayoutParams 参数的差异。因此重点是我们能否拿到WindowManager.LayoutParams对象。
    对于Dialog:

    可以通过dialog.getWindow().getAttributes() 获取WindowManager.LayoutParams对象,对象获取到了那么里边的各种参数就可以设置了。
    需要注意的是:setContentView(xx)可能会重置LayoutParams里的一些参数,因此一般我们更改LayoutParams参数最好在setContentView(xx)之后。

    对于PopupWindow/Toast
    这两者并没有提供方法获取WindowManager.LayoutParams对象,仅仅提供一些方法单独设置WindowManager.LayoutParams对象里的一些变量。比如设置Window的位置、设置touch/key 事件接收、动画等。

    使用建议

    1、对于想要设置背景蒙层的,建议使用Dialog。PopupWindow/Toast并没有提供方法设置该参数
    2、对于想要基于某个锚点(View)位置展示Window的,建议使用PopupWindow。当然Dialog/Toast也是可以指定位置,只是PopupWindow已经将这套封装了,不用重复造轮子
    3、对于想要监听外部touch/key 事件的,建议使用Dialog;Dialog重写touch/key比较方便。
    4、对于想要简单弹出提示,并且有时长限制的,建议使用Toast。

    如若对Dialog/PopupWindow/Toast 都不能解决你的需求,那就更容易了。这三者都是封装了WindowManager的操作,我们直接使用原生的WindowManager,能拿到所有参数,想要啥效果都可以设置。

    Dialog/PopupWindow/Toast 默认动画都是用了系统的属性,对styleable/style/attr 有疑问的,请移步:
    全网最深入 Android Style/Theme/Attr/Styleable/TypedArray 清清楚楚明明白白

    本文源码基于Android 10.0

    相关文章

      网友评论

        本文标题:Dialog PopupWindow Toast 你还有疑惑吗

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