Skip to content

Commit c138546

Browse files
authored
fix(🤖): fix android warmup bug (#3565)
1 parent d8fc961 commit c138546

File tree

14 files changed

+111
-41
lines changed

14 files changed

+111
-41
lines changed

apps/docs/docs/canvas/canvas.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Behind the scenes, it is using its own React renderer.
1414
| style? | `ViewStyle` | View style |
1515
| ref? | `Ref<SkiaView>` | Reference to the `SkiaView` object |
1616
| onSize? | `SharedValue<Size>` | Reanimated value to which the canvas size will be assigned (see [canvas size](#canvas-size)) |
17+
| androidWarmup? | `boolean` | Draw the first frame directly on the Android compositor. Use it for static icons or fully opaque drawings—animated or translucent canvases can misrender, so it remains opt-in. |
1718

1819
## Canvas size
1920

@@ -124,4 +125,4 @@ export const Demo = () => {
124125

125126
The Canvas component supports the same properties as a View component including its [accessibility properties](https://reactnative.dev/docs/accessibility#accessible).
126127
You can make elements inside the canvas accessible as well by overlaying views on top of your canvas.
127-
This is the same recipe used for [applying gestures on specific canvas elements](https://shopify.github.io/react-native-skia/docs/animations/gestures/#element-tracking).
128+
This is the same recipe used for [applying gestures on specific canvas elements](https://shopify.github.io/react-native-skia/docs/animations/gestures/#element-tracking).

apps/example/src/Examples/API/FirstFrame.tsx

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
11
import React, { useState, useEffect } from "react";
2-
import { Button, StyleSheet, Text, View } from "react-native";
2+
import {
3+
Button,
4+
StyleSheet,
5+
Text,
6+
useWindowDimensions,
7+
View,
8+
} from "react-native";
9+
import { useNavigation } from "@react-navigation/native";
10+
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
311
import {
412
Canvas,
513
Circle,
614
Skia,
715
SkiaPictureView,
816
} from "@shopify/react-native-skia";
17+
import { ScrollView } from "react-native-gesture-handler";
18+
19+
import { AnimationWithTouchHandler } from "../Reanimated/AnimationWithTouchHandler";
20+
21+
import type { Routes } from "./Routes";
922

1023
const red = Skia.PictureRecorder();
1124
const canvas = red.beginRecording(Skia.XYWHRect(0, 0, 200, 200));
@@ -15,35 +28,60 @@ canvas.drawCircle(100, 100, 50, paint);
1528
const picture = red.finishRecordingAsPicture();
1629

1730
export const FirstFrame = () => {
31+
const { width } = useWindowDimensions();
1832
const [count, setCount] = useState(0);
1933
const [isRunning, setIsRunning] = useState(true);
34+
const navigation =
35+
useNavigation<NativeStackNavigationProp<Routes, "FirstFrame">>();
2036

2137
useEffect(() => {
2238
if (isRunning) {
2339
const interval = setInterval(() => {
2440
setCount((value) => value + 1);
25-
}, 50);
41+
}, 200);
2642

2743
return () => clearInterval(interval);
2844
}
2945
return undefined;
3046
}, [isRunning]);
3147

48+
return (
49+
<ScrollView>
50+
<View style={styles.container}>
51+
<Button
52+
onPress={() => setIsRunning((prev) => !prev)}
53+
title={isRunning ? "PAUSE" : "START"}
54+
/>
55+
<Text>{count}</Text>
56+
<SkiaPictureView
57+
key={`picture-${count}`}
58+
picture={picture}
59+
style={styles.canvas}
60+
androidWarmup
61+
></SkiaPictureView>
62+
<Canvas style={styles.canvas} key={`canvas-${count}`} androidWarmup>
63+
<Circle cx={100} cy={100} r={50} color="red" />
64+
</Canvas>
65+
<View style={{ width, height: 100 }}>
66+
<AnimationWithTouchHandler />
67+
</View>
68+
<Button
69+
title="Go to empty screen"
70+
onPress={() => navigation.navigate("FirstFrameEmpty")}
71+
/>
72+
</View>
73+
</ScrollView>
74+
);
75+
};
76+
77+
export const FirstFrameEmpty = () => {
78+
const navigation =
79+
useNavigation<NativeStackNavigationProp<Routes, "FirstFrameEmpty">>();
80+
3281
return (
3382
<View style={styles.container}>
34-
<Button
35-
onPress={() => setIsRunning((prev) => !prev)}
36-
title={isRunning ? "PAUSE" : "START"}
37-
/>
38-
<Text>{count}</Text>
39-
<SkiaPictureView
40-
key={`picture-${count}`}
41-
picture={picture}
42-
style={styles.canvas}
43-
></SkiaPictureView>
44-
<Canvas style={styles.canvas} key={`canvas-${count}`}>
45-
<Circle cx={100} cy={100} r={50} color="red" />
46-
</Canvas>
83+
<Text>Empty screen</Text>
84+
<Button title="Go back" onPress={() => navigation.goBack()} />
4785
</View>
4886
);
4987
};

apps/example/src/Examples/API/Icons/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ interface IconProps {
7474
const style = { width: 48, height: 48 };
7575

7676
const Icon = ({ icon }: IconProps) => {
77-
return <SkiaPictureView picture={icon} style={style} />;
77+
return <SkiaPictureView picture={icon} style={style} androidWarmup />;
7878
};
7979

8080
type Props = { color: string };
@@ -96,7 +96,7 @@ const Screen: React.FC<Props> = ({ color }) => {
9696
<Icon icon={stackExchange} />
9797
<Icon icon={overflow} />
9898
<Text>React Native Skia Canvas</Text>
99-
<Canvas style={{ width: 50, height: 50 }}>
99+
<Canvas style={{ width: 50, height: 50 }} androidWarmup>
100100
<Rect x={0} y={0} width={50} height={50} color={color} />
101101
</Canvas>
102102
</View>

apps/example/src/Examples/API/Routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,5 @@ export type Routes = {
3333
StressTest3: undefined;
3434
StressTest4: undefined;
3535
FirstFrame: undefined;
36+
FirstFrameEmpty: undefined;
3637
};

apps/example/src/Examples/API/index.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import { StressTest } from "./StressTest";
3535
import { StressTest2 } from "./StressTest2";
3636
import { StressTest3 } from "./StressTest3";
3737
import { StressTest4 } from "./StressTest4";
38-
import { FirstFrame } from "./FirstFrame";
38+
import { FirstFrame, FirstFrameEmpty } from "./FirstFrame";
3939
import { ZIndexExample } from "./ZIndex";
4040

4141
const Stack = createNativeStackNavigator<Routes>();
@@ -282,6 +282,13 @@ export const API = () => {
282282
title: "🎬 First Frame",
283283
}}
284284
/>
285+
<Stack.Screen
286+
name="FirstFrameEmpty"
287+
component={FirstFrameEmpty}
288+
options={{
289+
title: "⬜️ Empty Screen",
290+
}}
291+
/>
285292
</Stack.Navigator>
286293
);
287294
};

apps/example/src/Examples/Reanimated/AnimationWithTouchHandler.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from "react";
22
import { StyleSheet, useWindowDimensions } from "react-native";
3-
import { Canvas, Circle, Fill } from "@shopify/react-native-skia";
3+
import { Canvas, Circle } from "@shopify/react-native-skia";
44
import { GestureDetector, Gesture } from "react-native-gesture-handler";
55
import { useSharedValue, withDecay } from "react-native-reanimated";
66

@@ -28,7 +28,6 @@ export const AnimationWithTouchHandler = () => {
2828
<AnimationDemo title={"Decay animation with touch handler"}>
2929
<GestureDetector gesture={gesture}>
3030
<Canvas style={styles.canvas}>
31-
<Fill color="white" />
3231
<Circle cx={translateX} cy={40} r={20} color="#3E3E" />
3332
<Circle cx={translateX} cy={40} r={15} color="#AEAE" />
3433
</Canvas>

packages/skia/android/src/main/java/com/shopify/reactnative/skia/SkiaPictureView.java

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
import android.graphics.Bitmap;
55
import android.graphics.Canvas;
66
import android.graphics.Paint;
7+
import android.graphics.PorterDuff;
8+
import android.graphics.SurfaceTexture;
9+
import android.view.Surface;
710

811
import com.facebook.jni.HybridData;
912
import com.facebook.jni.annotations.DoNotStrip;
@@ -14,17 +17,17 @@ public class SkiaPictureView extends SkiaBaseView {
1417
private HybridData mHybridData;
1518
private Paint paint = new Paint();
1619

17-
private boolean coldStart = false;
20+
private boolean androidWarmup = false;
1821

1922
public SkiaPictureView(Context context) {
2023
super(context);
2124
RNSkiaModule skiaModule = ((ReactContext) context).getNativeModule(RNSkiaModule.class);
2225
mHybridData = initHybrid(skiaModule.getSkiaManager());
2326
}
2427

25-
public void setColdStart(boolean coldStart) {
26-
this.coldStart = coldStart;
27-
setWillNotDraw(coldStart);
28+
public void setAndroidWarmup(boolean androidWarmup) {
29+
this.androidWarmup = androidWarmup;
30+
setWillNotDraw(!androidWarmup);
2831
}
2932

3033
@Override
@@ -37,9 +40,9 @@ protected void finalize() throws Throwable {
3740
protected void onDraw(Canvas canvas) {
3841
super.onDraw(canvas);
3942

40-
// Skip the warming up feature if coldStart is true or running on software renderer
41-
if (coldStart) {
42-
return; // Skip warmup on cold start or software rendering
43+
// Skip the warming up feature if it is disabled or already cleared.
44+
if (!androidWarmup) {
45+
return;
4346
}
4447

4548
// Get the view dimensions
@@ -79,4 +82,24 @@ protected void onDraw(Canvas canvas) {
7982
protected native void unregisterView();
8083

8184
protected native int[] getBitmap(int width, int height);
85+
86+
@Override
87+
public void onSurfaceTextureCreated(SurfaceTexture surface, int width, int height) {
88+
super.onSurfaceTextureCreated(surface, width, height);
89+
}
90+
91+
@Override
92+
public void onSurfaceTextureChanged(SurfaceTexture surface, int width, int height) {
93+
super.onSurfaceTextureChanged(surface, width, height);
94+
}
95+
96+
@Override
97+
public void onSurfaceCreated(Surface surface, int width, int height) {
98+
super.onSurfaceCreated(surface, width, height);
99+
}
100+
101+
@Override
102+
public void onSurfaceChanged(Surface surface, int width, int height) {
103+
super.onSurfaceChanged(surface, width, height);
104+
}
82105
}

packages/skia/android/src/main/java/com/shopify/reactnative/skia/SkiaPictureViewManager.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public void setColorSpace(SkiaPictureView view, @Nullable String value) {
3838
}
3939

4040
@Override
41-
public void setColdStart(SkiaPictureView view, boolean value) {
42-
view.setColdStart(value);
41+
public void setAndroidWarmup(SkiaPictureView view, boolean value) {
42+
view.setAndroidWarmup(value);
4343
}
44-
}
44+
}

packages/skia/android/src/paper/java/com/facebook/react/viewmanagers/SkiaPictureViewManagerDelegate.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ public void setProperty(T view, String propName, @Nullable Object value) {
3131
case "colorSpace":
3232
mViewManager.setColorSpace(view, value == null ? null : (String) value);
3333
break;
34-
case "coldStart":
35-
mViewManager.setColdStart(view, value != null && (boolean) value);
34+
case "androidWarmup":
35+
mViewManager.setAndroidWarmup(view, value != null && (boolean) value);
3636
break;
3737
default:
3838
super.setProperty(view, propName, value);

packages/skia/android/src/paper/java/com/facebook/react/viewmanagers/SkiaPictureViewManagerInterface.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@ public interface SkiaPictureViewManagerInterface<T extends View> {
1818
void setDebug(T view, boolean value);
1919
void setOpaque(T view, boolean value);
2020
void setColorSpace(SkiaPictureView view, @Nullable String value);
21-
void setColdStart(T view, boolean value);
21+
void setAndroidWarmup(T view, boolean value);
2222
}

0 commit comments

Comments
 (0)