Skip to content

Commit

Permalink
Add possibility to start layout animation when entering one hasn't fi…
Browse files Browse the repository at this point in the history
…nished (#2135)

Fixes: #2119
When we get notified by ReactBatchObserver about the dirtiness of a component then we have to choose how should we animate that layout change. For instance, we have to decide if it should be entering, layout, or exiting animation.
Each component is treated as a state machine with the following lifecycle:
inactive -> appearing -> layout -> disappearing -> toRemove
Currently, we do not start a layout animation if the component is still entering. The problem with that is sometimes the coordinates of the component can change and our component will not be rendered properly.  You can read more about it in the issue linked above. The solution for that is simply to start a layout animation when the component is dirty. The problem with that solution is the sometimes component can be dirty which doesn't mean that the layout of the component has changed. For instance, the component can be marked dirty because one of its descendants had changed its layout.
To fix the solution I also added check if the startingValues (layout snapshot took as the first operation of UIManager's UI queue operations) are the same as targetValues ((layout snapshot took as the last operation of UIManager's UI queue operations). If they are it means that there was no UI operation that changed the layout of the component and it means that the dirtiness of the component is a false positive.  

I also added 3 examples that helped me test this pull-request.
  • Loading branch information
Szymon20000 committed Jun 17, 2021
1 parent 1a6b2fb commit 4d85ca1
Show file tree
Hide file tree
Showing 9 changed files with 341 additions and 14 deletions.
2 changes: 1 addition & 1 deletion Example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ SPEC CHECKSUMS:
boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c
DoubleConversion: cf9b38bf0b2d048436d9a82ad2abe1404f11e7de
FBLazyVector: 49cbe4b43e445b06bf29199b6ad2057649e4c8f5
FBReactNativeSpec: 6d46d7c05350e10b3e711c32af90bb188b792a56
FBReactNativeSpec: a231a5702f05925855ad8d6449930e1a024bf38b
glog: 73c2498ac6884b13ede40eda8228cb1eee9d9d62
RCT-Folly: ec7a233ccc97cc556cf7237f0db1ff65b986f27c
RCTRequired: 2f8cb5b7533219bf4218a045f92768129cf7050a
Expand Down
5 changes: 5 additions & 0 deletions Example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
Carousel,
ModalNewAPI,
DefaultAnimations,
CustomLayoutAnimationScreen,
} from './LayoutReanimation';

import Reanimated1 from '../reanimated1/App';
Expand Down Expand Up @@ -41,6 +42,10 @@ const SCREENS: Screens = {
screen: DefaultAnimations,
title: '🆕 Default layout animations',
},
CustomLayoutAnimation: {
screen: CustomLayoutAnimationScreen,
title: '🆕 Custom layout animation',
},
ModalNewAPI: {
title: '🆕 ModalNewAPI',
screen: ModalNewAPI,
Expand Down
89 changes: 89 additions & 0 deletions Example/src/LayoutReanimation/CustomLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { selectAssetSource } from 'expo-asset/build/AssetSources';
import React, { useState } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import Animated, {
AnimatedLayout,
makeMutable,
withTiming,
withDelay,
SlideInDown,
} from 'react-native-reanimated';

function CustomLayoutTransiton() {
const isEven = makeMutable(1);
return (values) => {
'worklet';
const isEvenLocal = isEven.value;
isEven.value = 1 - isEven.value;

return {
animations: {
originX: withDelay(
isEvenLocal ? 1000 : 0,
withTiming(values.originX, { duration: 1000 })
),
originY: withDelay(
isEvenLocal ? 0 : 1000,
withTiming(values.originY, { duration: 1000 })
),
width: withTiming(values.width, { duration: 1000 }),
height: withTiming(values.height, { duration: 1000 }),
},
initialValues: {
originX: values.boriginX,
originY: values.boriginY,
width: values.bwidth,
height: values.bheight,
},
};
};
}

function Box({ label, state }: { label: string; state: boolean }) {
const ind = label.charCodeAt(0) - 'A'.charCodeAt(0);
const delay = 300 * ind;
return (
<Animated.View
layout={CustomLayoutTransiton()}
style={[styles.box, { height: state ? 30 : 60 }]}>
<Text> {label} </Text>
</Animated.View>
);
}

export function CustomLayoutAnimationScreen(): React.ReactElement {
const [state, setState] = useState(true);
return (
<View style={{ marginTop: 30 }}>
<View style={{ height: 300 }}>
<AnimatedLayout
style={{ flexDirection: state ? 'row' : 'column', borderWidth: 1 }}>
<Box key="a" label="A" state={state} />
<Box key="b" label="B" state={state} />
<Box key="c" label="C" state={state} />
</AnimatedLayout>
</View>

<Button
onPress={() => {
setState(!state);
}}
title="toggle"
/>
</View>
);
}

const styles = StyleSheet.create({
box: {
margin: 20,
padding: 5,
borderWidth: 1,
borderColor: 'black',
width: 60,
height: 60,
},
});



3 changes: 2 additions & 1 deletion Example/src/LayoutReanimation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export * from './SwipeableList';
export * from './Modal';
export * from './Carousel';
export * from './ModalNewAPI';
export * from './DefaultAnimations';
export * from './DefaultAnimations';
export * from './CustomLayout';
90 changes: 90 additions & 0 deletions Example/test/CustomLayout2.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { selectAssetSource } from 'expo-asset/build/AssetSources';
import React, { useState } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import Animated, {
AnimatedLayout,
makeMutable,
withTiming,
withDelay,
SlideInDown,
} from 'react-native-reanimated';

function CustomLayoutTransiton() {
const isEven = makeMutable(1);
return (values) => {
'worklet';
const isEvenLocal = isEven.value;
isEven.value = 1 - isEven.value;

return {
animations: {
originX: withDelay(
isEvenLocal ? 1000 : 0,
withTiming(values.originX, { duration: 1000 })
),
originY: withDelay(
isEvenLocal ? 0 : 1000,
withTiming(values.originY, { duration: 1000 })
),
width: withTiming(values.width, { duration: 1000 }),
height: withTiming(values.height, { duration: 1000 }),
},
initialValues: {
originX: values.boriginX,
originY: values.boriginY,
width: values.bwidth,
height: values.bheight,
},
};
};
}

function Box({ label, state }: { label: string; state: boolean }) {
const ind = label.charCodeAt(0) - 'A'.charCodeAt(0);
const delay = 300 * ind;
return (
<Animated.View
layout={CustomLayoutTransiton()}
entering={SlideInDown.delay(delay).duration(3000)}
style={[styles.box, { height: state ? 30 : 60 }]}>
<Text> {label} </Text>
</Animated.View>
);
}

export function CustomLayoutAnimationScreen2(): React.ReactElement {
const [state, setState] = useState(true);
return (
<View style={{ marginTop: 30 }}>
<View style={{ height: 300 }}>
<AnimatedLayout
style={{ flexDirection: state ? 'row' : 'column', borderWidth: 1 }}>
<Box key="a" label="A" state={state} />
<Box key="b" label="B" state={state} />
<Box key="c" label="C" state={state} />
</AnimatedLayout>
</View>

<Button
onPress={() => {
setState(!state);
}}
title="toggle"
/>
</View>
);
}

const styles = StyleSheet.create({
box: {
margin: 20,
padding: 5,
borderWidth: 1,
borderColor: 'black',
width: 60,
height: 60,
},
});



90 changes: 90 additions & 0 deletions Example/test/CustomLayout3.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { selectAssetSource } from 'expo-asset/build/AssetSources';
import React, { useState } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import Animated, {
AnimatedLayout,
makeMutable,
withTiming,
withDelay,
SlideInDown,
} from 'react-native-reanimated';

function CustomLayoutTransiton() {
const isEven = makeMutable(1);
return (values) => {
'worklet';
const isEvenLocal = isEven.value;
isEven.value = 1 - isEven.value;

return {
animations: {
originX: withDelay(
isEvenLocal ? 1000 : 0,
withTiming(values.originX, { duration: 1000 })
),
originY: withDelay(
isEvenLocal ? 0 : 1000,
withTiming(values.originY, { duration: 1000 })
),
width: withTiming(values.width, { duration: 1000 }),
height: withTiming(values.height, { duration: 1000 }),
},
initialValues: {
originX: values.boriginX,
originY: values.boriginY,
width: values.bwidth,
height: values.bheight,
},
};
};
}

function Box({ label, state }: { label: string; state: boolean }) {
const ind = label.charCodeAt(0) - 'A'.charCodeAt(0);
const delay = 300 * ind;
return (
<Animated.View
layout={CustomLayoutTransiton()}
entering={SlideInDown.delay(delay).duration(3000)}
style={[styles.box, { flexDirection: state ? 'row': 'row-reverse' }]}>
<Text> {label} </Text>
</Animated.View>
);
}

export function CustomLayoutAnimationScreen3(): React.ReactElement {
const [state, setState] = useState(true);
return (
<View style={{ marginTop: 30 }}>
<View style={{ height: 300 }}>
<AnimatedLayout
style={{ flexDirection: 'row', borderWidth: 1 }}>
<Box key="a" label="A" state={state} />
<Box key="b" label="B" state={state} />
<Box key="c" label="C" state={state} />
</AnimatedLayout>
</View>

<Button
onPress={() => {
setState(!state);
}}
title="toggle"
/>
</View>
);
}

const styles = StyleSheet.create({
box: {
margin: 20,
padding: 5,
borderWidth: 1,
borderColor: 'black',
width: 60,
height: 60,
},
});



10 changes: 10 additions & 0 deletions Example/test/TestApp.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import AnimatedReactionTest from './AnimatedReactionTest';
import AnimationsTest from './Animations';
import UpdatePropsTest from './UpdatePropsTest';
import AdaptersTest from './AdaptersTest';
import CustomLayout2 from './CustomLayout2';
import CustomLayout3 from './CustomLayout3';

LogBox.ignoreLogs(['Calling `getNode()`']);

Expand Down Expand Up @@ -52,6 +54,14 @@ const SCREENS = {
screen: AdaptersTest,
title: '🆕 Adapters',
},
CustomLayout2: {
screen: CustomLayout2,
title: '🆕 Custom Layout - switch to layout animation',
},
CustomLayout3: {
screen: CustomLayout3,
title: '🆕 Custom Layout - stay with entering animation ',
},
};

