diff --git a/android/build.gradle b/android/build.gradle index 81e011b579..99475ac756 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -45,9 +45,9 @@ def resolveReactNativeDirectory() { } throw new Exception( - "[react-native-gesture-handler] Unable to resolve react-native location in " + - "node_modules. You should add project extension property (in app/build.gradle) " + - "`REACT_NATIVE_NODE_MODULES_DIR` with path to react-native." + "[react-native-gesture-handler] Unable to resolve react-native location in " + + "node_modules. You should add project extension property (in app/build.gradle) " + + "`REACT_NATIVE_NODE_MODULES_DIR` with path to react-native." ) } @@ -75,6 +75,22 @@ def shouldUseCommonInterfaceFromReanimated() { } } +def shouldUseCommonInterfaceFromRNSVG() { + // common interface compatible with react-native-svg >= 15.11.2 + def rnsvg = rootProject.subprojects.find { it.name == 'react-native-svg' } + if (rnsvg == null) { + return false + } + + def inputFile = new File(rnsvg.projectDir, '../package.json') + def json = new JsonSlurper().parseText(inputFile.text) + def rnsvgVersion = json.version as String + def (major, minor, patch) = rnsvgVersion.tokenize('.') + return (Integer.parseInt(major) == 15 && Integer.parseInt(minor) == 11 && Integer.parseInt(patch) >= 2) || + (Integer.parseInt(major) == 15 && Integer.parseInt(minor) > 11) || + Integer.parseInt(major) > 15 +} + def reactNativeArchitectures() { def value = project.getProperties().get("reactNativeArchitectures") return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] @@ -119,15 +135,15 @@ android { buildConfigField "int", "REACT_NATIVE_MINOR_VERSION", REACT_NATIVE_MINOR_VERSION.toString() if (isNewArchitectureEnabled()) { - var appProject = rootProject.allprojects.find {it.plugins.hasPlugin('com.android.application')} + var appProject = rootProject.allprojects.find { it.plugins.hasPlugin('com.android.application') } externalNativeBuild { cmake { cppFlags "-O2", "-frtti", "-fexceptions", "-Wall", "-Werror", "-std=c++20", "-DANDROID" arguments "-DREACT_NATIVE_DIR=${REACT_NATIVE_DIR}", - "-DREACT_NATIVE_MINOR_VERSION=${REACT_NATIVE_MINOR_VERSION}", - "-DANDROID_STL=c++_shared", - "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON" - abiFilters (*reactNativeArchitectures()) + "-DREACT_NATIVE_MINOR_VERSION=${REACT_NATIVE_MINOR_VERSION}", + "-DANDROID_STL=c++_shared", + "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON" + abiFilters(*reactNativeArchitectures()) } } } @@ -168,13 +184,19 @@ android { srcDirs += 'noreanimated/src/main/java' } + if (shouldUseCommonInterfaceFromRNSVG()) { + srcDirs += 'svg/src/main/java' + } else { + srcDirs += 'nosvg/src/main/java' + } + if (isNewArchitectureEnabled()) { srcDirs += 'fabric/src/main/java' } else { // 'paper/src/main/java' includes files from codegen so the library can compile with // codegen turned off - if (REACT_NATIVE_MINOR_VERSION > 77){ + if (REACT_NATIVE_MINOR_VERSION > 77) { srcDirs += 'paper/src/main/java' } else { srcDirs += 'paper77/src/main/java' @@ -211,15 +233,20 @@ def kotlin_version = safeExtGet('kotlinVersion', project.properties['RNGH_kotlin dependencies { implementation 'com.facebook.react:react-native:+' // from node_modules - + if (shouldUseCommonInterfaceFromReanimated()) { // Include Reanimated as dependency to load the common interface - implementation (rootProject.subprojects.find { it.name == 'react-native-reanimated' }) { - exclude group:'com.facebook.fbjni' // resolves "Duplicate class com.facebook.jni.CppException" + implementation(rootProject.subprojects.find { it.name == 'react-native-reanimated' }) { + // resolves "Duplicate class com.facebook.jni.CppException" + exclude group: 'com.facebook.fbjni' } } + if (shouldUseCommonInterfaceFromRNSVG()) { + implementation rootProject.subprojects.find { it.name == 'react-native-svg' } + } + implementation 'androidx.appcompat:appcompat:1.2.0' implementation "androidx.core:core-ktx:1.6.0" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" diff --git a/android/nosvg/src/main/java/com/swmansion/gesturehandler/RNSVGHitTester.kt b/android/nosvg/src/main/java/com/swmansion/gesturehandler/RNSVGHitTester.kt new file mode 100644 index 0000000000..6c4aeddf78 --- /dev/null +++ b/android/nosvg/src/main/java/com/swmansion/gesturehandler/RNSVGHitTester.kt @@ -0,0 +1,13 @@ +package com.swmansion.gesturehandler + +import android.view.View + +class RNSVGHitTester { + companion object { + @Suppress("UNUSED_PARAMETER") + fun isSvgElement(view: Any) = false + + @Suppress("UNUSED_PARAMETER") + fun hitTest(view: View, posX: Float, posY: Float) = false + } +} diff --git a/android/spotless.gradle b/android/spotless.gradle index afddc5e67b..d624b3ba2d 100644 --- a/android/spotless.gradle +++ b/android/spotless.gradle @@ -2,7 +2,7 @@ apply plugin: "com.diffplug.spotless" spotless { kotlin { - target "src/**/*.kt", "reanimated/**/*.kt", "noreanimated/**/*.kt", "common/**/*.kt" + target "src/**/*.kt", "reanimated/**/*.kt", "noreanimated/**/*.kt", "svg/**/*.kt", "nosvg/**/*.kt", "common/**/*.kt" ktlint().editorConfigOverride([indent_size: 2]) trimTrailingWhitespace() indentWithSpaces() diff --git a/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt b/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt index a9082c2f49..4d705a6a60 100644 --- a/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt +++ b/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt @@ -15,6 +15,7 @@ import com.facebook.react.bridge.UiThreadUtil import com.facebook.react.bridge.WritableArray import com.facebook.react.uimanager.PixelUtil import com.swmansion.gesturehandler.BuildConfig +import com.swmansion.gesturehandler.RNSVGHitTester import com.swmansion.gesturehandler.react.RNGestureHandlerTouchEvent import java.lang.IllegalStateException import java.util.* @@ -610,9 +611,13 @@ open class GestureHandler val padLeft = hitSlop[HIT_SLOP_LEFT_IDX] diff --git a/android/svg/src/main/java/com/swmansion/gesturehandler/RNSVGHitTester.kt b/android/svg/src/main/java/com/swmansion/gesturehandler/RNSVGHitTester.kt new file mode 100644 index 0000000000..4b4e94a117 --- /dev/null +++ b/android/svg/src/main/java/com/swmansion/gesturehandler/RNSVGHitTester.kt @@ -0,0 +1,67 @@ +package com.swmansion.gesturehandler + +import android.view.View +import androidx.core.view.children +import com.horcrux.svg.SvgView +import com.horcrux.svg.VirtualView + +class RNSVGHitTester { + companion object { + private fun getRootSvgView(view: View): SvgView { + var rootSvgView: SvgView + + rootSvgView = if (view is VirtualView) { + view.svgView!! + } else { + view as SvgView + } + + while (isSvgElement(rootSvgView.parent)) { + rootSvgView = if (rootSvgView.parent is VirtualView) { + (rootSvgView.parent as VirtualView).svgView!! + } else { + rootSvgView.parent as SvgView + } + } + + return rootSvgView + } + + fun isSvgElement(view: Any): Boolean { + return (view is VirtualView || view is SvgView) + } + + fun hitTest(view: View, posX: Float, posY: Float): Boolean { + val rootSvgView = getRootSvgView(view) + val viewLocation = intArrayOf(0, 0) + val rootLocation = intArrayOf(0, 0) + + view.getLocationOnScreen(viewLocation) + rootSvgView.getLocationOnScreen(rootLocation) + + // convert View-relative coordinates into SvgView-relative coordinates + val rootX = posX + viewLocation[0] - rootLocation[0] + val rootY = posY + viewLocation[1] - rootLocation[1] + + val pressedId = rootSvgView.reactTagForTouch(rootX, rootY) + val hasBeenPressed = view.id == pressedId + + // hitTest(view, ...) should only be called after isSvgElement(view) returns true + // Consequently, `view` will always be either SvgView or VirtualView + + val pressIsInBounds = + posX in 0.0..view.width.toDouble() && + posY in 0.0..view.height.toDouble() + + if (view is SvgView) { + val childrenIds = view.children.map { it.id } + + val hasChildBeenPressed = pressedId in childrenIds + + return (hasBeenPressed || hasChildBeenPressed) && pressIsInBounds + } + + return hasBeenPressed && pressIsInBounds + } + } +} diff --git a/example/App.tsx b/example/App.tsx index e227545ef2..8eec79603b 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -40,12 +40,16 @@ import PointerType from './src/release_tests/pointerType'; import SwipeableReanimation from './src/release_tests/swipeableReanimation'; import NestedGestureHandlerRootViewWithModal from './src/release_tests/nestedGHRootViewWithModal'; import TwoFingerPan from './src/release_tests/twoFingerPan'; +import SvgCompatibility from './src/release_tests/svg'; import NestedText from './src/release_tests/nestedText'; + import { PinchableBox } from './src/recipes/scaleAndRotate'; import PanAndScroll from './src/recipes/panAndScroll'; + import { BottomSheet } from './src/showcase/bottomSheet'; import Swipeables from './src/showcase/swipeable'; import ChatHeads from './src/showcase/chatHeads'; + import Draggable from './src/basic/draggable'; import MultiTap from './src/basic/multitap'; import BouncingBox from './src/basic/bouncing'; @@ -186,6 +190,10 @@ const EXAMPLES: ExamplesSection[] = [ component: NestedButtons, unsupportedPlatforms: new Set(['web', 'ios', 'macos']), }, + { + name: 'Svg integration with Gesture Handler', + component: SvgCompatibility, + }, { name: 'Double pinch & rotate', component: DoublePinchRotate }, { name: 'Double draggable', component: DoubleDraggable }, { name: 'Rows', component: Rows }, diff --git a/example/package.json b/example/package.json index 6f9af40868..18219f1c22 100644 --- a/example/package.json +++ b/example/package.json @@ -38,7 +38,7 @@ "react-native-reanimated": "~3.16.1", "react-native-safe-area-context": "4.12.0", "react-native-screens": "~4.4.0", - "react-native-svg": "15.8.0", + "react-native-svg": "15.11.2", "react-native-web": "~0.19.10" }, "devDependencies": { diff --git a/example/src/release_tests/svg/index.tsx b/example/src/release_tests/svg/index.tsx new file mode 100644 index 0000000000..569ff30166 --- /dev/null +++ b/example/src/release_tests/svg/index.tsx @@ -0,0 +1,117 @@ +import React from 'react'; +import { Text, View, StyleSheet } from 'react-native'; +import { Gesture, GestureDetector } from 'react-native-gesture-handler'; + +import Svg, { Circle, Rect } from 'react-native-svg'; + +export default function SvgExample() { + const circleElementTap = Gesture.Tap().onStart(() => + console.log('RNGH: clicked circle') + ); + const rectElementTap = Gesture.Tap().onStart(() => + console.log('RNGH: clicked parallelogram') + ); + const containerTap = Gesture.Tap().onStart(() => + console.log('RNGH: clicked container') + ); + const vbContainerTap = Gesture.Tap().onStart(() => + console.log('RNGH: clicked viewbox container') + ); + const vbInnerContainerTap = Gesture.Tap().onStart(() => + console.log('RNGH: clicked inner viewbox container') + ); + const vbCircleTap = Gesture.Tap().onStart(() => + console.log('RNGH: clicked viewbox circle') + ); + + return ( + + + + Overlapping SVGs with gesture detectors + + + + console.log('SVG: clicked container')}> + + console.log('SVG: clicked circle')} + /> + + + console.log('SVG: clicked parallelogram')} + /> + + + + + + Tapping each color should read to a different console.log output + + + + SvgView with SvgView with ViewBox + + + console.log('SVG: clicked viewbox container')}> + + + console.log('SVG: clicked inner viewbox container') + }> + + + console.log('SVG: clicked viewbox circle')} + /> + + + + + + + The viewBox property remaps SVG's coordinate space + + + ); +} + +const styles = StyleSheet.create({ + container: { + alignItems: 'center', + justifyContent: 'center', + marginBottom: 48, + }, + header: { + fontSize: 18, + fontWeight: 'bold', + margin: 10, + }, +}); diff --git a/example/yarn.lock b/example/yarn.lock index 9ce93d62a7..f541ff8cd0 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -5695,7 +5695,7 @@ import-fresh@^2.0.0: caller-path "^2.0.0" resolve-from "^3.0.0" -import-fresh@^3.0.0, import-fresh@^3.2.1: +import-fresh@^3.0.0, import-fresh@^3.2.1, import-fresh@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -8302,10 +8302,10 @@ react-native-screens@~4.4.0: react-freeze "^1.0.0" warn-once "^0.1.0" -react-native-svg@15.8.0: - version "15.8.0" - resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-15.8.0.tgz#9b5fd4f5cf5675197b3f4cbfcc77c215de2b9502" - integrity sha512-KHJzKpgOjwj1qeZzsBjxNdoIgv2zNCO9fVcoq2TEhTRsVV5DGTZ9JzUZwybd7q4giT/H3RdtqC3u44dWdO0Ffw== +react-native-svg@15.11.2: + version "15.11.2" + resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-15.11.2.tgz#7540e8e1eabc4dcd3b1e35ada5a1d9f1b96d37c4" + integrity sha512-+YfF72IbWQUKzCIydlijV1fLuBsQNGMT6Da2kFlo1sh+LE3BIm/2Q7AR1zAAR6L0BFLi1WaQPLfFUC9bNZpOmw== dependencies: css-select "^5.1.0" css-tree "^1.1.3" @@ -8769,7 +8769,7 @@ semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.1.3, semver@^7.2.1, semver@^7.3.5, semver@^7.3.7, semver@^7.5.3: +semver@^7.1.3, semver@^7.2.1, semver@^7.3.5, semver@^7.3.7, semver@^7.5.2, semver@^7.5.3: version "7.6.3" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==