-
-
Notifications
You must be signed in to change notification settings - Fork 998
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
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 1dc6bd6
fix unnecessary path inclusion in build.gradle
latekvo c30f238
add optional java import declaration
latekvo 446ccb7
Merge branch 'main' into @latekvo/add-svg-integration
latekvo b09b0bc
add helper functions to the integration interface
latekvo 9ab3857
fix typing & lint on hit testing interface
latekvo 093f718
complete hitTest implementation, use hit tester impl. in gesture hand…
latekvo bd51ac7
add svg example, remove unnecessary import
latekvo 282cd4c
fix build when svg is not available
latekvo 4bf1b70
adjust example to highlight ios issues
latekvo d22413d
update svg to version supporting cross-library interaction
latekvo 013c7a3
add more tests to the example app
latekvo 6bc9147
add expected behaviour output to the example app
latekvo 782a433
Merge branch 'main' into @latekvo/add-svg-integration
latekvo 5b9a918
fix coordinate systems when using viewBox
latekvo 237ae24
Merge branch '@latekvo/add-svg-integration' of https://github.com/sof…
latekvo 5ac6665
use a getter instead of directly reading svgView
latekvo 45ddb0c
add comment explanation
latekvo a401536
add double-nested svg viewBox test case
latekvo 939ecc8
fix comment
latekvo cf0bc83
initial view-traversing bounds checker, fix SvgView bounds recognition
latekvo cb07a0d
fix tree navigation
latekvo a21ba54
remove SvgView click handling, for now it causes critical issues inte…
latekvo cb39671
add SvgView support
latekvo debe98e
allow for a broader scope of classes to be hitTested
latekvo 304b17c
fix onPress and GestureHandler discrepancy
latekvo a48dadc
simplify duplicate code
latekvo 0ab54cb
simplify code
latekvo 1651f80
remove redundant comments
latekvo 1df8a09
assert svgView is non-null
latekvo edbe17f
simplify ancestor traversing expression
latekvo 5a767c2
rename root svg function
latekvo 629ab9c
(amend) rename variables
latekvo 7660091
update example app name
latekvo 411b887
early return in gradle build
latekvo 543a8fe
use range syntax for bounds check
latekvo 53d54a7
simplify gradle expression
latekvo cd1fc6d
rename example component name
latekvo a2c2ffe
simplify no-svg implementation
latekvo a6ba30a
simplify redundant svgView.reactTagForTouch calls
latekvo 04f46dd
add comment explaining view parameter
latekvo 84f5c48
simplify redundant if statement
latekvo be08e35
narrow down type
latekvo 29e8492
fix nosvg function signature
latekvo cfc019a
Merge branch 'main' into @latekvo/add-svg-integration
latekvo b0c64a3
update yarn.lock
latekvo f826f83
bump svg to compatible version
latekvo 20d4081
fix version requirement in build.gradle
latekvo 0efb50f
Merge branch 'main' into @latekvo/add-svg-integration
latekvo d411487
fix out of bounds issue
latekvo 432956c
Merge branch '@latekvo/add-svg-integration' of https://github.com/sof…
latekvo 70bf93a
(amend) fix version checks
latekvo 98731e5
fix comment
latekvo 423ac1c
move formatted comment
latekvo e589ebf
run formatter on gradle
latekvo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
Comment on lines
+89
to
+91
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
} | ||
|
||
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" | ||
|
13 changes: 13 additions & 0 deletions
13
android/nosvg/src/main/java/com/swmansion/gesturehandler/RNSVGHitTester.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
67 changes: 67 additions & 0 deletions
67
android/svg/src/main/java/com/swmansion/gesturehandler/RNSVGHitTester.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
||
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 | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
}); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.