Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add react-native-svg interface #3242

Merged
merged 55 commits into from
Feb 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
f79a1fd
add initial svg integration code
latekvo Nov 27, 2024
1dc6bd6
fix unnecessary path inclusion in build.gradle
latekvo Nov 27, 2024
c30f238
add optional java import declaration
latekvo Nov 27, 2024
446ccb7
Merge branch 'main' into @latekvo/add-svg-integration
latekvo Nov 27, 2024
b09b0bc
add helper functions to the integration interface
latekvo Nov 28, 2024
9ab3857
fix typing & lint on hit testing interface
latekvo Nov 28, 2024
093f718
complete hitTest implementation, use hit tester impl. in gesture hand…
latekvo Nov 28, 2024
bd51ac7
add svg example, remove unnecessary import
latekvo Nov 28, 2024
282cd4c
fix build when svg is not available
latekvo Nov 29, 2024
4bf1b70
adjust example to highlight ios issues
latekvo Nov 29, 2024
d22413d
update svg to version supporting cross-library interaction
latekvo Dec 3, 2024
013c7a3
add more tests to the example app
latekvo Dec 4, 2024
6bc9147
add expected behaviour output to the example app
latekvo Dec 18, 2024
782a433
Merge branch 'main' into @latekvo/add-svg-integration
latekvo Dec 18, 2024
5b9a918
fix coordinate systems when using viewBox
latekvo Dec 18, 2024
237ae24
Merge branch '@latekvo/add-svg-integration' of https://github.com/sof…
latekvo Dec 18, 2024
5ac6665
use a getter instead of directly reading svgView
latekvo Dec 18, 2024
45ddb0c
add comment explanation
latekvo Dec 19, 2024
a401536
add double-nested svg viewBox test case
latekvo Dec 19, 2024
939ecc8
fix comment
latekvo Dec 20, 2024
cf0bc83
initial view-traversing bounds checker, fix SvgView bounds recognition
latekvo Dec 20, 2024
cb07a0d
fix tree navigation
latekvo Jan 7, 2025
a21ba54
remove SvgView click handling, for now it causes critical issues inte…
latekvo Jan 7, 2025
cb39671
add SvgView support
latekvo Jan 7, 2025
debe98e
allow for a broader scope of classes to be hitTested
latekvo Jan 7, 2025
304b17c
fix onPress and GestureHandler discrepancy
latekvo Jan 8, 2025
a48dadc
simplify duplicate code
latekvo Jan 8, 2025
0ab54cb
simplify code
latekvo Jan 8, 2025
1651f80
remove redundant comments
latekvo Jan 8, 2025
1df8a09
assert svgView is non-null
latekvo Jan 8, 2025
edbe17f
simplify ancestor traversing expression
latekvo Jan 8, 2025
5a767c2
rename root svg function
latekvo Jan 8, 2025
629ab9c
(amend) rename variables
latekvo Jan 8, 2025
7660091
update example app name
latekvo Jan 8, 2025
411b887
early return in gradle build
latekvo Jan 8, 2025
543a8fe
use range syntax for bounds check
latekvo Jan 8, 2025
53d54a7
simplify gradle expression
latekvo Jan 8, 2025
cd1fc6d
rename example component name
latekvo Jan 9, 2025
a2c2ffe
simplify no-svg implementation
latekvo Jan 9, 2025
a6ba30a
simplify redundant svgView.reactTagForTouch calls
latekvo Jan 13, 2025
04f46dd
add comment explaining view parameter
latekvo Jan 14, 2025
84f5c48
simplify redundant if statement
latekvo Jan 14, 2025
be08e35
narrow down type
latekvo Jan 14, 2025
29e8492
fix nosvg function signature
latekvo Jan 14, 2025
cfc019a
Merge branch 'main' into @latekvo/add-svg-integration
latekvo Jan 29, 2025
b0c64a3
update yarn.lock
latekvo Jan 29, 2025
f826f83
bump svg to compatible version
latekvo Feb 24, 2025
20d4081
fix version requirement in build.gradle
latekvo Feb 24, 2025
0efb50f
Merge branch 'main' into @latekvo/add-svg-integration
latekvo Feb 24, 2025
d411487
fix out of bounds issue
latekvo Feb 24, 2025
432956c
Merge branch '@latekvo/add-svg-integration' of https://github.com/sof…
latekvo Feb 24, 2025
70bf93a
(amend) fix version checks
latekvo Feb 25, 2025
98731e5
fix comment
latekvo Feb 25, 2025
423ac1c
move formatted comment
latekvo Feb 25, 2025
e589ebf
run formatter on gradle
latekvo Feb 25, 2025
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
51 changes: 39 additions & 12 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Comment on lines +48 to +50
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this done by a formatter?

Copy link
Contributor Author

@latekvo latekvo Feb 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the formatter.
I debated myself about removing these changes, but figured there's no benefit for deferring them, or opening a separate PR for them.

)
}

Expand Down Expand Up @@ -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
Comment on lines +89 to +91
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment above states that it is available from 15.11.0, but here we check for 15.11.2

Copy link
Contributor Author

@latekvo latekvo Feb 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the comment in 98731e5. The correct version is 15.11.2 (source).

}

def reactNativeArchitectures() {
def value = project.getProperties().get("reactNativeArchitectures")
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
Expand Down Expand Up @@ -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())
}
}
}
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
2 changes: 1 addition & 1 deletion android/spotless.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down Expand Up @@ -610,9 +611,13 @@ open class GestureHandler<ConcreteGestureHandlerT : GestureHandler<ConcreteGestu
}

fun isWithinBounds(view: View?, posX: Float, posY: Float): Boolean {
if (RNSVGHitTester.isSvgElement(view!!)) {
return RNSVGHitTester.hitTest(view, posX, posY)
}

var left = 0f
var top = 0f
var right = view!!.width.toFloat()
var right = view.width.toFloat()
var bottom = view.height.toFloat()
hitSlop?.let { hitSlop ->
val padLeft = hitSlop[HIT_SLOP_LEFT_IDX]
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
}
8 changes: 8 additions & 0 deletions example/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,16 @@
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';
Expand Down Expand Up @@ -186,6 +190,10 @@
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 },
Expand Down Expand Up @@ -327,7 +335,7 @@
renderSectionHeader={({ section: { sectionTitle } }) => (
<Text style={styles.sectionTitle}>{sectionTitle}</Text>
)}
ItemSeparatorComponent={() => <View style={styles.separator} />}

Check warning on line 338 in example/App.tsx

View workflow job for this annotation

GitHub Actions / check (example)

Do not define components during render. React will see a new component type on every render and destroy the entire subtree’s DOM nodes and state (https://reactjs.org/docs/reconciliation.html#elements-of-different-types). Instead, move this component definition out of the parent component “MainScreen” and pass data as props. If you want to allow component creation in props, set allowAsProps option to true
/>
</SafeAreaView>
);
Expand Down
2 changes: 1 addition & 1 deletion example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
117 changes: 117 additions & 0 deletions example/src/release_tests/svg/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<View>
<View style={styles.container}>
<Text style={styles.header}>
Overlapping SVGs with gesture detectors
</Text>
<View style={{ backgroundColor: 'tomato' }}>
<GestureDetector gesture={containerTap}>
<Svg
height="250"
width="250"
onPress={() => console.log('SVG: clicked container')}>
<GestureDetector gesture={circleElementTap}>
<Circle
cx="125"
cy="125"
r="125"
fill="green"
onPress={() => console.log('SVG: clicked circle')}
/>
</GestureDetector>
<GestureDetector gesture={rectElementTap}>
<Rect
skewX="45"
width="125"
height="250"
fill="yellow"
onPress={() => console.log('SVG: clicked parallelogram')}
/>
</GestureDetector>
</Svg>
</GestureDetector>
</View>
<Text>
Tapping each color should read to a different console.log output
</Text>
</View>
<View style={styles.container}>
<Text style={styles.header}>SvgView with SvgView with ViewBox</Text>
<View style={{ backgroundColor: 'tomato' }}>
<GestureDetector gesture={vbContainerTap}>
<Svg
height="250"
width="250"
viewBox="-50 -50 150 150"
onPress={() => console.log('SVG: clicked viewbox container')}>
<GestureDetector gesture={vbInnerContainerTap}>
<Svg
height="250"
width="250"
viewBox="-300 -300 600 600"
onPress={() =>
console.log('SVG: clicked inner viewbox container')
}>
<Rect
x="-300"
y="-300"
width="600"
height="600"
fill="yellow"
/>
<GestureDetector gesture={vbCircleTap}>
<Circle
r="300"
fill="green"
onPress={() => console.log('SVG: clicked viewbox circle')}
/>
</GestureDetector>
</Svg>
</GestureDetector>
</Svg>
</GestureDetector>
</View>
<Text>The viewBox property remaps SVG's coordinate space</Text>
</View>
</View>
);
}

const styles = StyleSheet.create({
container: {
alignItems: 'center',
justifyContent: 'center',
marginBottom: 48,
},
header: {
fontSize: 18,
fontWeight: 'bold',
margin: 10,
},
});
Loading
Loading