joy keeps flowin'

DialogFragment中取消弹窗

xx
目次

某天(周六)测试反馈一个本该不能用返回取消的弹窗,是可以用返回手势取消的。心中满是疑惑,代码中已经写了dialog?.setCanceledOnTouchOutside(false)怎么不生效呢?

带着关键字查Google,都说用setCancelable(false)就可以了,大为震惊。日思夜想睡不着,今天一定要查个明白。

Dialog #

Dialog的方法做了是什么?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    // Dialog.java

    /**
     * Sets whether this dialog is canceled when touched outside the window's
     * bounds. If setting to true, the dialog is set to be cancelable if not
     * already set.
     * 
     * @param cancel Whether the dialog should be canceled when touched outside
     *            the window.
     */
    public void setCanceledOnTouchOutside(boolean cancel) {
        if (cancel && !mCancelable) {
            mCancelable = true;
        }
        
        mWindow.setCloseOnTouchOutside(cancel);
    }

    // Window.java

    /** @hide */
    @UnsupportedAppUsage
    public void setCloseOnTouchOutside(boolean close) {
        mCloseOnTouchOutside = close;
        mSetCloseOnTouchOutside = true;
    }

两件事:

  1. 如果cancel的值为true,保证mCancelable为true。
  2. 改变Window的值。

DialogFragment #

为什么是DialogFragment而不是Dialog,是DialogFragment是可以用Fragment的生命周期的,Dialog作为Fragment中的一个成员变量。

Dialog是在DialogFragment创建View的时候,更确切的说是在获取LayoutInflater的时候创建了Dialog实例。DialogFragment的构造方法中穿进去的layoutId也是用作创建dialog,并没有真的把这个layout加载出来显示在Fragment中。

因此更改dialog的属性至少应该在onCreateView之后,

setCancelable做了什么?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
    /**
    * Called when the dialog has detected the user's press of the back
    * key.  The default implementation simply cancels the dialog (only if
    * it is cancelable), but you can override this to do whatever you want.
    *
    * <p>
    * If you target version {@link android.os.Build.VERSION_CODES#TIRAMISU} or later, you
    * should not use this method but register an {@link OnBackInvokedCallback} on an
    * {@link OnBackInvokedDispatcher} that you can retrieve using
    * {@link #getOnBackInvokedDispatcher()}. You should also set
    * {@code android:enableOnBackInvokedCallback="true"} in the application manifest.
    *
    * <p>Alternatively, you
    * can use {@code androidx.activity.ComponentDialog#getOnBackPressedDispatcher()}
    * for backward compatibility.
    *
    * @deprecated Use {@link OnBackInvokedCallback} or
    * {@code androidx.activity.OnBackPressedCallback} to handle back navigation instead.
    * <p>
    * Starting from Android 13 (API level 33), back event handling is
    * moving to an ahead-of-time model and {@link #onBackPressed()} and
    * {@link KeyEvent#KEYCODE_BACK} should not be used to handle back events (back gesture or
    * back button click). Instead, an {@link OnBackInvokedCallback} should be registered using
    * {@link Dialog#getOnBackInvokedDispatcher()}
    * {@link OnBackInvokedDispatcher#registerOnBackInvokedCallback(int, OnBackInvokedCallback)
    * .registerOnBackInvokedCallback(priority, callback)}.
    */
   @Deprecated
   public void onBackPressed() {
       if (mCancelable) {
           cancel();
       }
   }

取消的问题 #

我们知道事件(MotionEvent)先发送至ViewRootImpl,然后给对应的Window。在Dialog中的Window同样对应PhoneWIndow实例。随后Window先把事件传给Window.Callback,Activity和Dialog都实现了Window.Callback接口,所以它们才能收到事件。

Window.Callback先把事件分发给自己的View,从DecorView开始,一直到根节点的View。如果所有View都不消费事件,事件最后还是回到Window.Callback,在这里是Dialog。交给onTouchEvent。可以看到是判断能不能被取消且有没有点击在Window上,如果都满足,Dialog就被cancel了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
    /**
    * Called when a touch screen event was not handled by any of the views
    * under it. This is most useful to process touch events that happen outside
    * of your window bounds, where there is no view to receive it.
    * 
    * @param event The touch screen event being processed.
    * @return Return true if you have consumed the event, false if you haven't.
    *         The default implementation will cancel the dialog when a touch
    *         happens outside of the window bounds.
    */
   public boolean onTouchEvent(@NonNull MotionEvent event) {
       if (mCancelable && mShowing && mWindow.shouldCloseOnTouch(mContext, event)) {
           cancel();
           return true;
       }
       
       return false;
   }

mWindow.shouldCloseOnTouch中判断点击在Window外部需要不需要取消的依据就是mCloseOnTouchOutside

到这里setCanceledOnTouchOutside这一部分清晰了。

在Android中除了MotionEvent还有另一套事件——KeyEvent,用作分发按键。

KeyEvent事件会先交给DecorView,DecorView又把事件给了Window.Callback….是不是很熟悉,没错,又到了DIalog。只不过这次是dispatchKeyEvent。如果没有消费,最后依旧回到Dialog。如果事件手指离开按键,那么会触发onBackPressed()

1
2
3
4
5
    public void onBackPressed() {
        if (mCancelable) {
            cancel();
        }
    }
标签:
Categories: