gre / react-native-view-shot

Snapshot a React Native view and save it to an image
https://github.com/gre/react-native-view-shot-example
MIT License
2.64k stars 343 forks source link

Android production "IndexOutOfBoundsException" crashes #459

Closed maximilize closed 1 year ago

maximilize commented 1 year ago

Version & Platform

Version: v3.5.0 Platform: Android

Description

On production builds, we get quite many IndexOutOfBoundsException crashes. We are creating shots of views when the user is switching to another screen. This way, we can give the user an overview of all open screens in our app.

This is by far the biggest reason for our app crashing in production.

Stack trace

java.lang.IndexOutOfBoundsException: Index: 1, Size: 0
    at java.util.ArrayList.get(ArrayList.java:437)
    at android.view.ViewGroup.getAndVerifyPreorderedView(ViewGroup.java:4468)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8144)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at android.view.ViewGroup.gatherTransparentRegion(ViewGroup.java:8146)
    at com.android.internal.policy.DecorView.gatherTransparentRegion(DecorView.java:549)
    at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:3174)
    at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:2222)
    at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:9123)
    at android.view.Choreographer$CallbackRecord.run(Choreographer.java:999)
    at android.view.Choreographer.doCallbacks(Choreographer.java:797)
    at android.view.Choreographer.doFrame(Choreographer.java:732)
    at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:984)
    at android.os.Handler.handleCallback(Handler.java:883)
    at android.os.Handler.dispatchMessage(Handler.java:100)
    at android.os.Looper.loop(Looper.java:237)
    at android.app.ActivityThread.main(ActivityThread.java:8167)
    at java.lang.reflect.Method.invoke(Method.java)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:496)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1100)

Reproduce

I was not able to reproduce this bug in my dev build. However, this is a snipped of the code:

import { useIsFocused } from "@react-navigation/native";
import type { FC, ReactElement, StyleSheet } from "react";
import React, { useCallback, useEffect, useRef } from "react";
import { View } from "react-native";
import ViewShot from "react-native-view-shot";

const saveViewShot = async (res: any) => {
  console.log("Dummy saving view shot...");
};

const CardContent: FC<{
  children: ReactElement;
  isCardActive: boolean;
  allowScreenshot: boolean;
}> = ({ children, isCardActive, allowScreenshot }) => {
  const viewShotRef = useRef(null);
  const isScreenFocused = useIsFocused();

  const refData = () => ({
    isScreenFocused,
    isCardActive,
    allowScreenshot,
  });
  const ref = useRef(refData());
  ref.current = refData();

  const takeScreenshot = useCallback(async () => {
    try {
      const res = await viewShotRef.current?.capture();
      await saveViewShot(res);
    } catch (error) {
      console.log("Preview image capture error: ", error);
    }
  }, []);

  useEffect(() => {
    if (!isScreenFocused) {
      return;
    }

    return () => {
      if (ref.current.isCardActive && ref.current.allowScreenshot) {
        takeScreenshot().then();
      }
    };
  }, [isScreenFocused, takeScreenshot]);

  return (
    <ViewShot
      ref={viewShotRef}
      style={styles.container}
      options={viewShotConfig}
    >
      <View style={styles.container}>{children}</View>
    </ViewShot>
  );
};

const viewShotConfig = {
  format: "jpg",
  quality: 0.5,
  result: "base64",
} as const;

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
});