From a2afc13d4b25dd31712fc0e2131d2d56cb3d4283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Sun, 20 Oct 2024 08:58:26 +0200 Subject: [PATCH] RFC: FullWindowOverlay on android --- .../rnscreens/FullWindowOverlayViewManager.kt | 124 ++++++++++++++++++ .../swmansion/rnscreens/RNScreensPackage.kt | 1 + apps/src/tests/TestFullWindowOverlay.tsx | 30 +++++ apps/src/tests/index.ts | 1 + guides/CONTRIBUTING.md | 2 +- src/components/FullWindowOverlay.tsx | 6 +- 6 files changed, 158 insertions(+), 6 deletions(-) create mode 100644 android/src/main/java/com/swmansion/rnscreens/FullWindowOverlayViewManager.kt create mode 100644 apps/src/tests/TestFullWindowOverlay.tsx diff --git a/android/src/main/java/com/swmansion/rnscreens/FullWindowOverlayViewManager.kt b/android/src/main/java/com/swmansion/rnscreens/FullWindowOverlayViewManager.kt new file mode 100644 index 0000000000..026f197523 --- /dev/null +++ b/android/src/main/java/com/swmansion/rnscreens/FullWindowOverlayViewManager.kt @@ -0,0 +1,124 @@ +package com.swmansion.rnscreens + +import android.content.Context +import android.graphics.PixelFormat +import android.view.MotionEvent +import android.view.View +import android.view.WindowManager +import com.facebook.react.bridge.ReactContext +import com.facebook.react.bridge.UiThreadUtil +import com.facebook.react.module.annotations.ReactModule +import com.facebook.react.uimanager.JSPointerDispatcher +import com.facebook.react.uimanager.JSTouchDispatcher +import com.facebook.react.uimanager.RootView +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.UIManagerHelper +import com.facebook.react.uimanager.ViewGroupManager +import com.facebook.react.uimanager.events.EventDispatcher +import com.facebook.react.views.view.ReactViewGroup + +class FullWindowOverlayRootViewGroup(val reactContext: ThemedReactContext): ReactViewGroup(reactContext), RootView { + internal var eventDispatcher: EventDispatcher? = null + + internal val jSTouchDispatcher: JSTouchDispatcher = JSTouchDispatcher(this) + internal var jSPointerDispatcher: JSPointerDispatcher? = null + + override fun onChildStartedNativeGesture(childView: View, ev: MotionEvent) { + eventDispatcher?.let { + jSTouchDispatcher.onChildStartedNativeGesture(ev, it) + jSPointerDispatcher?.onChildStartedNativeGesture(childView, ev, it) + } + } + + override fun onChildEndedNativeGesture(childView: View, ev: MotionEvent) { + eventDispatcher?.let { + jSTouchDispatcher.onChildEndedNativeGesture(ev, it) + jSPointerDispatcher?.onChildEndedNativeGesture() + } + } + + 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) + } + + fun addToViewHierarchy() { + val windowManager = reactContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager + val windowParams = WindowManager.LayoutParams( + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.TYPE_APPLICATION, + (WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS + or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN), + PixelFormat.TRANSLUCENT + ) + windowManager.addView(this, windowParams) + } +} + +class FullWindowOverlay(val reactContext: ThemedReactContext): ReactViewGroup(reactContext) { + private val fullWindowOverlayRootViewGroup = FullWindowOverlayRootViewGroup(reactContext) + + init { + fullWindowOverlayRootViewGroup.addToViewHierarchy() + } + + public var eventDispatcher: EventDispatcher? + get() = fullWindowOverlayRootViewGroup.eventDispatcher + public set(eventDispatcher) { + fullWindowOverlayRootViewGroup.eventDispatcher = eventDispatcher + } + + public override fun getChildCount(): Int = fullWindowOverlayRootViewGroup.childCount + + public override fun getChildAt(index: Int): View? = fullWindowOverlayRootViewGroup.getChildAt(index) + + override fun addView(child: View?, index: Int) { + UiThreadUtil.assertOnUiThread() + fullWindowOverlayRootViewGroup.addView(child, index) + } + + override fun removeView(child: View?) { + UiThreadUtil.assertOnUiThread() + + if (child != null) { + fullWindowOverlayRootViewGroup.removeView(child) + } + } + + public override fun removeViewAt(index: Int) { + UiThreadUtil.assertOnUiThread() + val child = getChildAt(index) + fullWindowOverlayRootViewGroup.removeView(child) + } +} + +@ReactModule(name = FullWindowOverlayViewManager.REACT_CLASS) +class FullWindowOverlayViewManager: ViewGroupManager() { + companion object { + const val REACT_CLASS = "RNSFullWindowOverlay" + } + + override fun getName() = FullWindowOverlayViewManager.REACT_CLASS + + override fun createViewInstance(reactContext: ThemedReactContext) = FullWindowOverlay(reactContext) + + protected override fun addEventEmitters( + reactContext: ThemedReactContext, + view: FullWindowOverlay + ) { + val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.id) + if (dispatcher != null) { + view.eventDispatcher = dispatcher + } + } +} diff --git a/android/src/main/java/com/swmansion/rnscreens/RNScreensPackage.kt b/android/src/main/java/com/swmansion/rnscreens/RNScreensPackage.kt index 84124dd8ed..66d081871a 100644 --- a/android/src/main/java/com/swmansion/rnscreens/RNScreensPackage.kt +++ b/android/src/main/java/com/swmansion/rnscreens/RNScreensPackage.kt @@ -39,6 +39,7 @@ class RNScreensPackage : TurboReactPackage() { SearchBarManager(), ScreenFooterManager(), ScreenContentWrapperManager(), + FullWindowOverlayViewManager() ) } diff --git a/apps/src/tests/TestFullWindowOverlay.tsx b/apps/src/tests/TestFullWindowOverlay.tsx new file mode 100644 index 0000000000..7b5cda4fd4 --- /dev/null +++ b/apps/src/tests/TestFullWindowOverlay.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import { View, Modal, Text } from "react-native"; +import { FullWindowOverlay } from "react-native-screens"; + +export default function TestFullScreenOverlay() { + return ( + + + + RNView + + + + + Modal + + + + + FullWindowOverlay #1 + + + + + FullWindowOverlay #2 + + + + ) +} diff --git a/apps/src/tests/index.ts b/apps/src/tests/index.ts index 979aecaae8..5fdbb4de0a 100644 --- a/apps/src/tests/index.ts +++ b/apps/src/tests/index.ts @@ -121,3 +121,4 @@ export { default as TestActivityStateProgression } from './TestActivityStateProg export { default as TestHeaderTitle } from './TestHeaderTitle'; export { default as TestModalNavigation } from './TestModalNavigation'; export { default as TestMemoryLeak } from './TestMemoryLeak'; +export { default as TestFullWindowOverlay } from './TestFullWindowOverlay'; diff --git a/guides/CONTRIBUTING.md b/guides/CONTRIBUTING.md index 0e8fd74494..ed1787d84e 100644 --- a/guides/CONTRIBUTING.md +++ b/guides/CONTRIBUTING.md @@ -91,7 +91,7 @@ To begin with, let install all dependencies: 1. `yarn` 2. `yarn submodules` -3. `(cd react-navigation && yarn prepare)` +3. `(cd react-navigation && yarn build)` 4. `cd Example` 5. `yarn` 6. `yarn start` – make sure to start metro bundler before building the app in Android Studio diff --git a/src/components/FullWindowOverlay.tsx b/src/components/FullWindowOverlay.tsx index bbe41b61f7..bb7234249d 100644 --- a/src/components/FullWindowOverlay.tsx +++ b/src/components/FullWindowOverlay.tsx @@ -1,5 +1,5 @@ import React, { PropsWithChildren, ReactNode } from 'react'; -import { Platform, StyleProp, View, ViewStyle } from 'react-native'; +import { StyleProp, ViewStyle } from 'react-native'; // Native components import FullWindowOverlayNativeComponent from '../fabric/FullWindowOverlayNativeComponent'; @@ -10,10 +10,6 @@ const NativeFullWindowOverlay: React.ComponentType< > = FullWindowOverlayNativeComponent as any; function FullWindowOverlay(props: { children: ReactNode }) { - if (Platform.OS !== 'ios') { - console.warn('Using FullWindowOverlay is only valid on iOS devices.'); - return ; - } return (