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

Open
wants to merge 46 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 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
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
24 changes: 24 additions & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,20 @@ def shouldUseCommonInterfaceFromReanimated() {
}
}

// Using the latest version for now, todo: find the oldest working version
def shouldUseCommonInterfaceFromRNSVG() {
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) >= 8) || Integer.parseInt(major) > 15
}

def reactNativeArchitectures() {
def value = project.getProperties().get("reactNativeArchitectures")
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
Expand Down Expand Up @@ -167,6 +181,12 @@ 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 {
Expand Down Expand Up @@ -214,6 +234,10 @@ dependencies {
}
}

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 {
jakex7 marked this conversation as resolved.
Show resolved Hide resolved
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

if (view is SvgView) {
val childrenIds = view.children.map { it.id }

val hasChildBeenPressed = pressedId in childrenIds

val pressIsInBounds =
posX in 0.0..view.width.toDouble() &&
posY in 0.0..view.height.toDouble()

return (hasBeenPressed || hasChildBeenPressed) && pressIsInBounds
}

return hasBeenPressed
}
}
}
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.10.0",
"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