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 39 commits into
base: main
Choose a base branch
from
Open

Conversation

latekvo
Copy link
Contributor

@latekvo latekvo commented Nov 27, 2024

Description

This PR adds integration with the react-native-svg (RNSVG) to improve hitbox detection of the SVG elements they provide.

blocked by software-mansion/react-native-svg#2583
blocked by nested SvgViews with viewBox prop have invalid hit detection - No issue opened yet

Test plan

  • use the newly added SVG integration example for testing this feature

@latekvo latekvo changed the title Integrate with react-native-svg for improved hit detection of svg elements Add interaction with react-native-svg to improve hit-boxes of svg elements Nov 27, 2024
@latekvo latekvo changed the title Add interaction with react-native-svg to improve hit-boxes of svg elements Add react-native-svg interface Nov 27, 2024
@latekvo
Copy link
Contributor Author

latekvo commented Nov 28, 2024

Integration works as of bd51ac7, but I have to test it a bit more before it's ready.

jakex7 pushed a commit to software-mansion/react-native-svg that referenced this pull request Nov 28, 2024
…interface (#2555)

<!-- Thanks for submitting a pull request! We appreciate you spending
the time to work on these changes. Please follow the template so that
the reviewers can easily understand what the code changes affect -->

# Summary

Adding an `RNGH <-> RNSVG` interface requires usage of
`RenderableView.hitTest` to work.
([link](software-mansion/react-native-gesture-handler#3242))
Currently, `RenderableView.hitTest` is `package-private`, meaning it
cannot be accessed by other packages.
This change does not change any functionality of the library, it only
exposes existing functions to other libraries.

I only made public those `hitTest` implementation which are strictly
necessary for this interface, this is why multiple, if not most
`hitTest` implementations remain `package-private` despite the changes
made in the PR.

## Test Plan

- open the example app, see how the app builds successfully

### What's required for testing (prerequisites)?

- `RNSVG`'s `paper-example` app

### What are the steps to reproduce (after prerequisites)?

## Compatibility

| OS      | Implemented |
| ------- | :---------: |
| iOS     |    ❌      |
| MacOS   |    ❌      |
| Android |    ✅      |
| Web     |    ❌      |

## Checklist

<!-- Check completed item, when applicable, via: [X] -->

- [X] I have tested this on a device and a simulator
- [ ] I added documentation in `README.md`
- [ ] ~~I updated the typed files (typescript)~~
- [ ] I added a test for the API in the `__tests__` folder
@latekvo latekvo changed the base branch from main to next December 5, 2024 15:17
@latekvo latekvo changed the base branch from next to main December 18, 2024 14:31
@latekvo latekvo requested review from m-bert and jakex7 January 8, 2025 13:53
@latekvo latekvo marked this pull request as ready for review January 8, 2025 13:53
@latekvo
Copy link
Contributor Author

latekvo commented Jan 8, 2025

// Using the latest version for now, todo: find the oldest working version

Will remove this comment, and replace the minimal version check with the latest version, as soon as this PR is released.

@latekvo latekvo requested a review from j-piasecki January 8, 2025 14:06
Copy link
Contributor

@m-bert m-bert left a comment

Choose a reason for hiding this comment

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

Overall looks ok, left some comments

android/build.gradle Outdated Show resolved Hide resolved
example/src/release_tests/svg/index.tsx Outdated Show resolved Hide resolved
example/App.tsx Outdated Show resolved Hide resolved
Comment on lines 49 to 63
if (view is SvgView) {
val childrenIds = view.children.map { it.id }

val hasBeenPressed = view.id == pressedId
val hasChildBeenPressed = pressedId in childrenIds

val pressIsInBounds = 0 < posX && posX < view.width && 0 < posY && posY < view.height

return (hasBeenPressed || hasChildBeenPressed) && pressIsInBounds
}

if (view is VirtualView) {
return view.id == highestOrderSvgView.reactTagForTouch(rootX, rootY)
}
return false
Copy link
Contributor

Choose a reason for hiding this comment

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

As far as I can see this is function is called iff view is SvgElement. In that case, can't we simplify it?

Suggested change
if (view is SvgView) {
val childrenIds = view.children.map { it.id }
val hasBeenPressed = view.id == pressedId
val hasChildBeenPressed = pressedId in childrenIds
val pressIsInBounds = 0 < posX && posX < view.width && 0 < posY && posY < view.height
return (hasBeenPressed || hasChildBeenPressed) && pressIsInBounds
}
if (view is VirtualView) {
return view.id == highestOrderSvgView.reactTagForTouch(rootX, rootY)
}
return false
if (view is VirtualView) {
return view.id == highestOrderSvgView.reactTagForTouch(rootX, rootY)
}
val childrenIds = view.children.map { it.id }
val pressIsInBounds = 0 < posX && posX < view.width && 0 < posY && posY < view.height
return (view.id == pressedId || pressedId in childrenIds) && pressIsInBounds

Also it would be nice to use .. instead of <= in bounds checks, but 0 might be a bit problematic.

Speaking of which, is it possible to detect click out of bounds, so that posX will be for example -1?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Are you sure we want to do that?
To me it seems the simplified version, especially the return statement, is much less readable.
The if statement also has a double purpose of triggering the smart-cast of view from View to SvgView.
Without the if statement, I'll have to explicitly cast the view to SvgView, which will also contribute to making the code less readable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also it would be nice to use .. instead of <= in bounds checks, but 0 might be a bit problematic.

Thankfully the ranges in Kotlin are inclusive, so 0 won't cause any issues.

Done in 543a8fe

Copy link
Contributor Author

@latekvo latekvo Jan 8, 2025

Choose a reason for hiding this comment

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

Speaking of which, is it possible to detect click out of bounds, so that posX will be for example -1?

I think that should be currently possible, the isWithinBounds call is executed for every gesture handler no matter it's position on the screen.

Are you asking about this in context of hitSlop?
Most SVG shapes aren't rectangular, so to implement hitSlop for them it'll likely require a lot more work, as the hitSlops will also have to be non-rectangular.

Copy link
Contributor

Choose a reason for hiding this comment

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

To me it seems the simplified version, especially the return statement, is much less readable.

For me it doesn't make much of a difference if we use pressedId == view.id instead of hasBeenPressed, it is simple enough to understand and I wouldn't call it much less readable.

The if statement also has a double purpose of triggering the smart-cast of view from View to SvgView.

Okay, that complicates things a bit. Adding 4 more casts may not be the cleanest solution. I also don't like stuff like x = x as ... (at least not in TypeScript). As far as I can see Kotlin doesn't support union types, so I'm not sure if there's much that we can do (cc kotlin master @j-piasecki).

Are you asking about this in context of hitSlop?

No, I was just wondering whether we have to check if posX >= 0, since coordinates are view-relative.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Adding 4 more casts may not be the cleanest solution

Only 1 cast will be necessary, but it's still not ideal.

No, I was just wondering whether we have to check if posX >= 0, since coordinates are view-relative.

I see. The received coordinates are often negative, so the posX >= 0 is indeed necessary.

For me it doesn't make much of a difference if we use pressedId == view.id instead of hasBeenPressed, it is simple enough to understand and I wouldn't call it much less readable.

It reduces the self-descriptiveness of both pressedId == view.id and pressedId in childrenIds, making it a bit harder to read, but i agree it's not that much of a difference.
If you prefer the simpler notation a lot, i'll change it right away.

Copy link
Contributor

Choose a reason for hiding this comment

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

Only 1 cast will be necessary, but it's still not ideal.

Do you mean only in first occurrence of view? If it's only 1 cast than it sounds ok.

If you prefer the simpler notation a lot, i'll change it right away.

It is not a strong opinion, it is just a fact that I don't think it is necessary to add new variables for obvious stuff. If for you it becomes less readable than you can leave it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you mean only in first occurrence of view? If it's only 1 cast than it sounds ok.

Yes, only the first occurrence.

But as I said, just looking at hitTest() function, it's not in any way clear that view is limited to being VirtualView and SvgView without looking at both the usages of hitTest(), and the rest of RNSVGHitTester.
Having if (view is SvgView) and if (view is VirtualView) is the only clear indication within hitTest() that view can be only either one of those.
Removing that indication in favour of an early return for VirtualView, and then casting the view to SvgView in the rest of the code may seem arbitrary to someone unfamiliar with the code, so I would prefer to keep it the way it is now.

Copy link
Contributor

Choose a reason for hiding this comment

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

(...) it's not in any way clear that view is limited to being VirtualView and SvgView (...)

I wouldn't say so since we are in the RNSVGHitTester file. Also, one line above hitTest there's isSvgElement method that also indicates which ones are from SVG.

The thing is, I don't like the idea of using if as cast if we can use cast directly (as you said, there's only one that is needed)

The if statement also has a double purpose of triggering the smart-cast of view from View to SvgView.

Now I'd say that it is its only purpose, am I right?

Removing that indication in favour of an early return for VirtualView, and then casting the view to SvgView in the rest of the code may seem arbitrary to someone unfamiliar with the code, so I would prefer to keep it the way it is now.

It is not really about early return, it is because this if is unnecessary. Also, even if it would be arbitrary, we can leave a comment explaining that these are the only two possibilities.

What's your stance on this, @j-piasecki?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants