Skip to content

Commit

Permalink
Merge pull request #3 from margelo/fix/race-condition
Browse files Browse the repository at this point in the history
  • Loading branch information
hannojg authored Dec 12, 2023
2 parents 776d39d + 878e195 commit 01b4f62
Show file tree
Hide file tree
Showing 3 changed files with 249 additions and 105 deletions.
62 changes: 35 additions & 27 deletions cpp/RNSkSkottieView.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
#include "SkCanvas.h"
#include "SkPictureRecorder.h"
#include <modules/skottie/include/Skottie.h>
#include <vector>
#include <algorithm>

#pragma clang diagnostic pop

Expand Down Expand Up @@ -81,7 +83,7 @@ class RNSkSkottieRenderer : public RNSkRenderer, public std::enable_shared_from_
}

bool isPaused() {
return _lastPauseTime > 0.0;
return _lastPauseTime > 0.0 || _startTime == -1.0;
}

void pause() {
Expand Down Expand Up @@ -181,39 +183,45 @@ class RNSkSkottieView : public RNSkView {

RNSkView::setJsiProperties(props);

for (auto& prop : props) {
// We need to make sure .start gets called last.
// It might happen that setJsiProperties gets called multiple times before the view is actually ready.
// In this case all our "props" will be stored, and then once its ready setJsiProperties gets called
// with all the props at once. Then .start has to be called last, otherwise the animation will not play.
std::vector<std::pair<std::string, RNJsi::JsiValueWrapper>> sortedProps(props.begin(), props.end());
if (sortedProps.size() > 1) {
// Custom sort function to place 'start' at the end
std::sort(sortedProps.begin(), sortedProps.end(),
[](const auto& a, const auto& b) {
return !(a.first == "start") && (b.first == "start" || a.first < b.first);
});
}

for (auto& prop : sortedProps) {
if (prop.first == "src" && prop.second.getType() == RNJsi::JsiWrapperValueType::HostObject) {
std::static_pointer_cast<RNSkSkottieRenderer>(getRenderer())->setSrc(prop.second.getAsHostObject());
renderImmediate(); // Draw the first frame
}
if (prop.first == "scaleType") {
} else if (prop.first == "scaleType") {
std::static_pointer_cast<RNSkSkottieRenderer>(getRenderer())->setResizeMode(prop.second.getAsString());
}
}
}
} else if (prop.first == "setProgress") {
// Get first argument as number as it contains the updated value
auto progressValue = prop.second.getAsNumber();
std::static_pointer_cast<RNSkSkottieRenderer>(getRenderer())->setProgress(progressValue);
requestRedraw();
} else if (prop.first == "start") {
std::static_pointer_cast<RNSkSkottieRenderer>(getRenderer())->setStartTime(RNSkTime::GetSecs());
setDrawingMode(RNSkDrawingMode::Continuous);
} else if (prop.first == "pause") {
if (std::static_pointer_cast<RNSkSkottieRenderer>(getRenderer())->isPaused()) {
continue;
}

jsi::Value callJsiMethod(jsi::Runtime& runtime, const std::string& name, const jsi::Value* arguments, size_t count) override {
if (name == "setProgress") {
// Get first argument as number as it contains the updated value
auto progressValue = arguments[0].asNumber();
std::static_pointer_cast<RNSkSkottieRenderer>(getRenderer())->setProgress(progressValue);
requestRedraw();
} else if (name == "start") {
std::static_pointer_cast<RNSkSkottieRenderer>(getRenderer())->setStartTime(RNSkTime::GetSecs());
setDrawingMode(RNSkDrawingMode::Continuous);
} else if (name == "pause") {
if (std::static_pointer_cast<RNSkSkottieRenderer>(getRenderer())->isPaused()) {
return {};
setDrawingMode(RNSkDrawingMode::Default);
std::static_pointer_cast<RNSkSkottieRenderer>(getRenderer())->pause();
} else if (prop.first == "reset") {
std::static_pointer_cast<RNSkSkottieRenderer>(getRenderer())->resetPlayback();
setDrawingMode(RNSkDrawingMode::Default); // This will also trigger a requestRedraw
}

setDrawingMode(RNSkDrawingMode::Default);
std::static_pointer_cast<RNSkSkottieRenderer>(getRenderer())->pause();
} else if (name == "reset") {
std::static_pointer_cast<RNSkSkottieRenderer>(getRenderer())->resetPlayback();
setDrawingMode(RNSkDrawingMode::Default); // This will also trigger a requestRedraw
}

return {};
}
};
} // namespace RNSkia
181 changes: 151 additions & 30 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
StyleSheet,
SafeAreaView,
ScrollView,
Switch,
} from 'react-native';
import {
SkiaSkottieView,
Expand All @@ -15,6 +16,7 @@ import {
import * as Animations from './animations';
import LottieView from 'lottie-react-native';
import DotLottieAnimation from './animations/Hands.lottie';
import { useMemo } from 'react';

const animations = {
...Animations,
Expand Down Expand Up @@ -79,6 +81,61 @@ function SkottieImperativeAPI({ source }: { source: AnimationObject }) {
);
}

function SkottiePropsAPI({ source }: { source: AnimationObject }) {
const [loop, setLoop] = React.useState(true);
const [autoPlay, setAutoPlay] = React.useState(false);
const [speed, setSpeed] = React.useState(1);
const [_progress, setProgress] = React.useState(0);

return (
<View style={styles.flex1}>
<Button
title="Play"
onPress={() => {
setAutoPlay(true);
}}
/>
<Button
title="Pause"
onPress={() => {
setAutoPlay(false);
}}
/>
<Button
title="Reset"
onPress={() => {
setProgress(0);
}}
/>
<Button
title={`Toggle loop (its ${loop ? 'on' : 'off'})`}
onPress={() => {
setLoop((p) => !p);
}}
/>
<Button
title="Speed +1"
onPress={() => {
setSpeed((p) => p + 1);
}}
/>
<SkiaSkottieView
resizeMode="contain"
style={styles.flex1}
source={source}
loop={loop}
autoPlay={autoPlay}
speed={speed}
// TODO: that wouldn't work at the minute, and the imperative API should be used!
// progress={progress}
onAnimationFinish={() => {
console.log('onAnimationFinish');
}}
/>
</View>
);
}

function LottieImperativeAPI({ source }: { source: AnimationObject }) {
const lottieRef = React.useRef<LottieView>(null);

Expand Down Expand Up @@ -120,35 +177,100 @@ function LottieImperativeAPI({ source }: { source: AnimationObject }) {
);
}

type ExampleType =
| 'default'
| 'imperative'
| 'props-controlled'
| 'progress-controlled';

function ExampleTypeSwitches({
exampleType,
setExampleType,
}: {
exampleType: ExampleType;
setExampleType: (type: ExampleType) => void;
}) {
return (
<View>
<View style={styles.switchOption}>
<Switch
value={exampleType === 'default'}
onValueChange={() => setExampleType('default')}
/>
<Text>None</Text>
</View>
<View style={styles.switchOption}>
<Switch
value={exampleType === 'imperative'}
onValueChange={() => setExampleType('imperative')}
/>
<Text>Imperative API</Text>
</View>
<View style={styles.switchOption}>
<Switch
value={exampleType === 'props-controlled'}
onValueChange={() => setExampleType('props-controlled')}
/>
<Text>Props controlled</Text>
</View>
<View style={styles.switchOption}>
<Switch
value={exampleType === 'progress-controlled'}
onValueChange={() => setExampleType('progress-controlled')}
/>
<Text>Progress controlled</Text>
</View>
</View>
);
}

export default function App() {
const [type, setType] = React.useState<'skottie' | 'lottie'>('skottie');
const [isImperativeAPI, setIsImperativeAPI] = React.useState(false);
const [exampleType, setExampleType] = React.useState<ExampleType>('default');
const [animation, setAnimation] = React.useState<
AnimationObject | undefined
>();

const animationContent = useMemo(() => {

Check failure on line 234 in example/src/App.tsx

View workflow job for this annotation

GitHub Actions / lint

Not all code paths return a value.
if (animation == null) return null;

if (type === 'skottie') {
switch (exampleType) {
case 'default':
return <SkottieAnimation source={animation} />;
case 'imperative':
return <SkottieImperativeAPI source={animation} />;
case 'props-controlled':
return <SkottiePropsAPI source={animation} />;
case 'progress-controlled':
return <SkottieAnimation source={animation} />;
}
}
if (type === 'lottie') {
switch (exampleType) {
case 'default':
return <LottieAnimation source={animation} />;
case 'imperative':
return <LottieImperativeAPI source={animation} />;
case 'props-controlled':
return <LottieAnimation source={animation} />;
case 'progress-controlled':
return <LottieAnimation source={animation} />;
}
}
}, [animation, exampleType, type]);

return (
<SafeAreaView style={styles.flex1}>
{animation == null ? (
<ScrollView style={styles.flex1}>
<Button
title="Skottie imperative API"
onPress={() => {
setType('skottie');
setAnimation(animations.Hands);
setIsImperativeAPI(true);
}}
/>
<Button
title="Lottie imperative API"
onPress={() => {
setType('lottie');
setAnimation(animations.FastMoney);
setIsImperativeAPI(true);
}}
<Text style={styles.heading}>Example type</Text>
<ExampleTypeSwitches
exampleType={exampleType}
setExampleType={setExampleType}
/>

<Text>Skottie</Text>
<Text style={styles.heading}>Skottie</Text>
{Object.keys(animations).map((key) => (
<View key={`skottie-${key}`}>
<Button
Expand All @@ -161,7 +283,7 @@ export default function App() {
/>
</View>
))}
<Text>Lottie</Text>
<Text style={styles.heading}>Lottie</Text>
{Object.keys(animations).map((key) => (
<View key={`lottie-${key}`}>
<Button
Expand All @@ -177,21 +299,10 @@ export default function App() {
</ScrollView>
) : (
<View style={styles.flex1}>
{type === 'skottie' ? (
isImperativeAPI ? (
<SkottieImperativeAPI source={animation} />
) : (
<SkottieAnimation source={animation} />
)
) : isImperativeAPI ? (
<LottieImperativeAPI source={animation} />
) : (
<LottieAnimation source={animation} />
)}
{animationContent}
<Button
title="Back"
onPress={() => {
setIsImperativeAPI(false);
setAnimation(undefined);
}}
/>
Expand All @@ -205,4 +316,14 @@ const styles = StyleSheet.create({
flex1: {
flex: 1,
},
heading: {
marginTop: 16,
marginBottom: 8,
fontSize: 24,
fontWeight: 'bold',
},
switchOption: {
flexDirection: 'row',
alignItems: 'center',
},
});
Loading

0 comments on commit 01b4f62

Please sign in to comment.