mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Convert ReactModalHostView to Kotlin (#43845)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/43845 This class is quite legacy and requires a couple of eyes before merging it. I've tested that modals are working fine on RN-Tester both Old & New Arch. Changelog: [Internal] [Changed] - ReactModalHostView to Kotlin Reviewed By: javache Differential Revision: D55739128 fbshipit-source-id: 740d24df39ceb7b6a8120ae5315cf0684d1e6e27
This commit is contained in:
committed by
Facebook GitHub Bot
parent
20c1cc5e65
commit
ab44550f6e
@@ -6255,32 +6255,52 @@ public class com/facebook/react/views/modal/ReactModalHostManager$$PropsSetter :
|
||||
public fun setProperty (Lcom/facebook/react/views/modal/ReactModalHostManager;Lcom/facebook/react/views/modal/ReactModalHostView;Ljava/lang/String;Ljava/lang/Object;)V
|
||||
}
|
||||
|
||||
public class com/facebook/react/views/modal/ReactModalHostView : android/view/ViewGroup, com/facebook/react/bridge/LifecycleEventListener {
|
||||
public final class com/facebook/react/views/modal/ReactModalHostView : android/view/ViewGroup, com/facebook/react/bridge/LifecycleEventListener {
|
||||
public fun <init> (Lcom/facebook/react/uimanager/ThemedReactContext;)V
|
||||
public fun addChildrenForAccessibility (Ljava/util/ArrayList;)V
|
||||
public fun addView (Landroid/view/View;I)V
|
||||
public fun dispatchPopulateAccessibilityEvent (Landroid/view/accessibility/AccessibilityEvent;)Z
|
||||
public fun dispatchProvideStructure (Landroid/view/ViewStructure;)V
|
||||
public final fun getAnimationType ()Ljava/lang/String;
|
||||
public fun getChildAt (I)Landroid/view/View;
|
||||
public fun getChildCount ()I
|
||||
public fun getStateWrapper ()Lcom/facebook/react/uimanager/StateWrapper;
|
||||
protected fun onDetachedFromWindow ()V
|
||||
public fun onDropInstance ()V
|
||||
public final fun getEventDispatcher ()Lcom/facebook/react/uimanager/events/EventDispatcher;
|
||||
public final fun getHardwareAccelerated ()Z
|
||||
public final fun getOnRequestCloseListener ()Lcom/facebook/react/views/modal/ReactModalHostView$OnRequestCloseListener;
|
||||
public final fun getOnShowListener ()Landroid/content/DialogInterface$OnShowListener;
|
||||
public final fun getStateWrapper ()Lcom/facebook/react/uimanager/StateWrapper;
|
||||
public final fun getStatusBarTranslucent ()Z
|
||||
public final fun getTransparent ()Z
|
||||
public final fun onDropInstance ()V
|
||||
public fun onHostDestroy ()V
|
||||
public fun onHostPause ()V
|
||||
public fun onHostResume ()V
|
||||
protected fun onLayout (ZIIII)V
|
||||
public fun removeView (Landroid/view/View;)V
|
||||
public fun removeViewAt (I)V
|
||||
protected fun setAnimationType (Ljava/lang/String;)V
|
||||
protected fun setHardwareAccelerated (Z)V
|
||||
protected fun setOnRequestCloseListener (Lcom/facebook/react/views/modal/ReactModalHostView$OnRequestCloseListener;)V
|
||||
protected fun setOnShowListener (Landroid/content/DialogInterface$OnShowListener;)V
|
||||
public fun setStateWrapper (Lcom/facebook/react/uimanager/StateWrapper;)V
|
||||
protected fun setStatusBarTranslucent (Z)V
|
||||
protected fun setTransparent (Z)V
|
||||
protected fun showOrUpdate ()V
|
||||
public fun updateState (II)V
|
||||
public final fun setAnimationType (Ljava/lang/String;)V
|
||||
public final fun setEventDispatcher (Lcom/facebook/react/uimanager/events/EventDispatcher;)V
|
||||
public final fun setHardwareAccelerated (Z)V
|
||||
public final fun setOnRequestCloseListener (Lcom/facebook/react/views/modal/ReactModalHostView$OnRequestCloseListener;)V
|
||||
public final fun setOnShowListener (Landroid/content/DialogInterface$OnShowListener;)V
|
||||
public final fun setStateWrapper (Lcom/facebook/react/uimanager/StateWrapper;)V
|
||||
public final fun setStatusBarTranslucent (Z)V
|
||||
public final fun setTransparent (Z)V
|
||||
public final fun showOrUpdate ()V
|
||||
public final fun updateState (II)V
|
||||
}
|
||||
|
||||
public final class com/facebook/react/views/modal/ReactModalHostView$DialogRootViewGroup : com/facebook/react/views/view/ReactViewGroup, com/facebook/react/uimanager/RootView {
|
||||
public fun <init> (Lcom/facebook/react/views/modal/ReactModalHostView;Landroid/content/Context;)V
|
||||
public fun addView (Landroid/view/View;ILandroid/view/ViewGroup$LayoutParams;)V
|
||||
public fun handleException (Ljava/lang/Throwable;)V
|
||||
public fun onChildEndedNativeGesture (Landroid/view/View;Landroid/view/MotionEvent;)V
|
||||
public fun onChildStartedNativeGesture (Landroid/view/View;Landroid/view/MotionEvent;)V
|
||||
public fun onHoverEvent (Landroid/view/MotionEvent;)Z
|
||||
public fun onInterceptHoverEvent (Landroid/view/MotionEvent;)Z
|
||||
public fun onInterceptTouchEvent (Landroid/view/MotionEvent;)Z
|
||||
public fun onTouchEvent (Landroid/view/MotionEvent;)Z
|
||||
public fun requestDisallowInterceptTouchEvent (Z)V
|
||||
public final fun updateState (II)V
|
||||
}
|
||||
|
||||
public abstract interface class com/facebook/react/views/modal/ReactModalHostView$OnRequestCloseListener {
|
||||
|
||||
-629
@@ -1,629 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
package com.facebook.react.views.modal;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Build;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewStructure;
|
||||
import android.view.Window;
|
||||
import android.view.WindowInsetsController;
|
||||
import android.view.WindowManager;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.widget.FrameLayout;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.UiThread;
|
||||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.infer.annotation.Assertions;
|
||||
import com.facebook.infer.annotation.Nullsafe;
|
||||
import com.facebook.react.R;
|
||||
import com.facebook.react.bridge.GuardedRunnable;
|
||||
import com.facebook.react.bridge.LifecycleEventListener;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.UiThreadUtil;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.bridge.WritableNativeMap;
|
||||
import com.facebook.react.common.annotations.VisibleForTesting;
|
||||
import com.facebook.react.config.ReactFeatureFlags;
|
||||
import com.facebook.react.uimanager.JSPointerDispatcher;
|
||||
import com.facebook.react.uimanager.JSTouchDispatcher;
|
||||
import com.facebook.react.uimanager.PixelUtil;
|
||||
import com.facebook.react.uimanager.RootView;
|
||||
import com.facebook.react.uimanager.StateWrapper;
|
||||
import com.facebook.react.uimanager.ThemedReactContext;
|
||||
import com.facebook.react.uimanager.UIManagerModule;
|
||||
import com.facebook.react.uimanager.events.EventDispatcher;
|
||||
import com.facebook.react.views.common.ContextUtils;
|
||||
import com.facebook.react.views.view.ReactViewGroup;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* ReactModalHostView is a view that sits in the view hierarchy representing a Modal view.
|
||||
*
|
||||
* <p>It does a number of things:
|
||||
*
|
||||
* <ol>
|
||||
* <li>It creates a Dialog. We use this Dialog to actually display the Modal in the window.
|
||||
* <li>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.
|
||||
* </ol>
|
||||
*/
|
||||
@Nullsafe(Nullsafe.Mode.LOCAL)
|
||||
public class ReactModalHostView extends ViewGroup implements LifecycleEventListener {
|
||||
|
||||
private static final String TAG = "ReactModalHost";
|
||||
|
||||
// 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 boolean mStatusBarTranslucent;
|
||||
private @Nullable 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(ThemedReactContext context) {
|
||||
super(context);
|
||||
context.addLifecycleEventListener(this);
|
||||
|
||||
mHostView = new DialogRootViewGroup(context);
|
||||
}
|
||||
|
||||
@TargetApi(23)
|
||||
@Override
|
||||
public void dispatchProvideStructure(ViewStructure structure) {
|
||||
mHostView.dispatchProvideStructure(structure);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
||||
// Do nothing as we are laid out by UIManager
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
dismiss();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addView(@Nullable View child, int index) {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
|
||||
mHostView.addView(child, index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildCount() {
|
||||
// This method may be called by the parent constructor
|
||||
// before mHostView is initialized.
|
||||
return mHostView == null ? 0 : mHostView.getChildCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getChildAt(int index) {
|
||||
return mHostView.getChildAt(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeView(@Nullable View child) {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
|
||||
if (child != null) {
|
||||
mHostView.removeView(child);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeViewAt(int index) {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
|
||||
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() {
|
||||
((ThemedReactContext) getContext()).removeLifecycleEventListener(this);
|
||||
dismiss();
|
||||
}
|
||||
|
||||
private void dismiss() {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
|
||||
if (mDialog != null) {
|
||||
if (mDialog.isShowing()) {
|
||||
Activity dialogContext =
|
||||
ContextUtils.findContextOfType(mDialog.getContext(), Activity.class);
|
||||
if (dialogContext == null || !dialogContext.isFinishing()) {
|
||||
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();
|
||||
if (parent != null) {
|
||||
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 setStatusBarTranslucent(boolean statusBarTranslucent) {
|
||||
mStatusBarTranslucent = statusBarTranslucent;
|
||||
mPropertyRequiresNewDialog = true;
|
||||
}
|
||||
|
||||
protected void setAnimationType(String animationType) {
|
||||
mAnimationType = animationType;
|
||||
mPropertyRequiresNewDialog = true;
|
||||
}
|
||||
|
||||
protected void setHardwareAccelerated(boolean hardwareAccelerated) {
|
||||
mHardwareAccelerated = hardwareAccelerated;
|
||||
mPropertyRequiresNewDialog = true;
|
||||
}
|
||||
|
||||
void setEventDispatcher(EventDispatcher eventDispatcher) {
|
||||
mHostView.setEventDispatcher(eventDispatcher);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHostResume() {
|
||||
// We show the dialog again when the host resumes
|
||||
showOrUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHostPause() {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
private @Nullable Activity getCurrentActivity() {
|
||||
return ((ThemedReactContext) getContext()).getCurrentActivity();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
|
||||
// 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) {
|
||||
Context dialogContext = ContextUtils.findContextOfType(mDialog.getContext(), Activity.class);
|
||||
// TODO(T85755791): remove after investigation
|
||||
FLog.e(TAG, "Updating existing dialog with context: " + dialogContext);
|
||||
|
||||
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 != null && mAnimationType.equals("fade")) {
|
||||
theme = R.style.Theme_FullScreenDialogAnimatedFade;
|
||||
} else if (mAnimationType != null && mAnimationType.equals("slide")) {
|
||||
theme = R.style.Theme_FullScreenDialogAnimatedSlide;
|
||||
}
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
Context context = currentActivity == null ? getContext() : currentActivity;
|
||||
mDialog = new Dialog(context, theme);
|
||||
Objects.requireNonNull(mDialog.getWindow())
|
||||
.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
|
||||
|
||||
// TODO(T85755791): remove after investigation
|
||||
FLog.e(TAG, "Creating new dialog from context: " + context + "@" + context.hashCode());
|
||||
|
||||
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 and ESCAPE key 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/escape key 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 || keyCode == KeyEvent.KEYCODE_ESCAPE) {
|
||||
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);
|
||||
}
|
||||
if (currentActivity != null && !currentActivity.isFinishing()) {
|
||||
mDialog.show();
|
||||
updateSystemAppearance();
|
||||
mDialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
if (mStatusBarTranslucent) {
|
||||
frameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
|
||||
} else {
|
||||
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");
|
||||
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
|
||||
Window window = Assertions.assertNotNull(mDialog.getWindow());
|
||||
if (currentActivity == null || currentActivity.isFinishing() || !window.isActive()) {
|
||||
// If the activity has disappeared, then we shouldn't update the window associated to the
|
||||
// Dialog.
|
||||
return;
|
||||
}
|
||||
Window activityWindow = currentActivity.getWindow();
|
||||
if (activityWindow != null) {
|
||||
int activityWindowFlags = activityWindow.getAttributes().flags;
|
||||
if ((activityWindowFlags & WindowManager.LayoutParams.FLAG_FULLSCREEN) != 0) {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
} else {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
}
|
||||
}
|
||||
|
||||
if (mTransparent) {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
|
||||
} else {
|
||||
window.setDimAmount(0.5f);
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_DIM_BEHIND, WindowManager.LayoutParams.FLAG_DIM_BEHIND);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSystemAppearance() {
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
if (currentActivity == null) {
|
||||
return;
|
||||
}
|
||||
Assertions.assertNotNull(mDialog, "mDialog must exist when we call updateSystemAppearance");
|
||||
Window window = Assertions.assertNotNull(mDialog.getWindow(), "mDialog.getWindow() must exist");
|
||||
// Modeled after the version check in StatusBarModule.setStyle
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {
|
||||
Window currentActivityWindow = Assertions.assertNotNull(currentActivity.getWindow());
|
||||
WindowInsetsController insetsController =
|
||||
Assertions.assertNotNull(currentActivityWindow.getInsetsController());
|
||||
int activityAppearance = insetsController.getSystemBarsAppearance();
|
||||
|
||||
int activityLightStatusBars =
|
||||
activityAppearance & WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
|
||||
|
||||
Objects.requireNonNull(window.getInsetsController())
|
||||
.setSystemBarsAppearance(
|
||||
activityLightStatusBars, WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS);
|
||||
} else {
|
||||
Window currentActivityWindow = Assertions.assertNotNull(currentActivity.getWindow());
|
||||
View decorView = currentActivityWindow.getDecorView();
|
||||
decorView.setSystemUiVisibility(decorView.getSystemUiVisibility());
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public StateWrapper getStateWrapper() {
|
||||
return mHostView.getStateWrapper();
|
||||
}
|
||||
|
||||
public void setStateWrapper(StateWrapper stateWrapper) {
|
||||
mHostView.setStateWrapper(stateWrapper);
|
||||
}
|
||||
|
||||
public void updateState(final int width, final int height) {
|
||||
mHostView.updateState(width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* <p>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 boolean hasAdjustedSize = false;
|
||||
private int viewWidth;
|
||||
private int viewHeight;
|
||||
private @Nullable EventDispatcher mEventDispatcher;
|
||||
|
||||
private @Nullable StateWrapper mStateWrapper = null;
|
||||
|
||||
private final JSTouchDispatcher mJSTouchDispatcher = new JSTouchDispatcher(this);
|
||||
@Nullable private JSPointerDispatcher mJSPointerDispatcher;
|
||||
|
||||
public DialogRootViewGroup(Context context) {
|
||||
super(context);
|
||||
if (ReactFeatureFlags.dispatchPointerEvents) {
|
||||
mJSPointerDispatcher = new JSPointerDispatcher(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void setEventDispatcher(EventDispatcher eventDispatcher) {
|
||||
mEventDispatcher = eventDispatcher;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(final int w, final int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
viewWidth = w;
|
||||
viewHeight = h;
|
||||
updateFirstChildView();
|
||||
}
|
||||
|
||||
private void updateFirstChildView() {
|
||||
if (getChildCount() > 0) {
|
||||
hasAdjustedSize = false;
|
||||
final int viewTag = getChildAt(0).getId();
|
||||
if (mStateWrapper != null) {
|
||||
// This will only be called under Fabric
|
||||
updateState(viewWidth, viewHeight);
|
||||
} else {
|
||||
// TODO: T44725185 remove after full migration to Fabric
|
||||
ReactContext reactContext = getReactContext();
|
||||
reactContext.runOnNativeModulesQueueThread(
|
||||
new GuardedRunnable(reactContext) {
|
||||
@Override
|
||||
public void runGuarded() {
|
||||
UIManagerModule uiManager =
|
||||
getReactContext()
|
||||
.getReactApplicationContext()
|
||||
.getNativeModule(UIManagerModule.class);
|
||||
|
||||
if (uiManager == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
uiManager.updateNodeSize(viewTag, viewWidth, viewHeight);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
hasAdjustedSize = true;
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public void updateState(final int width, final int height) {
|
||||
final float realWidth = PixelUtil.toDIPFromPixel(width);
|
||||
final float realHeight = PixelUtil.toDIPFromPixel(height);
|
||||
|
||||
// Check incoming state values. If they're already the correct value, return early to prevent
|
||||
// infinite UpdateState/SetState loop.
|
||||
ReadableMap currentState = mStateWrapper != null ? mStateWrapper.getStateData() : null;
|
||||
if (currentState != null) {
|
||||
float delta = (float) 0.9;
|
||||
float stateScreenHeight =
|
||||
currentState.hasKey("screenHeight")
|
||||
? (float) currentState.getDouble("screenHeight")
|
||||
: 0;
|
||||
float stateScreenWidth =
|
||||
currentState.hasKey("screenWidth") ? (float) currentState.getDouble("screenWidth") : 0;
|
||||
|
||||
if (Math.abs(stateScreenWidth - realWidth) < delta
|
||||
&& Math.abs(stateScreenHeight - realHeight) < delta) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (mStateWrapper != null) {
|
||||
WritableMap newStateData = new WritableNativeMap();
|
||||
newStateData.putDouble("screenWidth", realWidth);
|
||||
newStateData.putDouble("screenHeight", realHeight);
|
||||
mStateWrapper.updateState(newStateData);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addView(View child, int index, LayoutParams params) {
|
||||
super.addView(child, index, params);
|
||||
if (hasAdjustedSize) {
|
||||
updateFirstChildView();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleException(Throwable t) {
|
||||
getReactContext().getReactApplicationContext().handleException(new RuntimeException(t));
|
||||
}
|
||||
|
||||
private ThemedReactContext getReactContext() {
|
||||
return (ThemedReactContext) getContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(MotionEvent event) {
|
||||
if (mEventDispatcher != null) {
|
||||
mJSTouchDispatcher.handleTouchEvent(event, mEventDispatcher);
|
||||
}
|
||||
if (mJSPointerDispatcher != null && mEventDispatcher != null) {
|
||||
mJSPointerDispatcher.handleMotionEvent(event, mEventDispatcher, true);
|
||||
}
|
||||
return super.onInterceptTouchEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
if (mEventDispatcher != null) {
|
||||
mJSTouchDispatcher.handleTouchEvent(event, mEventDispatcher);
|
||||
}
|
||||
if (mJSPointerDispatcher != null && mEventDispatcher != null) {
|
||||
mJSPointerDispatcher.handleMotionEvent(event, mEventDispatcher, false);
|
||||
}
|
||||
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 boolean onInterceptHoverEvent(MotionEvent event) {
|
||||
if (mJSPointerDispatcher != null && mEventDispatcher != null) {
|
||||
mJSPointerDispatcher.handleMotionEvent(event, mEventDispatcher, true);
|
||||
}
|
||||
return super.onHoverEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onHoverEvent(MotionEvent event) {
|
||||
if (mJSPointerDispatcher != null && mEventDispatcher != null) {
|
||||
mJSPointerDispatcher.handleMotionEvent(event, mEventDispatcher, false);
|
||||
}
|
||||
return super.onHoverEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildStartedNativeGesture(View childView, MotionEvent ev) {
|
||||
if (mEventDispatcher != null) {
|
||||
mJSTouchDispatcher.onChildStartedNativeGesture(ev, mEventDispatcher);
|
||||
}
|
||||
if (mJSPointerDispatcher != null && mEventDispatcher != null) {
|
||||
mJSPointerDispatcher.onChildStartedNativeGesture(childView, ev, mEventDispatcher);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildEndedNativeGesture(View childView, MotionEvent ev) {
|
||||
if (mEventDispatcher != null) {
|
||||
mJSTouchDispatcher.onChildEndedNativeGesture(ev, mEventDispatcher);
|
||||
}
|
||||
if (mJSPointerDispatcher != null) {
|
||||
mJSPointerDispatcher.onChildEndedNativeGesture();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
|
||||
// No-op - override in order to still receive events to onInterceptTouchEvent
|
||||
// even when some other view disallow that
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public StateWrapper getStateWrapper() {
|
||||
return mStateWrapper;
|
||||
}
|
||||
|
||||
public void setStateWrapper(StateWrapper stateWrapper) {
|
||||
mStateWrapper = stateWrapper;
|
||||
}
|
||||
}
|
||||
}
|
||||
+540
@@ -0,0 +1,540 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.facebook.react.views.modal
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.os.Build
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewStructure
|
||||
import android.view.Window
|
||||
import android.view.WindowInsetsController
|
||||
import android.view.WindowManager
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import android.widget.FrameLayout
|
||||
import androidx.annotation.UiThread
|
||||
import com.facebook.common.logging.FLog
|
||||
import com.facebook.react.R
|
||||
import com.facebook.react.bridge.GuardedRunnable
|
||||
import com.facebook.react.bridge.LifecycleEventListener
|
||||
import com.facebook.react.bridge.ReactContext
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.facebook.react.bridge.UiThreadUtil
|
||||
import com.facebook.react.bridge.WritableMap
|
||||
import com.facebook.react.bridge.WritableNativeMap
|
||||
import com.facebook.react.common.annotations.VisibleForTesting
|
||||
import com.facebook.react.config.ReactFeatureFlags
|
||||
import com.facebook.react.uimanager.JSPointerDispatcher
|
||||
import com.facebook.react.uimanager.JSTouchDispatcher
|
||||
import com.facebook.react.uimanager.PixelUtil
|
||||
import com.facebook.react.uimanager.RootView
|
||||
import com.facebook.react.uimanager.StateWrapper
|
||||
import com.facebook.react.uimanager.ThemedReactContext
|
||||
import com.facebook.react.uimanager.UIManagerModule
|
||||
import com.facebook.react.uimanager.events.EventDispatcher
|
||||
import com.facebook.react.views.common.ContextUtils
|
||||
import com.facebook.react.views.view.ReactViewGroup
|
||||
import java.util.Objects
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@SuppressLint("ViewConstructor")
|
||||
public class ReactModalHostView(context: ThemedReactContext) :
|
||||
ViewGroup(context), LifecycleEventListener {
|
||||
|
||||
@get:VisibleForTesting
|
||||
public var dialog: Dialog? = null
|
||||
private set
|
||||
|
||||
public var transparent: Boolean = false
|
||||
public var onShowListener: DialogInterface.OnShowListener? = null
|
||||
public var onRequestCloseListener: OnRequestCloseListener? = null
|
||||
public var statusBarTranslucent: Boolean = false
|
||||
set(value) {
|
||||
field = value
|
||||
propertyRequiresNewDialog = true
|
||||
}
|
||||
|
||||
public var animationType: String? = null
|
||||
set(value) {
|
||||
field = value
|
||||
propertyRequiresNewDialog = true
|
||||
}
|
||||
|
||||
public var hardwareAccelerated: Boolean = false
|
||||
set(value) {
|
||||
field = value
|
||||
propertyRequiresNewDialog = true
|
||||
}
|
||||
|
||||
public var stateWrapper: StateWrapper?
|
||||
get() = hostView.stateWrapper
|
||||
public set(stateWrapper) {
|
||||
hostView.stateWrapper = stateWrapper
|
||||
}
|
||||
|
||||
public var eventDispatcher: EventDispatcher?
|
||||
get() = hostView.eventDispatcher
|
||||
public set(eventDispatcher) {
|
||||
hostView.eventDispatcher = eventDispatcher
|
||||
}
|
||||
|
||||
private var hostView: DialogRootViewGroup
|
||||
|
||||
// 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 var propertyRequiresNewDialog = false
|
||||
|
||||
init {
|
||||
context.addLifecycleEventListener(this)
|
||||
hostView = DialogRootViewGroup(context)
|
||||
}
|
||||
|
||||
public override fun dispatchProvideStructure(structure: ViewStructure) {
|
||||
hostView.dispatchProvideStructure(structure)
|
||||
}
|
||||
|
||||
protected override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
|
||||
// Do nothing as we are laid out by UIManager
|
||||
}
|
||||
|
||||
protected override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
dismiss()
|
||||
}
|
||||
|
||||
public override fun addView(child: View?, index: Int) {
|
||||
UiThreadUtil.assertOnUiThread()
|
||||
hostView.addView(child, index)
|
||||
}
|
||||
|
||||
public override fun getChildCount(): Int = hostView.childCount
|
||||
|
||||
public override fun getChildAt(index: Int): View? = hostView.getChildAt(index)
|
||||
|
||||
public override fun removeView(child: View?) {
|
||||
UiThreadUtil.assertOnUiThread()
|
||||
|
||||
if (child != null) {
|
||||
hostView.removeView(child)
|
||||
}
|
||||
}
|
||||
|
||||
public override fun removeViewAt(index: Int) {
|
||||
UiThreadUtil.assertOnUiThread()
|
||||
val child = getChildAt(index)
|
||||
hostView.removeView(child)
|
||||
}
|
||||
|
||||
public override fun addChildrenForAccessibility(outChildren: ArrayList<View>) {
|
||||
// Explicitly override this to prevent accessibility events being passed down to children
|
||||
// Those will be handled by the mHostView which lives in the dialog
|
||||
}
|
||||
|
||||
// Explicitly override this to prevent accessibility events being passed down to children
|
||||
// Those will be handled by the mHostView which lives in the dialog
|
||||
public override fun dispatchPopulateAccessibilityEvent(event: AccessibilityEvent): Boolean = false
|
||||
|
||||
public fun onDropInstance() {
|
||||
(context as ThemedReactContext).removeLifecycleEventListener(this)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
private fun dismiss() {
|
||||
UiThreadUtil.assertOnUiThread()
|
||||
|
||||
dialog?.let { nonNullDialog ->
|
||||
if (nonNullDialog.isShowing) {
|
||||
val dialogContext =
|
||||
ContextUtils.findContextOfType(nonNullDialog.context, Activity::class.java)
|
||||
if (dialogContext == null || !dialogContext.isFinishing) {
|
||||
nonNullDialog.dismiss()
|
||||
}
|
||||
}
|
||||
dialog = null
|
||||
|
||||
// We need to remove the mHostView from the parent
|
||||
// It is possible we are dismissing this dialog and reattaching the hostView to another
|
||||
(hostView.parent as? ViewGroup)?.removeViewAt(0)
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onHostResume() {
|
||||
// We show the dialog again when the host resumes
|
||||
showOrUpdate()
|
||||
}
|
||||
|
||||
public override fun onHostPause() {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
public override fun onHostDestroy() {
|
||||
// Drop the instance if the host is destroyed which will dismiss the dialog
|
||||
onDropInstance()
|
||||
}
|
||||
|
||||
private fun getCurrentActivity(): Activity? = (context as ThemedReactContext).currentActivity
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public fun showOrUpdate() {
|
||||
UiThreadUtil.assertOnUiThread()
|
||||
|
||||
// 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
|
||||
dialog?.let { nonNullDialog ->
|
||||
val dialogContext: Context? =
|
||||
ContextUtils.findContextOfType(nonNullDialog.context, Activity::class.java)
|
||||
// TODO(T85755791): remove after investigation
|
||||
FLog.e(TAG, "Updating existing dialog with context: $dialogContext")
|
||||
|
||||
if (propertyRequiresNewDialog) {
|
||||
dismiss()
|
||||
} else {
|
||||
updateProperties()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Reset the flag since we are going to create a new dialog
|
||||
propertyRequiresNewDialog = false
|
||||
val theme: Int =
|
||||
when (animationType) {
|
||||
"fade" -> R.style.Theme_FullScreenDialogAnimatedFade
|
||||
"slide" -> R.style.Theme_FullScreenDialogAnimatedSlide
|
||||
else -> R.style.Theme_FullScreenDialog
|
||||
}
|
||||
|
||||
val currentActivity = getCurrentActivity()
|
||||
val context = currentActivity ?: context
|
||||
val newDialog = Dialog(context, theme)
|
||||
dialog = newDialog
|
||||
Objects.requireNonNull<Window>(newDialog.window)
|
||||
.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)
|
||||
|
||||
// TODO(T85755791): remove after investigation
|
||||
FLog.e(TAG, "Creating new dialog from context: " + context + "@" + context.hashCode())
|
||||
|
||||
newDialog.setContentView(contentView)
|
||||
updateProperties()
|
||||
|
||||
newDialog.setOnShowListener(onShowListener)
|
||||
newDialog.setOnKeyListener(
|
||||
object : DialogInterface.OnKeyListener {
|
||||
override fun onKey(dialog: DialogInterface, keyCode: Int, event: KeyEvent): Boolean {
|
||||
if (event.action == KeyEvent.ACTION_UP) {
|
||||
// We need to stop the BACK button and ESCAPE key 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/escape key 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 || keyCode == KeyEvent.KEYCODE_ESCAPE) {
|
||||
val listener =
|
||||
checkNotNull(onRequestCloseListener) {
|
||||
"onRequestClose callback must be set if back key is expected to close the modal"
|
||||
}
|
||||
listener.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
|
||||
val innerCurrentActivity = (context as ReactContext).currentActivity
|
||||
if (innerCurrentActivity != null) {
|
||||
return innerCurrentActivity.onKeyUp(keyCode, event)
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
newDialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
|
||||
if (hardwareAccelerated) {
|
||||
newDialog.window?.addFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED)
|
||||
}
|
||||
if (currentActivity?.isFinishing == false) {
|
||||
newDialog.show()
|
||||
updateSystemAppearance()
|
||||
newDialog.window?.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)
|
||||
}
|
||||
}
|
||||
|
||||
private val contentView: View
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
get() {
|
||||
val frameLayout = FrameLayout(context)
|
||||
frameLayout.addView(hostView)
|
||||
if (statusBarTranslucent) {
|
||||
frameLayout.systemUiVisibility = SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
} else {
|
||||
frameLayout.fitsSystemWindows = 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 fun updateProperties() {
|
||||
val dialog = checkNotNull(dialog) { "dialog must exist when we call updateProperties" }
|
||||
val currentActivity = getCurrentActivity()
|
||||
val window =
|
||||
checkNotNull(dialog.window) { "dialog must have window when we call updateProperties" }
|
||||
if (currentActivity == null || currentActivity.isFinishing || !window.isActive) {
|
||||
// If the activity has disappeared, then we shouldn't update the window associated to the
|
||||
// Dialog.
|
||||
return
|
||||
}
|
||||
val activityWindow = currentActivity.window
|
||||
if (activityWindow != null) {
|
||||
val activityWindowFlags = activityWindow.attributes.flags
|
||||
if ((activityWindowFlags and WindowManager.LayoutParams.FLAG_FULLSCREEN) != 0) {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||
} else {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||
}
|
||||
}
|
||||
|
||||
if (transparent) {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
||||
} else {
|
||||
window.setDimAmount(0.5f)
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_DIM_BEHIND, WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSystemAppearance() {
|
||||
val currentActivity = getCurrentActivity() ?: return
|
||||
val dialog = checkNotNull(dialog) { "dialog must exist when we call updateProperties" }
|
||||
val window =
|
||||
checkNotNull(dialog.window) { "dialog must have window when we call updateProperties" }
|
||||
// Modeled after the version check in StatusBarModule.setStyle
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {
|
||||
val currentActivityWindow = currentActivity.window
|
||||
val insetsController: WindowInsetsController =
|
||||
checkNotNull(currentActivityWindow.insetsController)
|
||||
val activityAppearance: Int = insetsController.systemBarsAppearance
|
||||
|
||||
val activityLightStatusBars =
|
||||
activityAppearance and WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
|
||||
|
||||
window.insetsController?.setSystemBarsAppearance(
|
||||
activityLightStatusBars, WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS)
|
||||
} else {
|
||||
val currentActivityWindow = checkNotNull(currentActivity.window)
|
||||
val decorView = currentActivityWindow.decorView
|
||||
decorView.setSystemUiVisibility(decorView.systemUiVisibility)
|
||||
}
|
||||
}
|
||||
|
||||
public fun updateState(width: Int, height: Int) {
|
||||
hostView.updateState(width, height)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
public fun onRequestClose(dialog: DialogInterface?)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val TAG = "ReactModalHost"
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public inner class DialogRootViewGroup(context: Context?) : ReactViewGroup(context), RootView {
|
||||
internal var stateWrapper: StateWrapper? = null
|
||||
|
||||
private var hasAdjustedSize = false
|
||||
private var viewWidth = 0
|
||||
private var viewHeight = 0
|
||||
private val jSTouchDispatcher: JSTouchDispatcher = JSTouchDispatcher(this)
|
||||
private var jSPointerDispatcher: JSPointerDispatcher? = null
|
||||
internal var eventDispatcher: EventDispatcher? = null
|
||||
|
||||
private val reactContext: ThemedReactContext
|
||||
get() = context as ThemedReactContext
|
||||
|
||||
init {
|
||||
if (ReactFeatureFlags.dispatchPointerEvents) {
|
||||
jSPointerDispatcher = JSPointerDispatcher(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
viewWidth = w
|
||||
viewHeight = h
|
||||
updateFirstChildView()
|
||||
}
|
||||
|
||||
private fun updateFirstChildView() {
|
||||
if (childCount > 0) {
|
||||
hasAdjustedSize = false
|
||||
val viewTag: Int = getChildAt(0).id
|
||||
if (stateWrapper != null) {
|
||||
// This will only be called under Fabric
|
||||
updateState(viewWidth, viewHeight)
|
||||
} else {
|
||||
// TODO: T44725185 remove after full migration to Fabric
|
||||
val reactContext: ReactContext = reactContext
|
||||
reactContext.runOnNativeModulesQueueThread(
|
||||
object : GuardedRunnable(reactContext) {
|
||||
override fun runGuarded() {
|
||||
this@DialogRootViewGroup.reactContext.reactApplicationContext
|
||||
.getNativeModule(UIManagerModule::class.java)
|
||||
?.updateNodeSize(viewTag, viewWidth, viewHeight)
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
hasAdjustedSize = true
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public fun updateState(width: Int, height: Int) {
|
||||
val realWidth: Float = PixelUtil.toDIPFromPixel(width.toFloat())
|
||||
val realHeight: Float = PixelUtil.toDIPFromPixel(height.toFloat())
|
||||
|
||||
// Check incoming state values. If they're already the correct value, return early to prevent
|
||||
// infinite UpdateState/SetState loop.
|
||||
val currentState: ReadableMap? = stateWrapper?.getStateData()
|
||||
if (currentState != null) {
|
||||
val delta = 0.9f
|
||||
val stateScreenHeight =
|
||||
if (currentState.hasKey("screenHeight")) {
|
||||
currentState.getDouble("screenHeight").toFloat()
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
val stateScreenWidth =
|
||||
if (currentState.hasKey("screenWidth")) {
|
||||
currentState.getDouble("screenWidth").toFloat()
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
|
||||
if (abs((stateScreenWidth - realWidth).toDouble()) < delta &&
|
||||
abs((stateScreenHeight - realHeight).toDouble()) < delta) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
stateWrapper?.let { sw ->
|
||||
val newStateData: WritableMap = WritableNativeMap()
|
||||
newStateData.putDouble("screenWidth", realWidth.toDouble())
|
||||
newStateData.putDouble("screenHeight", realHeight.toDouble())
|
||||
sw.updateState(newStateData)
|
||||
}
|
||||
}
|
||||
|
||||
override fun addView(child: View, index: Int, params: LayoutParams) {
|
||||
super.addView(child, index, params)
|
||||
if (hasAdjustedSize) {
|
||||
updateFirstChildView()
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleException(t: Throwable) {
|
||||
reactContext.reactApplicationContext.handleException(RuntimeException(t))
|
||||
}
|
||||
|
||||
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
|
||||
eventDispatcher?.let { eventDispatcher ->
|
||||
jSTouchDispatcher.handleTouchEvent(event, eventDispatcher)
|
||||
jSPointerDispatcher?.handleMotionEvent(event, eventDispatcher, true)
|
||||
}
|
||||
return super.onInterceptTouchEvent(event)
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
eventDispatcher?.let { eventDispatcher ->
|
||||
jSTouchDispatcher.handleTouchEvent(event, eventDispatcher)
|
||||
jSPointerDispatcher?.handleMotionEvent(event, eventDispatcher, false)
|
||||
}
|
||||
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 fun onInterceptHoverEvent(event: MotionEvent): Boolean {
|
||||
eventDispatcher?.let { jSPointerDispatcher?.handleMotionEvent(event, it, true) }
|
||||
return super.onHoverEvent(event)
|
||||
}
|
||||
|
||||
override fun onHoverEvent(event: MotionEvent): Boolean {
|
||||
eventDispatcher?.let { jSPointerDispatcher?.handleMotionEvent(event, it, false) }
|
||||
return super.onHoverEvent(event)
|
||||
}
|
||||
|
||||
override fun onChildStartedNativeGesture(childView: View, ev: MotionEvent) {
|
||||
eventDispatcher?.let { eventDispatcher ->
|
||||
jSTouchDispatcher.onChildStartedNativeGesture(ev, eventDispatcher)
|
||||
jSPointerDispatcher?.onChildStartedNativeGesture(childView, ev, eventDispatcher)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onChildEndedNativeGesture(childView: View, ev: MotionEvent) {
|
||||
eventDispatcher?.let { jSTouchDispatcher.onChildEndedNativeGesture(ev, it) }
|
||||
jSPointerDispatcher?.onChildEndedNativeGesture()
|
||||
}
|
||||
|
||||
override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
|
||||
// No-op - override in order to still receive events to onInterceptTouchEvent
|
||||
// even when some other view disallow that
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user