DialogFragment 监听外部点击事件

疫情概要

国内疫情

6月20日0—24时,31个省(自治区、直辖市)和新疆生产建设兵团报告新增确诊病例26例,其中境外输入病例1例(在福建),本土病例25例(北京22例,河北3例);无新增死亡病例;新增疑似病例3例,均为本土病例(均在北京)。

国际疫情

世界卫生组织20日公布的最新数据显示,中国以外新冠确诊病例达到8440072例。   

世卫组织每日疫情报告显示,截至欧洲中部时间20日10时(北京时间16时),中国以外新冠确诊病例较前一日增加138950例,达到8440072例;中国以外死亡病例较前一日增加6271例,达到452328例。   全球范围内,新冠确诊病例较前一日增加138980例,达到8525042例;死亡病例较前一日增加6271例,达到456973例。

前言

本篇主要记录使用DialogFragment实现的弹框,对点击非内容区域事件的监听和控制。

这个需求在着手实现之初,就断定是需要从事件的分发机制处展开的。

实现过程分析

DialogFragment 显示Dialog的具体流程为:

FragmentManager.java

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
void moveToState(Fragment f, int newState, int transit, int transitionStyle,
boolean keepActive) {

...
switch (f.mState) {
case Fragment.INITIALIZING:
if (DEBUG) Log.v(TAG, "moveto CREATED: " + f);
if (f.mSavedFragmentState != null) {
f.mSavedFragmentState.setClassLoader(mHost.getContext().getClassLoader());
f.mSavedViewState = f.mSavedFragmentState.getSparseParcelableArray(
FragmentManagerImpl.VIEW_STATE_TAG);
f.mTarget = getFragment(f.mSavedFragmentState,
FragmentManagerImpl.TARGET_STATE_TAG);
if (f.mTarget != null) {
f.mTargetRequestCode = f.mSavedFragmentState.getInt(
FragmentManagerImpl.TARGET_REQUEST_CODE_STATE_TAG, 0);
}
f.mUserVisibleHint = f.mSavedFragmentState.getBoolean(
FragmentManagerImpl.USER_VISIBLE_HINT_TAG, true);
if (!f.mUserVisibleHint) {
f.mDeferStart = true;
if (newState > Fragment.STOPPED) {
newState = Fragment.STOPPED;
}
}
}
f.mHost = mHost;
f.mParentFragment = mParent;
f.mFragmentManager = mParent != null
? mParent.mChildFragmentManager : mHost.getFragmentManagerImpl();
dispatchOnFragmentPreAttached(f, mHost.getContext(), false);
f.mCalled = false;
f.onAttach(mHost.getContext());
if (!f.mCalled) {
throw new SuperNotCalledException("Fragment " + f
+ " did not call through to super.onAttach()");
}
if (f.mParentFragment == null) {
mHost.onAttachFragment(f);
} else {
f.mParentFragment.onAttachFragment(f);
}
dispatchOnFragmentAttached(f, mHost.getContext(), false);

if (!f.mRetaining) {
f.performCreate(f.mSavedFragmentState);
dispatchOnFragmentCreated(f, f.mSavedFragmentState, false);
} else {
f.restoreChildFragmentState(f.mSavedFragmentState);
f.mState = Fragment.CREATED;
}
f.mRetaining = false;
if (f.mFromLayout) {
// For fragments that are part of the content view
// layout, we need to instantiate the view immediately
// and the inflater will take care of adding it.
f.mView = f.performCreateView(f.getLayoutInflater(
f.mSavedFragmentState), null, f.mSavedFragmentState);
if (f.mView != null) {
f.mInnerView = f.mView;
if (Build.VERSION.SDK_INT >= 11) {
ViewCompat.setSaveFromParentEnabled(f.mView, false);
} else {
f.mView = NoSaveStateFrameLayout.wrap(f.mView);
}
if (f.mHidden) f.mView.setVisibility(View.GONE);
f.onViewCreated(f.mView, f.mSavedFragmentState);
dispatchOnFragmentViewCreated(f, f.mView, f.mSavedFragmentState, false);
} else {
f.mInnerView = null;
}
}
case Fragment.CREATED:
if (newState > Fragment.CREATED) {
if (DEBUG) Log.v(TAG, "moveto ACTIVITY_CREATED: " + f);
if (!f.mFromLayout) {
ViewGroup container = null;
if (f.mContainerId != 0) {
if (f.mContainerId == View.NO_ID) {
throwException(new IllegalArgumentException(
"Cannot create fragment "
+ f
+ " for a container view with no id"));
}
container = (ViewGroup) mContainer.onFindViewById(f.mContainerId);
if (container == null && !f.mRestored) {
String resName;
try {
resName = f.getResources().getResourceName(f.mContainerId);
} catch (NotFoundException e) {
resName = "unknown";
}
throwException(new IllegalArgumentException(
"No view found for id 0x"
+ Integer.toHexString(f.mContainerId) + " ("
+ resName
+ ") for fragment " + f));
}
}
f.mContainer = container;
f.mView = f.performCreateView(f.getLayoutInflater(
f.mSavedFragmentState), container, f.mSavedFragmentState);
if (f.mView != null) {
f.mInnerView = f.mView;
if (Build.VERSION.SDK_INT >= 11) {
ViewCompat.setSaveFromParentEnabled(f.mView, false);
} else {
f.mView = NoSaveStateFrameLayout.wrap(f.mView);
}
if (container != null) {
container.addView(f.mView);
}
if (f.mHidden) {
f.mView.setVisibility(View.GONE);
}
f.onViewCreated(f.mView, f.mSavedFragmentState);
dispatchOnFragmentViewCreated(f, f.mView, f.mSavedFragmentState,
false);
// Only animate the view if it is visible. This is done after
// dispatchOnFragmentViewCreated in case visibility is changed
f.mIsNewlyAdded = (f.mView.getVisibility() == View.VISIBLE)
&& f.mContainer != null;
} else {
f.mInnerView = null;
}
}

f.performActivityCreated(f.mSavedFragmentState);
dispatchOnFragmentActivityCreated(f, f.mSavedFragmentState, false);
if (f.mView != null) {
f.restoreViewState(f.mSavedFragmentState);
}
f.mSavedFragmentState = null;
}
case Fragment.ACTIVITY_CREATED:
if (newState > Fragment.ACTIVITY_CREATED) {
f.mState = Fragment.STOPPED;
}
case Fragment.STOPPED:
if (newState > Fragment.STOPPED) {
if (DEBUG) Log.v(TAG, "moveto STARTED: " + f);
f.performStart();
dispatchOnFragmentStarted(f, false);
}
case Fragment.STARTED:
if (newState > Fragment.STARTED) {
if (DEBUG) Log.v(TAG, "moveto RESUMED: " + f);
f.performResume();
dispatchOnFragmentResumed(f, false);
f.mSavedFragmentState = null;
f.mSavedViewState = null;
}
...

...

}

Fragment.java

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
34
35
36
void performCreate(Bundle savedInstanceState) {
if (mChildFragmentManager != null) {
mChildFragmentManager.noteStateNotSaved();
}
mState = CREATED;
mCalled = false;
onCreate(savedInstanceState);
if (!mCalled) {
throw new SuperNotCalledException("Fragment " + this
+ " did not call through to super.onCreate()");
}
}

View performCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
if (mChildFragmentManager != null) {
mChildFragmentManager.noteStateNotSaved();
}
return onCreateView(inflater, container, savedInstanceState);
}

void performActivityCreated(Bundle savedInstanceState) {
if (mChildFragmentManager != null) {
mChildFragmentManager.noteStateNotSaved();
}
mState = ACTIVITY_CREATED;
mCalled = false;
onActivityCreated(savedInstanceState);
if (!mCalled) {
throw new SuperNotCalledException("Fragment " + this
+ " did not call through to super.onActivityCreated()");
}
if (mChildFragmentManager != null) {
mChildFragmentManager.dispatchActivityCreated();
}
}

从上面可以看出Fragment的部分生命周期回调:

  1. onAttach(mHost.getContext())
  2. performCreate(f.mSavedFragmentState) performCreate内部则回调了onCreate
  3. f.performCreateView(f.getLayoutInflater(f.mSavedFragmentState), null, f.mSavedFragmentState) 这个是重点,DialogFragment会重写getLayoutInflater -> performGetLayoutInflater -> onGetLayoutInflater 调用顺序调用到的onGetLayoutInflater方法创建Dialog对象(onCreateDialog),并将performCreateView返回的view保存到Fragmnet内。即onViewCreated的第一个入参。performCreateView内会回调onCreateView
  4. onViewCreated(f.mView, f.mSavedFragmentState)
  5. performActivityCreated(f.mSavedFragmentState) performActivityCreated内部会回调 onActivityCreated

