Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ open class NativeProxy {

mFabricUIManager =
UIManagerHelper.getUIManager(context, UIManagerType.FABRIC) as FabricUIManager
pseudoSelectorManager = PseudoSelectorManager(mFabricUIManager)
pseudoSelectorManager = PseudoSelectorManager(mFabricUIManager, mContext)

val callInvokerHolder = context.jsCallInvokerHolder as CallInvokerHolderImpl
mHybridData =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.swmansion.reanimated.pseudoSelectors

import android.util.Log
import android.view.Window
import com.facebook.react.bridge.ReactContext
import java.lang.reflect.Proxy

internal class ExtraWindowObserverBridge(
private val reactContext: ReactContext,
private val hover: TouchHoverCoordinator,
) {
private var listener: Any? = null

fun install() {
if (listener != null) {
return
}
try {
val listenerClass = Class.forName(LISTENER_CLASS)
val proxy =
Proxy.newProxyInstance(listenerClass.classLoader, arrayOf(listenerClass)) { self, method, args ->
when (method.name) {
"onExtraWindowCreate" -> hover.observeExtraWindow(args!![0] as Window)
"onExtraWindowDestroy" -> hover.stopObservingExtraWindow(args!![0] as Window)
"equals" -> return@newProxyInstance self === args!![0]
"hashCode" -> return@newProxyInstance System.identityHashCode(self)
"toString" ->
return@newProxyInstance "ReanimatedExtraWindowListener@" +
Integer.toHexString(System.identityHashCode(self))
}
null
}
reactContext.javaClass
.getMethod("addExtraWindowEventListener", listenerClass)
.invoke(reactContext, proxy)
listener = proxy
} catch (e: Throwable) {
Log.w(TAG, "ExtraWindowEventListener unavailable; modal :hover uses the per-view fallback", e)
}
}

fun uninstall() {
val proxy = listener ?: return
listener = null
try {
val listenerClass = Class.forName(LISTENER_CLASS)
reactContext.javaClass
.getMethod("removeExtraWindowEventListener", listenerClass)
.invoke(reactContext, proxy)
} catch (e: Throwable) {
Log.w(TAG, "Failed to remove ExtraWindowEventListener", e)
}
}

private companion object {
const val LISTENER_CLASS = "com.facebook.react.interfaces.ExtraWindowEventListener"
const val TAG = "Reanimated"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,22 @@ import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.view.ViewParent
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.UIManager
import com.facebook.react.bridge.UIManagerListener
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.common.annotations.UnstableReactNativeAPI
import com.facebook.react.fabric.FabricUIManager
import com.facebook.react.uimanager.IllegalViewOperationException
import com.facebook.react.uimanager.ReactCompoundView
import com.swmansion.reanimated.BuildConfig
import com.swmansion.reanimated.nativeProxy.PseudoSelectorCallback
import java.lang.ref.WeakReference

@OptIn(UnstableReactNativeAPI::class)
class PseudoSelectorManager(
private val fabricUIManager: FabricUIManager,
private val reactContext: WeakReference<ReactApplicationContext>,
) {
private val detachActions = HashMap<String, Runnable>()

Expand All @@ -30,6 +34,8 @@ class PseudoSelectorManager(

private val hover = TouchHoverCoordinator()

private var extraWindowBridge: ExtraWindowObserverBridge? = null

private val pendingAttaches = LinkedHashMap<String, PendingAttach>()
private var mountListenerRegistered = false

Expand Down Expand Up @@ -164,13 +170,22 @@ class PseudoSelectorManager(
val host = findTouchHost(view)
acquireTouchListener(host)
hover.register(view, callback)
ensureExtraWindowBridge()
detachActions[key] =
Runnable {
hover.unregister(view)
releaseTouchListener(host)
}
}

private fun ensureExtraWindowBridge() {
if (!BuildConfig.IS_REACT_NATIVE_86_OR_NEWER || extraWindowBridge != null) {
return
}
val context = reactContext.get() ?: return
extraWindowBridge = ExtraWindowObserverBridge(context, hover).also { it.install() }
}

private fun attachActive(
view: View,
key: String,
Expand Down Expand Up @@ -268,7 +283,7 @@ class PseudoSelectorManager(
MotionEvent.ACTION_CANCEL ->
if (event.findPointerIndex(0) >= 0) {
onHostRelease(host)
hover.onViewTouchCancel(event)
hover.onViewTouchCancel(host, event)
}
}
return false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,28 @@ class TouchHoverCoordinator {
private val tmpLocation = IntArray(2)
private val tmpCoords = FloatArray(2)

private var observedGestureDownTime = Long.MIN_VALUE

private var settledGestureDownTime = Long.MIN_VALUE

private var observedWindow: WeakReference<Window>? = null
private var originalWindowCallback: WeakReference<Window.Callback>? = null
private var wrappedWindowCallback: WeakReference<Window.Callback>? = null
private val observedWindows = mutableListOf<WeakReference<WindowObserver>>()

private inner class WindowObserver(
window: Window,
val original: Window.Callback,
) : Window.Callback by original {
val windowRef = WeakReference(window)

override fun dispatchTouchEvent(event: MotionEvent): Boolean {
val root = windowRef.get()?.decorView as? ViewGroup
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> reconcile(root, event.rawX, event.rawY)
MotionEvent.ACTION_UP ->
if (event.findPointerIndex(0) >= 0) settleHover(root, event)
MotionEvent.ACTION_POINTER_UP ->
if (event.getPointerId(event.actionIndex) == 0) settleHover(root, event)
}
return original.dispatchTouchEvent(event)
}
}

fun register(
view: View,
Expand All @@ -49,10 +64,19 @@ class TouchHoverCoordinator {
callback?.onSelectorStateChanged(false)
}
if (hoverCallbacks.isEmpty()) {
removeWindowObserver()
removeAllWindowObservers()
}
}

fun observeExtraWindow(window: Window) {
installObserverOnWindow(window)
}

fun stopObservingExtraWindow(window: Window) {
removeObserverFromWindow(window)
clearHoverForWindow(window)
}

fun recompute(
sourceView: View,
screenX: Float,
Expand All @@ -65,7 +89,7 @@ class TouchHoverCoordinator {
sourceView: View,
event: MotionEvent,
) {
if (event.downTime == observedGestureDownTime || isGestureSettled(event)) {
if (isWindowObserved(sourceView) || isGestureSettled(event)) {
return
}
reconcile(sourceView.rootView as? ViewGroup, event.rawX, event.rawY)
Expand All @@ -75,14 +99,17 @@ class TouchHoverCoordinator {
sourceView: View,
event: MotionEvent,
) {
if (event.downTime == observedGestureDownTime) {
if (isWindowObserved(sourceView)) {
return
}
settleHover(sourceView.rootView as? ViewGroup, event)
}

fun onViewTouchCancel(event: MotionEvent) {
if (event.downTime == observedGestureDownTime || isGestureSettled(event)) {
fun onViewTouchCancel(
sourceView: View,
event: MotionEvent,
) {
if (isWindowObserved(sourceView) || isGestureSettled(event)) {
return
}
settledGestureDownTime = event.downTime
Expand All @@ -91,13 +118,6 @@ class TouchHoverCoordinator {

fun isGestureSettled(event: MotionEvent) = event.downTime == settledGestureDownTime

fun recompute(
screenX: Float,
screenY: Float,
) {
reconcile(hoverRootViewGroup(), screenX, screenY)
}

private fun reconcile(
root: ViewGroup?,
screenX: Float,
Expand Down Expand Up @@ -147,11 +167,6 @@ class TouchHoverCoordinator {
return targets.mapTo(HashSet(targets.size)) { it.getViewId() }
}

private fun hoverRootViewGroup(): ViewGroup? {
val window = observedWindow?.get() ?: hoverCallbacks.keys.firstOrNull()?.activityWindow()
return window?.decorView as? ViewGroup
}

private fun clearAll() {
if (hoveredViews.isEmpty()) {
return
Expand All @@ -161,6 +176,18 @@ class TouchHoverCoordinator {
}
}

private fun clearHoverForWindow(window: Window) {
if (hoveredViews.isEmpty()) {
return
}
val decor = window.decorView
for (view in hoveredViews.toList()) {
if (view.rootView === decor) {
hoverCallbacks[view]?.let { setHovered(view, it, false) }
}
}
}

private fun setHovered(
view: View,
callback: PseudoSelectorCallback,
Expand All @@ -175,49 +202,66 @@ class TouchHoverCoordinator {

private fun ensureWindowObserver(view: View) {
val window = view.activityWindow() ?: return
if (observedWindow?.get() === window) {
installObserverOnWindow(window)
}

private fun installObserverOnWindow(window: Window) {
pruneObservers()
if (isObserving(window)) {
return
}
removeWindowObserver()
val original = window.callback ?: return
val wrapper =
object : Window.Callback by original {
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
observedGestureDownTime = event.downTime
recompute(event.rawX, event.rawY)
}
MotionEvent.ACTION_UP ->
if (event.findPointerIndex(0) >= 0) {
settleHover(hoverRootViewGroup(), event)
}
MotionEvent.ACTION_POINTER_UP ->
if (event.getPointerId(event.actionIndex) == 0) {
settleHover(hoverRootViewGroup(), event)
}
}
return original.dispatchTouchEvent(event)
val observer = WindowObserver(window, original)
observedWindows.add(WeakReference(observer))
window.callback = observer
}

private fun removeObserverFromWindow(window: Window) {
val iterator = observedWindows.iterator()
while (iterator.hasNext()) {
val observer = iterator.next().get()
if (observer == null) {
iterator.remove()
continue
}
if (observer.windowRef.get() === window) {
if (window.callback === observer) {
window.callback = observer.original
}
iterator.remove()
}
originalWindowCallback = WeakReference(original)
wrappedWindowCallback = WeakReference(wrapper)
observedWindow = WeakReference(window)
window.callback = wrapper
}
}

private fun removeWindowObserver() {
val window = observedWindow?.get()
val wrapper = wrappedWindowCallback?.get()
if (window != null && wrapper != null && window.callback === wrapper) {
window.callback = originalWindowCallback?.get()
private fun removeAllWindowObservers() {
for (reference in observedWindows) {
val observer = reference.get() ?: continue
val window = observer.windowRef.get() ?: continue
if (window.callback === observer) {
window.callback = observer.original
}
}
observedWindow = null
originalWindowCallback = null
wrappedWindowCallback = null
observedWindows.clear()
clearAll()
}

private fun isObserving(window: Window) = observedWindows.any { it.get()?.windowRef?.get() === window }

private fun isWindowObserved(view: View): Boolean {
val decor = view.rootView
return observedWindows.any {
it
.get()
?.windowRef
?.get()
?.decorView === decor
}
}

private fun pruneObservers() {
observedWindows.removeAll { it.get()?.windowRef?.get() == null }
}

private fun View.activityWindow(): Window? {
var ctx: Context? = context
while (ctx is ContextWrapper) {
Expand Down
Loading