-
Notifications
You must be signed in to change notification settings - Fork 24.3k
/
ReactModalHostView.java
320 lines (274 loc) · 11.3 KB
/
ReactModalHostView.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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.views.modal;
import javax.annotation.Nullable;
import java.util.ArrayList;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Point;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.R;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.uimanager.JSTouchDispatcher;
import com.facebook.react.uimanager.RootView;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.events.EventDispatcher;
import com.facebook.react.views.view.ReactViewGroup;
/**
* ReactModalHostView is a view that sits in the view hierarchy representing a Modal view.
*
* It does a number of things:
* 1. It creates a Dialog. We use this Dialog to actually display the Modal in the window.
* 2. It creates a DialogRootViewGroup. This view is the view that is displayed by the Dialog. To
* display a view within a Dialog, that view must have its parent set to the window the Dialog
* creates. Because of this, we can not use the ReactModalHostView since it sits in the
* normal React view hierarchy. We do however want all of the layout magic to happen as if the
* DialogRootViewGroup were part of the hierarchy. Therefore, we forward all view changes
* around addition and removal of views to the DialogRootViewGroup.
*/
public class ReactModalHostView extends ViewGroup implements LifecycleEventListener {
// This listener is called when the user presses KeyEvent.KEYCODE_BACK
// An event is then passed to JS which can either close or not close the Modal by setting the
// visible property
public interface OnRequestCloseListener {
void onRequestClose(DialogInterface dialog);
}
private DialogRootViewGroup mHostView;
private @Nullable Dialog mDialog;
private boolean mTransparent;
private String mAnimationType;
// Set this flag to true if changing a particular property on the view requires a new Dialog to
// be created. For instance, animation does since it affects Dialog creation through the theme
// but transparency does not since we can access the window to update the property.
private boolean mPropertyRequiresNewDialog;
private @Nullable DialogInterface.OnShowListener mOnShowListener;
private @Nullable OnRequestCloseListener mOnRequestCloseListener;
public ReactModalHostView(Context context) {
super(context);
((ReactContext) context).addLifecycleEventListener(this);
mHostView = new DialogRootViewGroup(context);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// Do nothing as we are laid out by UIManager
}
@Override
public void addView(View child, int index) {
mHostView.addView(child, index);
}
@Override
public int getChildCount() {
return mHostView.getChildCount();
}
@Override
public View getChildAt(int index) {
return mHostView.getChildAt(index);
}
@Override
public void removeView(View child) {
mHostView.removeView(child);
}
@Override
public void removeViewAt(int index) {
View child = getChildAt(index);
mHostView.removeView(child);
}
@Override
public void addChildrenForAccessibility(ArrayList<View> outChildren) {
// Explicitly override this to prevent accessibility events being passed down to children
// Those will be handled by the mHostView which lives in the dialog
}
@Override
public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
// Explicitly override this to prevent accessibility events being passed down to children
// Those will be handled by the mHostView which lives in the dialog
return false;
}
public void onDropInstance() {
((ReactContext) getContext()).removeLifecycleEventListener(this);
dismiss();
}
private void dismiss() {
if (mDialog != null) {
mDialog.dismiss();
mDialog = null;
// We need to remove the mHostView from the parent
// It is possible we are dismissing this dialog and reattaching the hostView to another
ViewGroup parent = (ViewGroup) mHostView.getParent();
parent.removeViewAt(0);
}
}
protected void setOnRequestCloseListener(OnRequestCloseListener listener) {
mOnRequestCloseListener = listener;
}
protected void setOnShowListener(DialogInterface.OnShowListener listener) {
mOnShowListener = listener;
}
protected void setTransparent(boolean transparent) {
mTransparent = transparent;
}
protected void setAnimationType(String animationType) {
mAnimationType = animationType;
mPropertyRequiresNewDialog = true;
}
@Override
public void onHostResume() {
// We show the dialog again when the host resumes
showOrUpdate();
}
@Override
public void onHostPause() {
// We dismiss the dialog and reconstitute it onHostResume
dismiss();
}
@Override
public void onHostDestroy() {
// Drop the instance if the host is destroyed which will dismiss the dialog
onDropInstance();
}
@VisibleForTesting
public @Nullable Dialog getDialog() {
return mDialog;
}
/**
* showOrUpdate will display the Dialog. It is called by the manager once all properties are set
* because we need to know all of them before creating the Dialog. It is also smart during
* updates if the changed properties can be applied directly to the Dialog or require the
* recreation of a new Dialog.
*/
protected void showOrUpdate() {
// If the existing Dialog is currently up, we may need to redraw it or we may be able to update
// the property without having to recreate the dialog
if (mDialog != null) {
if (mPropertyRequiresNewDialog) {
dismiss();
} else {
updateProperties();
return;
}
}
// Reset the flag since we are going to create a new dialog
mPropertyRequiresNewDialog = false;
int theme = R.style.Theme_FullScreenDialog;
if (mAnimationType.equals("fade")) {
theme = R.style.Theme_FullScreenDialogAnimatedFade;
} else if (mAnimationType.equals("slide")) {
theme = R.style.Theme_FullScreenDialogAnimatedSlide;
}
mDialog = new Dialog(getContext(), theme);
mDialog.setContentView(mHostView);
updateProperties();
mDialog.setOnShowListener(mOnShowListener);
mDialog.setOnKeyListener(
new DialogInterface.OnKeyListener() {
@Override
public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
// We need to stop the BACK button from closing the dialog by default so we capture that
// event and instead inform JS so that it can make the decision as to whether or not to
// allow the back button to close the dialog. If it chooses to, it can just set visible
// to false on the Modal and the Modal will go away
if (keyCode == KeyEvent.KEYCODE_BACK) {
if (event.getAction() == KeyEvent.ACTION_UP) {
Assertions.assertNotNull(
mOnRequestCloseListener,
"setOnRequestCloseListener must be called by the manager");
mOnRequestCloseListener.onRequestClose(dialog);
}
return true;
}
return false;
}
});
mDialog.show();
}
/**
* updateProperties will update the properties that do not require us to recreate the dialog
* Properties that do require us to recreate the dialog should set mPropertyRequiresNewDialog to
* true when the property changes
*/
private void updateProperties() {
Assertions.assertNotNull(mDialog, "mDialog must exist when we call updateProperties");
mDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
if (mTransparent) {
mDialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
} else {
mDialog.getWindow().setDimAmount(0.5f);
mDialog.getWindow().setFlags(
WindowManager.LayoutParams.FLAG_DIM_BEHIND,
WindowManager.LayoutParams.FLAG_DIM_BEHIND);
}
}
/**
* DialogRootViewGroup is the ViewGroup which contains all the children of a Modal. It gets all
* child information forwarded from ReactModalHostView and uses that to create children. It is
* also responsible for acting as a RootView and handling touch events. It does this the same
* way as ReactRootView.
*
* To get layout to work properly, we need to layout all the elements within the Modal as if they
* can fill the entire window. To do that, we need to explicitly set the styleWidth and
* styleHeight on the LayoutShadowNode to be the window size. This is done through the
* UIManagerModule, and will then cause the children to layout as if they can fill the window.
*/
static class DialogRootViewGroup extends ReactViewGroup implements RootView {
private final JSTouchDispatcher mJSTouchDispatcher = new JSTouchDispatcher(this);
public DialogRootViewGroup(Context context) {
super(context);
}
@Override
protected void onSizeChanged(final int w, final int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (getChildCount() > 0) {
((ReactContext) getContext()).runOnNativeModulesQueueThread(
new Runnable() {
@Override
public void run() {
Point modalSize = ModalHostHelper.getModalHostSize(getContext());
((ReactContext) getContext()).getNativeModule(UIManagerModule.class)
.updateNodeSize(getChildAt(0).getId(), modalSize.x, modalSize.y);
}
});
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
mJSTouchDispatcher.handleTouchEvent(event, getEventDispatcher());
return super.onInterceptTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mJSTouchDispatcher.handleTouchEvent(event, getEventDispatcher());
super.onTouchEvent(event);
// In case when there is no children interested in handling touch event, we return true from
// the root view in order to receive subsequent events related to that gesture
return true;
}
@Override
public void onChildStartedNativeGesture(MotionEvent androidEvent) {
mJSTouchDispatcher.onChildStartedNativeGesture(androidEvent, getEventDispatcher());
}
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
// No-op - override in order to still receive events to onInterceptTouchEvent
// even when some other view disallow that
}
private EventDispatcher getEventDispatcher() {
ReactContext reactContext = (ReactContext) getContext();
return reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher();
}
}
}