I have a React Native app which will include a graph that the user can scroll through. For the sake of this example, I'm just using a horizontal row of circles.
To draw my graph, I am using Skia (https://shopify.github.io/react-native-skia/)
Normally, I would create a Canvas object that is large enough to contain the entire graph, and throw that inside a ScrollView.
<ScrollView horizontal={true}>
<Canvas style={{ width:5000, height:200 }}>
<GetCircles />
</Canvas>
</ScrollView>
The problem is that this canvas needs to be VERY large, since the user will be able to scroll through months of data. Therefore, the width
property may need to be so large that the underlying OpenGL texture is created at a size that is too large for the hardware or OpenGL implementation (or so I assume from experience with Skia in other environments such as Xamarin.Forms).
In my tests, setting a width of around 6300 causes the app to immediately crash.
To address this, my thought was to overlay a ScrollView on top of the Skia Canvas. The ScrollView could contain a large View object to match the size that I intend to simulate, and whenever the ScrollView scrolls (its onScroll
event is fired), then I would set a state which ultimately results in the Skia Canvas being drawn with a translation, as shown in the following snippet:
const [scrollOffset, setScrollOffset] = useState(0);
function handleScroll(event: NativeSyntheticEvent<NativeScrollEvent>): void {
setScrollOffset(event.nativeEvent.contentOffset.x);
}
...
<Canvas style={{ width:800, height:200 }}>
<Group transform={[{translateX:-scrollOffset}]}>
<GetCircles />
</Group>
</Canvas>
<ScrollView horizontal={true} style={{height:100}} onScroll={handleScroll}>
{/* These are added just for the sake of the example: */}
<View style={{width:150, backgroundColor:"red"}} ></View>
<View style={{width:150, backgroundColor:"blue"}} ></View>
<View style={{width:150, backgroundColor:"red"}} ></View>
<View style={{width:150, backgroundColor:"blue"}} ></View>
<View style={{width:150, backgroundColor:"red"}} ></View>
<View style={{width:150, backgroundColor:"blue"}} ></View>
<View style={{width:150, backgroundColor:"red"}} ></View>
<View style={{width:150, backgroundColor:"blue"}} ></View>
</ScrollView>
This approach works in terms of having the Skia Canvas update its transformation according to the scroll view, but as you can see in the following GIF, there is some pretty serious lag. I have tested this on actual hardware, so I know the problem is not from my screen mirroring/recording.
How can I solve this problem? Is there some alternative way to render very large, scrollable graphics in Skia in React Native that does not result in a crash or lag?
Instead of having a scroll view, you could use React Native Reanimated and React Native Gesture Handler.
The
onScroll
handler is running on the JS thread, which makes animations laggy.If you wrap the Canvas with
GestureDetector
, and use the gesture to update a shared reanimated value, that is then passed to the group, the lagging should be fixed. When using shared values, instead of going from the handler through the UI thread and back to the UI component, they are directly passed to the reanimated component.To animate the Group transform using an animated value, it should be wrapped using the
createAnimatedComponent
Hope this helps!