function MainScreen({ navigation }) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;

public class AnimationsManager {

private final static String[] LAYOUT_KEYS = { Snapshooter.originX, Snapshooter.originY, Snapshooter.width, Snapshooter.height };
private ReactContext mContext;
private UIImplementation mUIImplementation;
private UIManagerModule mUIManager;
Expand Down Expand Up @@ -318,16 +319,33 @@ public void notifyAboutSnapshots(Snapshooter before, Snapshooter after) {
HashMap<String, Object> targetValues = after.capturedValues.get(view.getId());
ViewState state = mStates.get(view.getId());

if (state == ViewState.Appearing || state == ViewState.Disappearing || state == ViewState.ToRemove) {
if (state == ViewState.Appearing && startValues != null && targetValues == null) {
mStates.put(tag, ViewState.Disappearing);
type = "exiting";
HashMap<String, Float> preparedValues = prepareDataForAnimationWorklet(startValues);
mNativeMethodsHolder.startAnimationForTag(tag, type, preparedValues);
}
if (state == ViewState.Disappearing || state == ViewState.ToRemove) {
continue;
}
if (state == ViewState.Appearing && startValues != null && targetValues == null) {
mStates.put(tag, ViewState.Disappearing);
type = "exiting";
HashMap<String, Float> preparedValues = prepareDataForAnimationWorklet(startValues);
mNativeMethodsHolder.startAnimationForTag(tag, type, preparedValues);
continue;
}

// If startValues are equal to targetValues it means that there was no UI Operation changing
// layout of the View. So dirtiness of that View is false positive
if (state == ViewState.Appearing) {
boolean doNotStartLayout = true;
for (String key : LAYOUT_KEYS) {
double startV = ((Number) startValues.get(key)).doubleValue();
double targetV = ((Number) targetValues.get(key)).doubleValue();
if (startV != targetV) {
doNotStartLayout = false;
}
}
if (doNotStartLayout) {
continue;
}
}

if (state == ViewState.Inactive) { // it can be a fresh view
if (startValues == null && targetValues != null) {
HashMap<String, Float> preparedValues = prepareDataForAnimationWorklet(targetValues);
Expand Down

0 comments on commit 4d85ca1

Please sign in to comment.