From 5d25053c43763b232c16964580f2b32bef4d3a10 Mon Sep 17 00:00:00 2001 From: "satyajit.happy" Date: Tue, 21 May 2019 13:28:21 +0200 Subject: [PATCH] Add a simple animated header --- src/components/Card.tsx | 4 +- src/components/Header/HeaderAnimated.tsx | 93 +++++++++++++++++ src/components/Header/HeaderAnimatedItem.tsx | 104 +++++++++++++++++++ src/components/Header/HeaderBackButton.tsx | 3 +- src/components/Header/HeaderSimple.tsx | 14 ++- src/components/Header/HeaderTitle.tsx | 10 +- src/components/Stack.tsx | 97 +++++++++-------- 7 files changed, 271 insertions(+), 54 deletions(-) create mode 100644 src/components/Header/HeaderAnimated.tsx create mode 100644 src/components/Header/HeaderAnimatedItem.tsx diff --git a/src/components/Card.tsx b/src/components/Card.tsx index 72284f0..f3e08bf 100755 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -387,7 +387,7 @@ export default class Card extends React.Component { const styles = StyleSheet.create({ container: { - flex: 1 + flex: 1, }, card: { ...StyleSheet.absoluteFillObject, @@ -395,7 +395,7 @@ const styles = StyleSheet.create({ shadowRadius: 5, shadowColor: '#000', backgroundColor: 'white', - elevation: 2 + elevation: 2, }, overlay: { ...StyleSheet.absoluteFillObject, diff --git a/src/components/Header/HeaderAnimated.tsx b/src/components/Header/HeaderAnimated.tsx new file mode 100644 index 0000000..ebc2731 --- /dev/null +++ b/src/components/Header/HeaderAnimated.tsx @@ -0,0 +1,93 @@ +import * as React from 'react'; +import { + View, + StyleSheet, + Platform, + StatusBar, + StyleProp, + ViewStyle, +} from 'react-native'; +import Animated from 'react-native-reanimated'; +import HeaderSheet from './HeaderSheet'; +import { Route, Layout } from '../Stack'; +import HeaderAnimatedItem, { + HeaderAnimationPreset, + InterpolationProps, +} from './HeaderAnimatedItem'; + +type Scene = { + title: string; + route: T; + progress: Animated.Node; +}; + +type Props = { + layout: Layout; + onGoBack: (props: { route: T }) => void; + preset: HeaderAnimationPreset; + scenes: Scene[]; + style?: StyleProp; +}; + +const { interpolate, multiply } = Animated; + +const FadePreset: HeaderAnimationPreset = { + styleInterpolator: ({ current, next }: InterpolationProps) => { + const progress = next + ? multiply( + current, + interpolate(next, { + inputRange: [0, 1], + outputRange: [1, 0], + }) + ) + : current; + + return { + leftButtonStyle: { opacity: progress }, + titleStyle: { opacity: progress }, + }; + }, +}; + +export default class HeaderAnimated extends React.Component< + Props +> { + static defaultProps = { + preset: FadePreset, + }; + + render() { + const { preset, scenes, layout, onGoBack } = this.props; + + return ( + + + {scenes.map((scene, i, self) => { + const previous = self[i - 1]; + const next = self[i + 1]; + + return ( + onGoBack({ route: scene.route })} + /> + ); + })} + + + ); + } +} + +const styles = StyleSheet.create({ + container: { + height: Platform.OS === 'ios' ? 44 : 56, + marginTop: Platform.OS === 'ios' ? 20 : StatusBar.currentHeight, + }, +}); diff --git a/src/components/Header/HeaderAnimatedItem.tsx b/src/components/Header/HeaderAnimatedItem.tsx new file mode 100644 index 0000000..3dfe7f8 --- /dev/null +++ b/src/components/Header/HeaderAnimatedItem.tsx @@ -0,0 +1,104 @@ +import * as React from 'react'; +import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native'; +import Animated from 'react-native-reanimated'; +import HeaderTitle from './HeaderTitle'; +import { Route, Layout } from '../Stack'; +import HeaderBackButton from './HeaderBackButton'; +import memoize from '../../utils/memoize'; + +export type InterpolationProps = { + current: Animated.Node; + next?: Animated.Node; + layout: Layout; +}; + +export type StyleInterpolator = ( + props: InterpolationProps +) => { + leftButtonStyle: any; + titleStyle: any; +}; + +export type HeaderAnimationPreset = { + styleInterpolator: StyleInterpolator; +}; + +export type Scene = { + title: string; + route: T; + progress: Animated.Node; +}; + +type Props = { + layout: Layout; + onGoBack: () => void; + preset: HeaderAnimationPreset; + scene: Scene; + previous?: Scene; + next?: Scene; + style?: StyleProp; +}; + +export default class HeaderAnimatedItem< + T extends Route +> extends React.Component> { + private getInterpolatedStyle = memoize( + ( + styleInterpolator: StyleInterpolator, + layout: Layout, + current: Animated.Node, + next?: Animated.Node + ) => styleInterpolator({ current, next, layout }) + ); + + render() { + const { + scene, + previous, + next, + preset, + layout, + onGoBack, + style, + } = this.props; + + const { titleStyle, leftButtonStyle } = this.getInterpolatedStyle( + preset.styleInterpolator, + layout, + scene.progress, + next ? next.progress : undefined + ); + + return ( + + {previous ? ( + + + + ) : null} + + {scene.title} + + + ); + } +} + +const styles = StyleSheet.create({ + content: { + ...StyleSheet.absoluteFillObject, + paddingHorizontal: 4, + flexDirection: 'row', + alignItems: 'center', + }, + left: { + position: 'absolute', + left: 0, + top: 0, + bottom: 0, + justifyContent: 'center', + }, + title: { + marginHorizontal: 48, + }, +}); diff --git a/src/components/Header/HeaderBackButton.tsx b/src/components/Header/HeaderBackButton.tsx index 8031907..c7a6cce 100644 --- a/src/components/Header/HeaderBackButton.tsx +++ b/src/components/Header/HeaderBackButton.tsx @@ -41,6 +41,7 @@ class HeaderBackButton extends React.Component { ios: '#037aff', web: '#5f6368', }), + backTitleVisible: Platform.OS === 'ios', truncatedTitle: 'Back', }; @@ -163,7 +164,7 @@ const styles = StyleSheet.create({ width: 36, margin: 6, alignItems: 'center', - justifyContent: 'center' + justifyContent: 'center', }, container: { alignItems: 'center', diff --git a/src/components/Header/HeaderSimple.tsx b/src/components/Header/HeaderSimple.tsx index 6583e3c..b81ba09 100644 --- a/src/components/Header/HeaderSimple.tsx +++ b/src/components/Header/HeaderSimple.tsx @@ -1,5 +1,12 @@ import * as React from 'react'; -import { View, StyleSheet, Platform, StatusBar } from 'react-native'; +import { + View, + StyleSheet, + Platform, + StatusBar, + StyleProp, + ViewStyle, +} from 'react-native'; import HeaderBackButton from './HeaderBackButton'; import HeaderTitle from './HeaderTitle'; import HeaderSheet from './HeaderSheet'; @@ -7,11 +14,12 @@ import HeaderSheet from './HeaderSheet'; type Props = { title: string; onGoBack?: () => void; + style?: StyleProp; }; -export default function HeaderAndroid({ title, onGoBack }: Props) { +export default function HeaderAndroid({ title, onGoBack, style }: Props) { return ( - + {onGoBack ? : null} {title} diff --git a/src/components/Header/HeaderTitle.tsx b/src/components/Header/HeaderTitle.tsx index dce5008..a542b3f 100644 --- a/src/components/Header/HeaderTitle.tsx +++ b/src/components/Header/HeaderTitle.tsx @@ -1,13 +1,16 @@ import * as React from 'react'; -import { Text, StyleSheet, Platform, StyleProp, TextStyle } from 'react-native'; +import { StyleSheet, Platform } from 'react-native'; +import Animated from 'react-native-reanimated'; type Props = { children: string; - style?: StyleProp; + style?: React.ComponentProps['style']; }; export default function HeaderTitle({ children, style }: Props) { - return {children}; + return ( + {children} + ); } const styles = StyleSheet.create({ @@ -16,6 +19,7 @@ const styles = StyleSheet.create({ marginHorizontal: 12, ...Platform.select({ ios: { + textAlign: 'center', fontSize: 17, fontWeight: '600', color: 'rgba(0, 0, 0, .9)', diff --git a/src/components/Stack.tsx b/src/components/Stack.tsx index 06e09b3..73c6a77 100755 --- a/src/components/Stack.tsx +++ b/src/components/Stack.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; import { View, StyleSheet, LayoutChangeEvent } from 'react-native'; import Animated from 'react-native-reanimated'; -import HeaderAndroid from './Header/HeaderSimple'; import Card from './Card'; import { SlideFromRightIOS } from '../TransitionConfigs/TransitionPresets'; +import HeaderAnimated from './Header/HeaderAnimated'; export type Route = { key: string }; @@ -79,51 +79,58 @@ export default class Stack extends React.Component< const { layout, progress } = this.state; return ( - - {routes.map((route, index, self) => { - const focused = index === self.length - 1; - const current = progress[route.key]; - const next = self[index + 1] - ? progress[self[index + 1].key] - : undefined; - - return ( - - onCloseRoute({ route })} - gesturesEnabled={index !== 0} - animationsEnabled={!initialRoutes.includes(route.key)} - {...SlideFromRightIOS} + + ({ + route, + progress: progress[route.key], + title: `Screen ${i}`, + }))} + onGoBack={onGoBack} + /> + + {routes.map((route, index, self) => { + const focused = index === self.length - 1; + const current = progress[route.key]; + const next = self[index + 1] + ? progress[self[index + 1].key] + : undefined; + + return ( + - onGoBack({ route }) : undefined} - /> - {renderScene({ - route, - index, - })} - - - ); - })} - + onCloseRoute({ route })} + gesturesEnabled={index !== 0} + animationsEnabled={!initialRoutes.includes(route.key)} + {...SlideFromRightIOS} + > + {renderScene({ + route, + index, + })} + + + ); + })} + + ); } }