DialogFragment.java

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
/** @hide */
@Override
public LayoutInflater onGetLayoutInflater(Bundle savedInstanceState) {
if (!mShowsDialog) {
return super.onGetLayoutInflater(savedInstanceState);
}

mDialog = onCreateDialog(savedInstanceState);
switch (mStyle) {
case STYLE_NO_INPUT:
mDialog.getWindow().addFlags(
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
// fall through...
case STYLE_NO_FRAME:
case STYLE_NO_TITLE:
mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
}
if (mDialog != null) {
return (LayoutInflater)mDialog.getContext().getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
}
return (LayoutInflater) mHost.getContext().getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
}

/**
* Override to build your own custom Dialog container. This is typically
* used to show an AlertDialog instead of a generic Dialog; when doing so,
* {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)} does not need
* to be implemented since the AlertDialog takes care of its own content.
*
* <p>This method will be called after {@link #onCreate(Bundle)} and
* before {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}. The
* default implementation simply instantiates and returns a {@link Dialog}
* class.
*
* <p><em>Note: DialogFragment own the {@link Dialog#setOnCancelListener
* Dialog.setOnCancelListener} and {@link Dialog#setOnDismissListener
* Dialog.setOnDismissListener} callbacks. You must not set them yourself.</em>
* To find out about these events, override {@link #onCancel(DialogInterface)}
* and {@link #onDismiss(DialogInterface)}.</p>
*
* @param savedInstanceState The last saved instance state of the Fragment,
* or null if this is a freshly created Fragment.
*
* @return Return a new Dialog instance to be displayed by the Fragment.
*/
public Dialog onCreateDialog(Bundle savedInstanceState) {
return new Dialog(getActivity(), getTheme());
}

@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);

if (!mShowsDialog) {
return;
}

View view = getView();
if (view != null) {
if (view.getParent() != null) {
throw new IllegalStateException(
"DialogFragment can not be attached to a container view");
}
mDialog.setContentView(view);
}
final Activity activity = getActivity();
if (activity != null) {
mDialog.setOwnerActivity(activity);
}
mDialog.setCancelable(mCancelable);
if (!mDialog.takeCancelAndDismissListeners("DialogFragment", this, this)) {
throw new IllegalStateException(
"You can not set Dialog's OnCancelListener or OnDismissListener");
}
if (savedInstanceState != null) {
Bundle dialogState = savedInstanceState.getBundle(SAVED_DIALOG_STATE_TAG);
if (dialogState != null) {
mDialog.onRestoreInstanceState(dialogState);
}
}
}

根据Fragment内主要生命周期方法调用顺序和DialogFragment重写的具体方法,可以确定一个调用顺序:(注:括号表示的是括号内的为括号前表示方法内部调用,不是表示入参

onAttach -> onCreate -> performCreateView( onGetLayoutInflater(onCreateDialog) -> onCreateView) -> onViewCreated -> onActivityCreated -> onStart

而且DialogFragment显示对话框主要还是通过内部创建Dialog对象来实现。

从Dialog的 dispatchTouchEvent入手:

Dialog.java

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
34
35
36
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
@Override
public boolean dispatchTouchEvent(@NonNull MotionEvent ev) {
if (mWindow.superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}

/**
* 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在Android中的具体实现类为PhoneWindow,关于事件分发的详细说明可查看文章 Android事件分发机制源码解析。由于我们点击的是外部区域,所以superDispatchTouchEvent不会消费此事件,从而转交给onTouchEvent处理。

onTouchEvent内主要看mWindow.shouldCloseOnTouch(mContext, event),由于PhoneWindow没有重写该方法,所以看Window内的具体实现

Window.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/** @hide */
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
final boolean isOutside =
event.getAction() == MotionEvent.ACTION_UP && isOutOfBounds(context, event)
|| event.getAction() == MotionEvent.ACTION_OUTSIDE;
if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
return true;
}
return false;
}


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));
}

Window#shouldCloseOnTouch主要是判断了点击位置是否在DecorView范围内。

实现方案

通过上述分析,可以通过重写DialogFragment#onCreateDialog(Bundle)方法,返回重写了onTouchEvent的Dialog对象OutsideClickDialog。通过onTouchEvent拦截事件。

