/**
* 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.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
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 android.widget.FrameLayout;
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;
private boolean mHardwareAccelerated;
// 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;
}
protected void setHardwareAccelerated(boolean hardwareAccelerated) {
mHardwareAccelerated = hardwareAccelerated;
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(getContentView());
updateProperties();
mDialog.setOnShowListener(mOnShowListener);
mDialog.setOnKeyListener(
new DialogInterface.OnKeyListener() {
@Override
public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_UP) {
// 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) {
Assertions.assertNotNull(
mOnRequestCloseListener,
"setOnRequestCloseListener must be called by the manager");
mOnRequestCloseListener.onRequestClose(dialog);
return true;
} else {
// We redirect the rest of the key events to the current activity, since the activity
// expects to receive those events and react to them, ie. in the case of the dev menu
Activity currentActivity = ((ReactContext) getContext()).getCurrentActivity();
if (currentActivity != null) {
return currentActivity.onKeyUp(keyCode, event);
}
}
}
return false;
}
});
mDialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
if (mHardwareAccelerated) {
mDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
}
mDialog.show();
}
/**
* Returns the view that will be the root view of the dialog. We are wrapping this in a
* FrameLayout because this is the system's way of notifying us that the dialog size has changed.
* This has the pleasant side-effect of us not having to preface all Modals with
* "top: statusBarHeight", since that margin will be included in the FrameLayout.
*/
private View getContentView() {
FrameLayout frameLayout = new FrameLayout(getContext());
frameLayout.addView(mHostView);
frameLayout.setFitsSystemWindows(true);
return frameLayout;
}
/**
* 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");
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() {
((ReactContext) getContext()).getNativeModule(UIManagerModule.class)
.updateNodeSize(getChildAt(0).getId(), w, h);
}
});
}
}
@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();
}
}
}