Skip to content

Commit

Permalink
Merge pull request #4 from margelo/fix/control-by-progress
Browse files Browse the repository at this point in the history
  • Loading branch information
hannojg authored Dec 12, 2023
2 parents 01b4f62 + 8570653 commit 89c34cc
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 99 deletions.
26 changes: 12 additions & 14 deletions cpp/RNSkSkottieView.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@
#include "SkBBHFactory.h"
#include "SkCanvas.h"
#include "SkPictureRecorder.h"
#include <algorithm>
#include <modules/skottie/include/Skottie.h>
#include <vector>
#include <algorithm>

#pragma clang diagnostic pop

Expand Down Expand Up @@ -183,18 +183,16 @@ class RNSkSkottieView : public RNSkView {

RNSkView::setJsiProperties(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);
});
}
// 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) {
Expand All @@ -212,7 +210,7 @@ class RNSkSkottieView : public RNSkView {
setDrawingMode(RNSkDrawingMode::Continuous);
} else if (prop.first == "pause") {
if (std::static_pointer_cast<RNSkSkottieRenderer>(getRenderer())->isPaused()) {
continue;
continue;
}

setDrawingMode(RNSkDrawingMode::Default);
Expand Down
105 changes: 71 additions & 34 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,18 @@ import {
SkiaSkottieView,
AnimationObject,
type SkiaSkottieViewRef,
SkottieAPI,
} from 'react-native-skottie';
import * as Animations from './animations';
import LottieView from 'lottie-react-native';
import DotLottieAnimation from './animations/Hands.lottie';
import { useMemo } from 'react';
import { useEffect, useMemo } from 'react';
import {
Easing,
useSharedValue,
withRepeat,
withTiming,
} from 'react-native-reanimated';

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

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

return (
<View style={styles.flex1}>
<Button
title="Play"
onPress={() => {
lottieRef.current?.play();
}}
/>
<Button
title="Pause"
onPress={() => {
lottieRef.current?.pause();
}}
/>
<Button
title="Resume"
onPress={() => {
lottieRef.current?.resume();
}}
/>
<Button
title="Reset"
onPress={() => {
lottieRef.current?.reset();
}}
/>
<LottieView
ref={lottieRef}
resizeMode="contain"
style={styles.flex1}
source={source}
loop={true}
autoPlay={false}
/>
</View>
);
}

function SkottiePropsAPI({ source }: { source: AnimationObject }) {
const [loop, setLoop] = React.useState(true);
const [autoPlay, setAutoPlay] = React.useState(false);
Expand Down Expand Up @@ -136,42 +184,29 @@ function SkottiePropsAPI({ source }: { source: AnimationObject }) {
);
}

function LottieImperativeAPI({ source }: { source: AnimationObject }) {
const lottieRef = React.useRef<LottieView>(null);
function SkottieProgressAPI({ source }: { source: AnimationObject }) {
// Create animation manually
const animation = useMemo(() => SkottieAPI.createFrom(source), [source]);
const progress = useSharedValue(0);

useEffect(() => {
// Run the animation using reanimated
progress.value = withRepeat(
withTiming(1, {
duration: animation.duration * 1000,
easing: Easing.linear,
}),
-1
);
}, [animation.duration, progress]);

return (
<View style={styles.flex1}>
<Button
title="Play"
onPress={() => {
lottieRef.current?.play();
}}
/>
<Button
title="Pause"
onPress={() => {
lottieRef.current?.pause();
}}
/>
<Button
title="Resume"
onPress={() => {
lottieRef.current?.resume();
}}
/>
<Button
title="Reset"
onPress={() => {
lottieRef.current?.reset();
}}
/>
<LottieView
ref={lottieRef}
resizeMode="contain"
<Text style={styles.heading}>Progress controlled example</Text>
<SkiaSkottieView
source={animation}
progress={progress}
style={styles.flex1}
source={source}
loop={true}
autoPlay={false}
/>
</View>
);
Expand Down Expand Up @@ -243,7 +278,7 @@ export default function App() {
case 'props-controlled':
return <SkottiePropsAPI source={animation} />;
case 'progress-controlled':
return <SkottieAnimation source={animation} />;
return <SkottieProgressAPI source={animation} />;
}
}
if (type === 'lottie') {
Expand All @@ -258,6 +293,8 @@ export default function App() {
return <LottieAnimation source={animation} />;
}
}

throw new Error('Invalid type');
}, [animation, exampleType, type]);

return (
Expand Down
46 changes: 31 additions & 15 deletions src/NativeSkottieModule.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import type {
SkCanvas,
SkJSIInstance,
SkRect,
} from '@shopify/react-native-skia';
import { NativeModules, Platform } from 'react-native';
import { Image, NativeModules, Platform } from 'react-native';
import type { SkottieViewSource, SkSkottie } from './types';

const LINKING_ERROR =
`The package 'react-native-skottie' doesn't seem to be linked. Make sure: \n\n` +
Expand Down Expand Up @@ -39,17 +35,37 @@ if (typeof SkiaSkottie.install === 'function') {
);
}