OutsideClickDialog具体代码如下:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public static class OutsideClickDialog extends Dialog {

private boolean mCancelable;

private OnOutsideClickListener onOutsideClickListener;

@Override
public void setCanceledOnTouchOutside(boolean cancel) {
super.setCanceledOnTouchOutside(cancel);
mCancelable = cancel;
}

public void setOnOutsideClickListener(OnOutsideClickListener onOutsideClickListener) {
this.onOutsideClickListener = onOutsideClickListener;
}

public OutsideClickDialog(@NonNull Context context) {
super(context);
}

public OutsideClickDialog(@NonNull Context context, int themeResId) {
super(context, themeResId);
}

protected OutsideClickDialog(@NonNull Context context, boolean cancelable, @Nullable OnCancelListener cancelListener) {
super(context, cancelable, cancelListener);
}

@Override
public boolean onTouchEvent(@NonNull MotionEvent event) {
if (mCancelable && isShowing() &&
(event.getAction() == MotionEvent.ACTION_UP && isOutOfBounds(getContext(), event) ||
event.getAction() == MotionEvent.ACTION_OUTSIDE)) {
boolean consume = onOutsideClickListener.consumeOutsideClick();
if (consume) {
return true;
}
}

return super.onTouchEvent(event);
}

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 = getWindow().getDecorView();
return (x < -slop) || (y < -slop)
|| (x > (decorView.getWidth() + slop))
|| (y > (decorView.getHeight() + slop));
}
}

在DialogFragment子类SimpleDialogFragment内:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
//自定义Dialog,通过重写onTouchEvent拦截外部点击事件
OutsideClickDialog outsideClickDialog = new OutsideClickDialog(getContext(), getTheme());
outsideClickDialog.setOnOutsideClickListener(this::consumeOutsideClick);
//对返回按键做监听
outsideClickDialog.setOnKeyListener((dialog1, keyCode, event) -> {
if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == MotionEvent.ACTION_UP) {
if (mBuilder.onOutsideClickListener != null) {
//根据回调方法判断是否拦截
return mBuilder.onOutsideClickListener.consumeOutsideClick();
} else {
return false;
}
} else {
return false;
}
});
return outsideClickDialog;
}

private boolean consumeOutsideClick() {
if (mBuilder.onOutsideClickListener != null) {
//根据回调方法判断是否拦截外部点击事件
return mBuilder.onOutsideClickListener.consumeOutsideClick();
}

return !mBuilder.cancelable;
}

public static class Builder {
boolean cancelable = true;
private OnOutsideClickListener onOutsideClickListener;
private DialogInterface.OnDismissListener onDismissListener;


public Builder setCancelable(boolean cancelable) {
this.cancelable = cancelable;
return this;
}

public Builder setOutsideClickListener(OnOutsideClickListener onOutsideClickListener) {
this.onOutsideClickListener = onOutsideClickListener;
return this;
}

public Builder setOnDismissListener(DialogInterface.OnDismissListener onDismissListener) {
this.onDismissListener = onDismissListener;
return this;
}

public SimpleDialogFragment create() {
return SimpleDialogFragment.getInstance(this);
}

public SimpleDialogFragment show(FragmentManager manager, String tag) {
if (manager.findFragmentByTag(tag) != null) {
return (SimpleDialogFragment) manager.findFragmentByTag(tag);
}
SimpleDialogFragment dialog = create();
dialog.show(manager, tag);
return dialog;
}
}

其中Builder 为SimpleDialogFragment静态内部类。

并通过OnKeyListener监听返回按键。

具体使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
protected void bindEvent() {
super.bindEvent();
btnShowDialog.setOnClickListener(this::showDialog);
}

private void showDialog(View v) {
new SimpleDialogFragment.Builder()
.setOutsideClickListener(() -> {
Log.w(TAG, "showDialog outer click ");
Toast.makeText(activityInstance, "outer click", Toast.LENGTH_SHORT).show();
return true;
})
.setOnDismissListener(dialog -> {
Log.w(TAG, "showDialog dismiss");
})
.show(getSupportFragmentManager(), "simple_dialog_test");
}

完整代码

完整代码在该目录下

https://gitee.com/goldsea/learn/tree/master/app/src/main/java/com/nhtzj/learnapplication/activity/sample/dialog

坚持原创技术分享,您的支持是对我最大的鼓励!