Skip to content

Feat/add transition animator #1479

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
Sep 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8c84624
Add TransitionAnimator
M-i-k-e-l Aug 18, 2021
73f1b0c
Merge branch 'master' into feat/add-transition-animator
M-i-k-e-l Aug 18, 2021
92d0ce9
Better version and a screen
M-i-k-e-l Aug 18, 2021
b168c11
Types
M-i-k-e-l Aug 18, 2021
b6b97ac
Merge branch 'master' into feat/add-transition-animator
M-i-k-e-l Aug 30, 2021
a2f5dff
Move const and rename props
M-i-k-e-l Aug 30, 2021
a15d73c
Remove unused code
M-i-k-e-l Aug 30, 2021
e8491b4
Switch to array.includes
M-i-k-e-l Aug 30, 2021
7b9b230
Remove animationEnded
M-i-k-e-l Aug 30, 2021
cfac59b
Refactor - move and rename
M-i-k-e-l Aug 30, 2021
1be1710
Merge branch 'master' into feat/add-transition-animator
M-i-k-e-l Sep 1, 2021
a5bd27f
Merge branch 'master' into feat/add-transition-animator
M-i-k-e-l Sep 9, 2021
7dd386d
Merge branch 'master' into feat/add-transition-animator
M-i-k-e-l Sep 12, 2021
18e4114
Rename HiddenLocation and Direction
M-i-k-e-l Sep 12, 2021
b17897f
Rename HiddenLocation (2)
M-i-k-e-l Sep 12, 2021
15b55eb
Add useAnimatedTransition
M-i-k-e-l Sep 12, 2021
756617f
Rename to useAnimatedTranslator
M-i-k-e-l Sep 12, 2021
d035fba
Add useAnimationEndNotifier
M-i-k-e-l Sep 12, 2021
98f51cc
Add useAnimatedTransition
M-i-k-e-l Sep 12, 2021
94a1c5d
Change TransitionAnimator to TransitionView
M-i-k-e-l Sep 14, 2021
cd9d873
Merge branch 'master' into feat/add-transition-animator
M-i-k-e-l Sep 14, 2021
7ce8b61
Merge branch 'master' into feat/add-transition-animator
M-i-k-e-l Sep 19, 2021
a95d03b
Merge branch 'master' into feat/add-transition-animator
M-i-k-e-l Sep 23, 2021
b7762c1
Change order of params
M-i-k-e-l Sep 23, 2021
bee16a3
TransitionAnimationEndType to TransitionViewAnimationType
M-i-k-e-l Sep 23, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions demo/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,9 @@ module.exports = {
get PanViewScreen() {
return require('./screens/incubatorScreens/PanViewScreen').default;
},
get TransitionViewScreen() {
return require('./screens/incubatorScreens/TransitionViewScreen').default;
},
// realExamples
get AppleMusic() {
return require('./screens/realExamples/AppleMusic').default;
Expand Down
3 changes: 2 additions & 1 deletion demo/src/screens/MenuStructure.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@ export const navigationData = {
{title: '(New) TextField', tags: 'text field input', screen: 'unicorn.components.IncubatorTextFieldScreen'},
{title: 'ExpandableOverlay', tags: 'text field expandable input picker', screen: 'unicorn.components.IncubatorExpandableOverlayScreen'},
{title: 'WheelPicker (Incubator)', tags: 'wheel picker spinner experimental', screen: 'unicorn.incubator.WheelPickerScreen'},
{title: 'Pan View', tags: 'pan swipe drag', screen: 'unicorn.incubator.PanViewScreen'}
{title: 'Pan View', tags: 'pan swipe drag', screen: 'unicorn.incubator.PanViewScreen'},
{title: 'Transition View', tags: 'transition animation enter exit', screen: 'unicorn.incubator.TransitionViewScreen'}
]
},
Inspirations: {
Expand Down
59 changes: 59 additions & 0 deletions demo/src/screens/incubatorScreens/TransitionViewScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React, {Component} from 'react';
import {View, Button, Incubator} from 'react-native-ui-lib';
const {TransitionView} = Incubator;
// @ts-ignore
import {renderRadioGroup} from '../ExampleScreenPresenter';

interface State {
enterDirection: Incubator.Direction;
exitDirection: Incubator.Direction;
key: number;
}

export default class TransitionViewScreen extends Component<{}, State> {
private ref = React.createRef<typeof TransitionView>();
state = {
enterDirection: 'left' as Incubator.Direction,
exitDirection: 'bottom' as Incubator.Direction,
key: 1
};

onPress = () => {
this.ref.current?.animateOut();
};

// onAnimationEnd = (type: Incubator.TransitionViewAnimationType) => {
// console.warn('Animation complete', type);
// };

render() {
const {key, enterDirection, exitDirection} = this.state;
return (
<View padding-20 bg-grey80 flex>
{renderRadioGroup.call(this,
'Enter direction',
'enterDirection',
{top: 'top', bottom: 'bottom', left: 'left', right: 'right'},
{isRow: true})}
{renderRadioGroup.call(this,
'Exit direction',
'exitDirection',
{top: 'top', bottom: 'bottom', left: 'left', right: 'right'},
{isRow: true})}
<Button label="Refresh" onPress={() => this.setState({key: key + 1})}/>
<View flex center>
<TransitionView
key={`${key}`}
// @ts-expect-error
ref={this.ref}
enterFrom={enterDirection}
exitTo={exitDirection}
// onAnimationEnd={this.onAnimationEnd}
>
<Button label="Press to remove" onPress={this.onPress}/>
</TransitionView>
</View>
</View>
);
}
}
3 changes: 2 additions & 1 deletion demo/src/screens/incubatorScreens/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export function registerScreens(registrar) {
gestureHandlerRootHOC(require('./TouchableOpacityScreen').default));
registrar('unicorn.components.IncubatorExpandableOverlayScreen', () => require('./IncubatorExpandableOverlayScreen').default);
registrar('unicorn.components.IncubatorTextFieldScreen', () => require('./IncubatorTextFieldScreen').default);
registrar('unicorn.incubator.WheelPickerScreen', () => gestureHandlerRootHOC(require('./WheelPickerScreen').default));
registrar('unicorn.incubator.PanViewScreen', () => require('./PanViewScreen').default);
registrar('unicorn.incubator.TransitionViewScreen', () => require('./TransitionViewScreen').default);
registrar('unicorn.incubator.WheelPickerScreen', () => gestureHandlerRootHOC(require('./WheelPickerScreen').default));
}
14 changes: 14 additions & 0 deletions generatedTypes/src/incubator/TransitionView/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React, { PropsWithChildren } from 'react';
import { ViewProps } from '../../components/view';
import { ForwardRefInjectedProps } from '../../commons/new';
import { Direction } from './useHiddenLocation';
import { TransitionViewAnimationType } from './useAnimationEndNotifier';
import { AnimatedTransitionProps } from './useAnimatedTransition';
export { Direction, TransitionViewAnimationType };
export declare type TransitionViewProps = AnimatedTransitionProps & ViewProps;
declare type Props = PropsWithChildren<TransitionViewProps> & ForwardRefInjectedProps;
interface Statics {
animateOut: () => void;
}
declare const _default: React.ComponentType<Props> & Statics;
export default _default;
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Direction, HiddenLocation } from './useHiddenLocation';
import { AnimationNotifierEndProps } from './useAnimationEndNotifier';
export interface AnimatedTransitionProps extends AnimationNotifierEndProps {
/**
* If this is given there will be an enter animation from this direction.
*/
enterFrom?: Direction;
/**
* If this is given there will be an exit animation to this direction.
*/
exitTo?: Direction;
}
declare type Props = AnimatedTransitionProps & {
hiddenLocation: HiddenLocation;
};
export default function useAnimatedTransition(props: Props): {
exit: () => void;
animatedStyle: {
transform: ({
translateX: number;
translateY?: undefined;
} | {
translateY: number;
translateX?: undefined;
})[];
opacity: number;
};
};
export {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Direction } from './useHiddenLocation';
export interface TranslatorProps {
initialVisibility: boolean;
}
export default function useAnimatedTranslator(props: TranslatorProps): {
init: (to: {
x: number;
y: number;
}, animationDirection: Direction, callback: (isFinished: boolean) => void) => void;
animate: (to: {
x: number;
y: number;
}, animationDirection: Direction, callback: (isFinished: boolean) => void) => void;
animatedStyle: {
transform: ({
translateX: number;
translateY?: undefined;
} | {
translateY: number;
translateX?: undefined;
})[];
opacity: number;
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export declare type TransitionViewAnimationType = 'enter' | 'exit';
export interface AnimationNotifierEndProps {
/**
* Callback to the animation end.
*/
onAnimationEnd?: (animationType: TransitionViewAnimationType) => void;
}
export default function useAnimationEndNotifier(props: AnimationNotifierEndProps): {
onEnterAnimationEnd: (isFinished: boolean) => void;
onExitAnimationEnd: (isFinished: boolean) => void;
};
17 changes: 17 additions & 0 deletions generatedTypes/src/incubator/TransitionView/useHiddenLocation.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { RefObject } from 'react';
import { View, LayoutChangeEvent } from 'react-native';
export declare type Direction = 'top' | 'bottom' | 'left' | 'right';
export interface HiddenLocation {
isDefault: boolean;
top: number;
bottom: number;
left: number;
right: number;
}
export interface HiddenLocationProps<T extends View> {
containerRef: RefObject<T>;
}
export default function useHiddenLocation<T extends View>(props: HiddenLocationProps<T>): {
onLayout: (event: LayoutChangeEvent) => void;
hiddenLocation: HiddenLocation;
};
1 change: 1 addition & 0 deletions generatedTypes/src/incubator/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export { default as TouchableOpacity, TouchableOpacityProps } from './TouchableO
export { default as TouchableOpacity2 } from './TouchableOpacity2';
export { default as WheelPicker, WheelPickerProps } from './WheelPicker';
export { default as PanView, PanViewProps, PanViewDirections, PanViewDismissThreshold } from './panView';
export { default as TransitionView, TransitionViewProps, Direction, TransitionViewAnimationType } from './TransitionView';
49 changes: 49 additions & 0 deletions src/incubator/TransitionView/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React, {PropsWithChildren, useCallback, useImperativeHandle} from 'react';
import {View as RNView, LayoutChangeEvent} from 'react-native';
import Animated from 'react-native-reanimated';
import View, {ViewProps} from '../../components/view';
import {forwardRef, ForwardRefInjectedProps} from '../../commons/new';
import useHiddenLocation, {Direction} from './useHiddenLocation';
import {TransitionViewAnimationType} from './useAnimationEndNotifier';
import useAnimatedTransition, {AnimatedTransitionProps} from './useAnimatedTransition';
const AnimatedView = Animated.createAnimatedComponent(View);
export {Direction, TransitionViewAnimationType};

// TODO: might need to create a file for types and create a fake component for docs
export type TransitionViewProps = AnimatedTransitionProps & ViewProps;

type Props = PropsWithChildren<TransitionViewProps> & ForwardRefInjectedProps;
interface Statics {
animateOut: () => void;
}

const TransitionView = (props: Props) => {
const {
onAnimationEnd,
enterFrom,
exitTo,
forwardedRef,
style: propsStyle,
onLayout: propsOnLayout,
...others
} = props;
const containerRef = React.createRef<RNView>();
const {onLayout: hiddenLocationOnLayout, hiddenLocation} = useHiddenLocation({containerRef});
const {exit, animatedStyle} = useAnimatedTransition({hiddenLocation, enterFrom, exitTo, onAnimationEnd});

useImperativeHandle(forwardedRef,
() => ({
animateOut: exit // TODO: should this be renamed as well?
}),
[exit]);

const onLayout = useCallback((event: LayoutChangeEvent) => {
hiddenLocationOnLayout(event);
propsOnLayout?.(event);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return <AnimatedView {...others} onLayout={onLayout} style={[propsStyle, animatedStyle]} ref={containerRef}/>;
};

export default forwardRef<Props, Statics>(TransitionView);
57 changes: 57 additions & 0 deletions src/incubator/TransitionView/useAnimatedTransition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {useEffect, useCallback} from 'react';
import {Direction, HiddenLocation} from './useHiddenLocation';
import useAnimatedTranslator from './useAnimatedTranslator';
import useAnimationEndNotifier, {AnimationNotifierEndProps} from './useAnimationEndNotifier';

export interface AnimatedTransitionProps extends AnimationNotifierEndProps {
/**
* If this is given there will be an enter animation from this direction.
*/
enterFrom?: Direction;
/**
* If this is given there will be an exit animation to this direction.
*/
exitTo?: Direction;
}

type Props = AnimatedTransitionProps & {
hiddenLocation: HiddenLocation;
};

export default function useAnimatedTransition(props: Props) {
const {hiddenLocation, enterFrom, exitTo, onAnimationEnd} = props;

const {init, animate, animatedStyle} = useAnimatedTranslator({initialVisibility: !enterFrom});
const {onEnterAnimationEnd, onExitAnimationEnd} = useAnimationEndNotifier({onAnimationEnd});

const getLocation = (direction?: Direction) => {
return {
x: direction && ['left', 'right'].includes(direction) ? hiddenLocation[direction] : 0,
y: direction && ['top', 'bottom'].includes(direction) ? hiddenLocation[direction] : 0
};
};

useEffect(() => {
if (!hiddenLocation.isDefault && enterFrom) {
const location = getLocation(enterFrom);
init(location, enterFrom, enter);
}
}, [hiddenLocation.isDefault]);

const enter = useCallback(() => {
'worklet';
if (enterFrom) {
animate({x: 0, y: 0}, enterFrom, onEnterAnimationEnd);
}
}, [onEnterAnimationEnd]);

const exit = useCallback(() => {
'worklet';
if (exitTo) {
animate(getLocation(exitTo), exitTo, onExitAnimationEnd);
}
}, [hiddenLocation, exitTo, onExitAnimationEnd]);

return {exit, animatedStyle};
}
58 changes: 58 additions & 0 deletions src/incubator/TransitionView/useAnimatedTranslator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {useCallback} from 'react';
import {useSharedValue, useAnimatedStyle, withSpring, withTiming} from 'react-native-reanimated';
import {Direction} from './useHiddenLocation';

export interface TranslatorProps {
initialVisibility: boolean;
}

const DEFAULT_ANIMATION_VELOCITY = 300;
const DEFAULT_ANIMATION_CONFIG = {velocity: DEFAULT_ANIMATION_VELOCITY, damping: 18, stiffness: 300, mass: 0.4};

export default function useAnimatedTranslator(props: TranslatorProps) {
const {initialVisibility} = props;

// Has to start at {0, 0} with {opacity: 0} so layout can be measured
const translateX = useSharedValue<number>(0);
const translateY = useSharedValue<number>(0);

const visible = useSharedValue<boolean>(initialVisibility);

const init = useCallback((to: {x: number; y: number},
animationDirection: Direction,
callback: (isFinished: boolean) => void) => {
'worklet';
if (['left', 'right'].includes(animationDirection)) {
translateX.value = withTiming(to.x, {duration: 0}, callback);
} else if (['top', 'bottom'].includes(animationDirection)) {
translateY.value = withTiming(to.y, {duration: 0}, callback);
}

visible.value = true;
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]);

const animate = useCallback((to: {x: number; y: number},
animationDirection: Direction,
callback: (isFinished: boolean) => void) => {
'worklet';
if (['left', 'right'].includes(animationDirection)) {
translateX.value = withSpring(to.x, DEFAULT_ANIMATION_CONFIG, callback);
} else if (['top', 'bottom'].includes(animationDirection)) {
translateY.value = withSpring(to.y, DEFAULT_ANIMATION_CONFIG, callback);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]);

const animatedStyle = useAnimatedStyle(() => {
return {
transform: [{translateX: translateX.value}, {translateY: translateY.value}],
// TODO: do we want to take the component's opacity here? - I think combining opacities is buggy
opacity: Number(visible.value)
};
}, []);

return {init, animate, animatedStyle};
}
33 changes: 33 additions & 0 deletions src/incubator/TransitionView/useAnimationEndNotifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {useCallback} from 'react';
import {runOnJS} from 'react-native-reanimated';

export type TransitionViewAnimationType = 'enter' | 'exit';

export interface AnimationNotifierEndProps {
/**
* Callback to the animation end.
*/
onAnimationEnd?: (animationType: TransitionViewAnimationType) => void;
}

export default function useAnimationEndNotifier(props: AnimationNotifierEndProps) {
const {onAnimationEnd} = props;

const onEnterAnimationEnd = useCallback((isFinished: boolean) => {
'worklet';
if (onAnimationEnd && isFinished) {
runOnJS(onAnimationEnd)('enter');
}
},
[onAnimationEnd]);

const onExitAnimationEnd = useCallback((isFinished: boolean) => {
'worklet';
if (onAnimationEnd && isFinished) {
runOnJS(onAnimationEnd)('exit');
}
},
[onAnimationEnd]);

return {onEnterAnimationEnd, onExitAnimationEnd};
}
Loading