export interface SkSkottie extends SkJSIInstance<'Skottie'> {
duration: number;
fps: number;
render: (canvas: SkCanvas, rect: SkRect) => void;
seek: (progress: number) => void;
}

declare global {
var SkiaApi_SkottieCtor: (jsonString: string) => SkSkottie;
var SkiaApi_SkottieFromUri: (uri: string) => SkSkottie;
}

export const makeSkSkottieFromString = global.SkiaApi_SkottieCtor;
export const makeSkSkottieFromUri = global.SkiaApi_SkottieFromUri;
export const SkottieAPI = {
createFrom: (source: SkottieViewSource): SkSkottie => {
// Turn the source either into a JSON string, or a URI string:
let _source: string | { sourceDotLottieURI: string };

if (typeof source === 'string') {
_source = source;
} else if (typeof source === 'object') {
_source = JSON.stringify(source);
} else if (typeof source === 'number') {
const uri = Image.resolveAssetSource(source)?.uri;
if (uri == null) {
throw Error(
'[react-native-skottie] Invalid src prop provided. Cant resolve asset source.'
);
}
_source = { sourceDotLottieURI: uri };
} else {
throw Error('[react-native-skottie] Invalid src prop provided.');
}

// Actually create the Skottie instance:
if (typeof _source === 'string') {
return global.SkiaApi_SkottieCtor(_source);
} else {
return global.SkiaApi_SkottieFromUri(_source.sourceDotLottieURI);
}
},
};
46 changes: 10 additions & 36 deletions src/SkiaSkottieView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,15 @@ import React, {
} from 'react';
import { SkiaViewApi } from './SkiaViewApi';

import type { AnimationObject } from './types';
import type { SkottieViewSource, SkSkottie } from './types';
import { NativeSkiaSkottieView } from './NativeSkiaSkottieView';
import {
SkSkottie,
makeSkSkottieFromString,
makeSkSkottieFromUri,
} from './NativeSkottieModule';
import { SkottieAPI } from './NativeSkottieModule';
import { SharedValue, startMapper, stopMapper } from 'react-native-reanimated';
import { Image } from 'react-native';

export type ResizeMode = 'cover' | 'contain' | 'stretch';

export type SkiaSkottieViewProps = NativeSkiaViewProps & {
source: number | string | AnimationObject;
source: SkottieViewSource;

/**
* A boolean flag indicating whether or not the animation should start automatically when
Expand Down Expand Up @@ -78,37 +73,16 @@ export const SkiaSkottieView = React.forwardRef<
>((props, ref) => {
const nativeId = useRef(SkiaViewNativeId.current++).current;

//#region Compute values
const source = useMemo(() => {
let _source: string | { sourceDotLottieURI: string };
if (typeof props.source === 'string') {
_source = props.source;
} else if (typeof props.source === 'object') {
_source = JSON.stringify(props.source);
} else if (typeof props.source === 'number') {
const uri = Image.resolveAssetSource(props.source)?.uri;
if (uri == null) {
throw Error(
'[react-native-skottie] Invalid src prop provided. Cant resolve asset source.'
);
}
_source = { sourceDotLottieURI: uri };
} else {
throw Error('[react-native-skottie] Invalid src prop provided.');
}
return _source;
}, [props.source]);

const skottieAnimation = useMemo(() => {
if (typeof source === 'string') {
return makeSkSkottieFromString(source);
} else {
return makeSkSkottieFromUri(source.sourceDotLottieURI);
if (typeof props.source === 'object' && 'fps' in props.source) {
// Case: the user passed a SkSkottie instance
return props.source;
}
}, [source]);

return SkottieAPI.createFrom(props.source);
}, [props.source]);

const progress = props.progress;
//#endregion

// Handle animation updates
useEffect(() => {
Expand Down Expand Up @@ -182,7 +156,7 @@ export const SkiaSkottieView = React.forwardRef<

useLayoutEffect(() => {
updateAnimation(skottieAnimation);
}, [nativeId, skottieAnimation, source, updateAnimation]);
}, [nativeId, skottieAnimation, updateAnimation]);

// #region Prop controlled animation
// Start the animation
Expand Down
15 changes: 15 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import type {
SkCanvas,
SkJSIInstance,
SkRect,
} from '@shopify/react-native-skia';

/**
* Serialized animation as generated from After Effects
*/
Expand All @@ -14,3 +20,12 @@ export interface AnimationObject {
layers: any[];
markers?: any[];
}

export interface SkSkottie extends SkJSIInstance<'Skottie'> {
duration: number;
fps: number;
render: (canvas: SkCanvas, rect: SkRect) => void;
seek: (progress: number) => void;
}

export type SkottieViewSource = number | string | AnimationObject | SkSkottie;

0 comments on commit 89c34cc

Please sign in to comment.