Shopify / react-native-skia

High-performance React Native Graphics using Skia
https://shopify.github.io/react-native-skia
MIT License
6.79k stars 435 forks source link

makeImageFromView. Android crash when attempting to create an image from a view that is outside the viewport. #2265

Closed chiefchief closed 3 months ago

chiefchief commented 6 months ago

Description

I'm implementing a signature pad feature where users can update their signature. Upon updating, I need to capture the user's first and last name and take a screenshot. However, my view is located outside the viewport. I'm using Expo for development. Interestingly, everything works fine when the app is in debug mode. However, upon building the internal distribution version, the app crashes.

Below is the crash report from Sentry.

Screenshot 2024-03-04 at 12 54 13

Version

^0.1.240

Steps to reproduce

described above

Snack, code example, screenshot, or link to a repository

import { Canvas, Path, Skia, makeImageFromView, useCanvasRef, useTouchHandler } from '@shopify/react-native-skia';
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
import { Image, Text, View, useWindowDimensions } from 'react-native';

const activePath = Skia.Path.Make();

type SignatureProps = {
  width: number;
  height: number;
  data: { name: string; secondName: string };
  onUpdate?: (img: string) => void;
};

export const Signature: FC<SignatureProps> = ({ height, width, data, onUpdate }) => {
  const { width: screenWidth } = useWindowDimensions();
  const viewRef = useRef<View>(null);
  const canvasRef = useCanvasRef();
  const [imageSrc, setImageSrc] = useState('');

  useEffect(() => {
    return () => {
      activePath.reset();
    };
  }, []);

  const onTouch = useTouchHandler({
    onStart: ({ x, y }) => {
      activePath.moveTo(x, y);
    },
    onActive: ({ x, y }) => {
      activePath.lineTo(x, y);
    },
    onEnd: () => {
      const tempImage = canvasRef.current?.makeImageSnapshot();
      const resImage = tempImage?.encodeToBase64();
      if (resImage) {
        setImageSrc(`data:image/png;base64,${resImage}`);
      }
    },
  });

  const onLoad = useCallback(() => {
    // crash here
    makeImageFromView(viewRef).then((res) => {
      const base64Image = res?.encodeToBase64();
      if (base64Image) {
        onUpdate?.(base64Image);
      }
    });
  }, [onUpdate]);

  return (
    <>
      <View ref={viewRef} style={{ position: 'absolute', left: screenWidth }}>
        <View style={{ flexDirection: 'row' }}>
          <View style={{ flex: 1 }}>
            <Text>{`Name: ${data.name}`}</Text>
          </View>
          <View style={{ flex: 1, alignItems: 'flex-end' }}>
            <Text>{`Second Name: ${data.secondName}`}</Text>
          </View>
        </View>
        {imageSrc && <Image source={{ uri: imageSrc }} style={{ width, height }} onLoad={onLoad} />}
      </View>
      <Canvas ref={canvasRef} style={{ height, width, backgroundColor: 'white' }} onTouch={onTouch}>
        <Path path={activePath} style={'stroke'} strokeWidth={2} />
      </Canvas>
    </>
  );
};
chanphiromsok commented 5 months ago

I have the same issue https://shopify.github.io/react-native-skia/docs/canvas/overview just copy and paste crash on android

wesselvantilburg commented 5 months ago

I believe you need a collapsable={false} on your viewRef to solve this on android for views outside viewport. See https://shopify.github.io/react-native-skia/docs/snapshotviews/

return (
    <>
      <View ref={viewRef} style={{ position: 'absolute', left: screenWidth }} collapsable={false}>
        <View style={{ flexDirection: 'row' }}>
          <View style={{ flex: 1 }}>
            <Text>{`Name: ${data.name}`}</Text>
          </View>
          <View style={{ flex: 1, alignItems: 'flex-end' }}>
            <Text>{`Second Name: ${data.secondName}`}</Text>
          </View>
        </View>
        {imageSrc && <Image source={{ uri: imageSrc }} style={{ width, height }} onLoad={onLoad} />}
      </View>
      <Canvas ref={canvasRef} style={{ height, width, backgroundColor: 'white' }} onTouch={onTouch}>
        <Path path={activePath} style={'stroke'} strokeWidth={2} />
      </Canvas>
    </>
  );
chiefchief commented 5 months ago

I believe you need a collapsable={false} on your viewRef to solve this on android for views outside viewport. See https://shopify.github.io/react-native-skia/docs/snapshotviews/

return (
    <>
      <View ref={viewRef} style={{ position: 'absolute', left: screenWidth }} collapsable={false}>
        <View style={{ flexDirection: 'row' }}>
          <View style={{ flex: 1 }}>
            <Text>{`Name: ${data.name}`}</Text>
          </View>
          <View style={{ flex: 1, alignItems: 'flex-end' }}>
            <Text>{`Second Name: ${data.secondName}`}</Text>
          </View>
        </View>
        {imageSrc && <Image source={{ uri: imageSrc }} style={{ width, height }} onLoad={onLoad} />}
      </View>
      <Canvas ref={canvasRef} style={{ height, width, backgroundColor: 'white' }} onTouch={onTouch}>
        <Path path={activePath} style={'stroke'} strokeWidth={2} />
      </Canvas>
    </>
  );

i tried but didn't help, also tried - removeClippedSubviews: false, the same result

wcandillon commented 3 months ago

this should be fixed now, if it's still happening let us know and we will add it to the test suite.