Skip to content
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

Shared Element Transition example #1

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
25 changes: 21 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
https://user-images.githubusercontent.com/4402166/134559538-a2801d52-7b8a-44a6-b12a-0cc1eafbb82b.MP4


https://user-images.githubusercontent.com/4402166/134838456-69181346-93bc-4e25-8dfa-f629ed58c813.MOV


This starter is a collection of libraries and approaches from my personal experience. No hard judgements ✌️

For more information, check out [Why](#why) section.
Expand Down Expand Up @@ -55,7 +59,8 @@ If you need to rename the app, do the following (based on [react-native-rename](

## What's inside

- [React Navigation (v6)](https://github.com/react-navigation/react-navigation) - routing and navigation for React Native apps. If you'd like to use [React Native Navigation](https://github.com/wix/react-native-navigation) by Wix, check out [rnn-starter](https://github.com/kanzitelli/rnn-starter).
- [React Navigation (v6)](https://github.com/react-navigation/react-navigation) - routing and navigation for React Native apps (with [Native Stack](https://reactnavigation.org/docs/native-stack-navigator/)). If you'd like to use [React Native Navigation](https://github.com/wix/react-native-navigation) by Wix, check out [rnn-starter](https://github.com/kanzitelli/rnn-starter).
- [React Native Screens](https://github.com/software-mansion/react-native-screens) - Native navigation primitives for React Native app
- [RN UI lib](https://github.com/wix/react-native-ui-lib) - amazing Design System, UI toolset & components library for React Native. Dark Mode is implemented using this library.
- [Reanimated 2](https://github.com/software-mansion/react-native-reanimated) - React Native's Animated library reimplemented.
- [MobX](https://github.com/mobxjs/mobx) - simple, scalable state management, with [mobx-persist-store](https://github.com/quarrant/mobx-persist-store) for persisting your stores.
Expand Down Expand Up @@ -145,6 +150,18 @@ const Screen = ({componentId}) => {
#### Samples for new screens, services, stores and components.

So you have one structure within the project. You can find them in corresponding folders. Just copy&paste it and make the necessary changes.
Future Feature: I think, it would be good to have it automatically generated by [cli-rn](https://github.com/kanzitelli/cli-rn), so won't be need even for copy&paste.

```bash
# Future feature
> cli-rn generate (screen|service|store|component) --name SName # maybe something else
> cli-rn g (sc|se|st|co) -n SName # short version
```

#### [WIP] Shared Element Transition (repo - [IjzerenHein/react-navigation-shared-element](https://github.com/IjzerenHein/react-navigation-shared-element))

It doesn’t use native api, it just [createSharedElementStackNavigator()](https://github.com/kanzitelli/rn-starter/blob/feature/shared-transition/src/services/navigation/help.tsx#L34) with [two screens](https://github.com/kanzitelli/rn-starter/blob/feature/shared-transition/src/screens/index.ts#L67) and [sets this Navigator](https://github.com/kanzitelli/rn-starter/blob/feature/shared-transition/src/screens/index.ts#L67) as a React Navigation’s native stack’s screen.
It was created as an experiment to combine great performance of [native screens](https://github.com/software-mansion/react-native-screens) and some fancy shared transitions by [IjzerenHein/react-navigation-shared-element](https://github.com/IjzerenHein/react-navigation-shared-element).

## Enhancements

Expand All @@ -155,13 +172,13 @@ There are still some things I would like to add to the starter:
- [x] Passing props to a screen example
- [x] Constants: add Dimensions
- [x] MMKV (AsyncStorage) stores persisting example
- [ ] Shared transitions — [IjzerenHein/react-navigation-shared-element](https://github.com/IjzerenHein/react-navigation-shared-element)
- [ WIP ] Shared transitions — [IjzerenHein/react-navigation-shared-element](https://github.com/IjzerenHein/react-navigation-shared-element)

#### Production

- [ ] Auth flow
- [ ] Fast Image — [DylanVann/react-native-fast-image](https://github.com/DylanVann/react-native-fast-image)
- [ ] Notifications — [wix/react-native-notifications](https://github.com/wix/react-native-notifications)
- [ ] Notifications — [wix/react-native-notifications](https://github.com/wix/react-native-notifications) or/and [invertase/notifee](https://github.com/invertase/notifee)
- [ ] E2E tests — [wix/Detox](https://github.com/wix/Detox)
- [ ] Permissions — [zoontek/react-native-permissions](https://github.com/zoontek/react-native-permissions)
- [ ] FB SDK — [thebergamo/react-native-fbsdk-next](https://github.com/thebergamo/react-native-fbsdk-next)
Expand All @@ -182,7 +199,7 @@ Feel free to open an issue for suggestions.
- Expo + React Native Navigation? Yes! - [Medium](https://kanzitelli.medium.com/expo-react-native-navigation-yes-ebda0cbfa4b1), [Dev.to](https://dev.to/kanzitelli/expo-react-native-navigation-1pll)
- cli-rn — making RN app developing experience as smooth as possible - [Medium](https://kanzitelli.medium.com/cli-rn-making-rn-app-developing-experience-as-smooth-as-possible-1022aae3a0d3), [Dev.to](https://dev.to/kanzitelli/cli-rn-making-rn-app-developing-experience-as-smooth-as-possible-4e98)

### Apps in production
### Apps in production (from [rnn-starter](https://github.com/kanzitelli/rnn-starter))

- Wallpapers App - [Twitter](https://twitter.com/kanzitelli/status/1408192827155177472?s=20), App Store soon
- Rabbit App. Lite Reddit client - [Github](https://github.com/kanzitelli/rabbit-app), [App Store](https://apps.apple.com/ru/app/rabbit-app-lite-reddit-client/id1535084154), [Google Play](https://play.google.com/store/apps/details?id=io.batyr.rabbitapp)
Expand Down
6 changes: 6 additions & 0 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,8 @@ PODS:
- RNScreens (3.7.2):
- React-Core
- React-RCTImage
- RNSharedElement (0.8.2):
- React
- RNVectorIcons (8.1.0):
- React-Core
- Yoga (1.14.0)
Expand Down Expand Up @@ -485,6 +487,7 @@ DEPENDENCIES:
- RNLocalize (from `../node_modules/react-native-localize`)
- RNReanimated (from `../node_modules/react-native-reanimated`)
- RNScreens (from `../node_modules/react-native-screens`)
- RNSharedElement (from `../node_modules/react-native-shared-element`)
- RNVectorIcons (from `../node_modules/react-native-vector-icons`)
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)

Expand Down Expand Up @@ -594,6 +597,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-reanimated"
RNScreens:
:path: "../node_modules/react-native-screens"
RNSharedElement:
:path: "../node_modules/react-native-shared-element"
RNVectorIcons:
:path: "../node_modules/react-native-vector-icons"
Yoga:
Expand Down Expand Up @@ -659,6 +664,7 @@ SPEC CHECKSUMS:
RNLocalize: 7f1e5792b65a839af55a9552d05b3558b66d017e
RNReanimated: ad24db8af24e3fe1b5c462785bc3db8d5baae2ee
RNScreens: 0591543e343c7444ea1756b6265d81a4295922c9
RNSharedElement: 87baf6d04da5b068f000dd8c953d28a1196999c0
RNVectorIcons: 31cebfcf94e8cf8686eb5303ae0357da64d7a5a4
Yoga: aa0cb45287ebe1004c02a13f279c55a95f1572f4
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@react-navigation/bottom-tabs": "^6.0.5",
"@react-navigation/native": "^6.0.2",
"@react-navigation/native-stack": "^6.1.0",
"@react-navigation/stack": "^6.0.7",
"date-fns": "^2.24.0",
"formik": "^2.2.9",
"i18n-js": "^3.8.0",
Expand All @@ -48,8 +49,10 @@
"react-native-restart": "^0.0.22",
"react-native-safe-area-context": "^3.3.2",
"react-native-screens": "^3.7.2",
"react-native-shared-element": "^0.8.2",
"react-native-ui-lib": "^6.0.0",
"react-native-vector-icons": "^8.1.0"
"react-native-vector-icons": "^8.1.0",
"react-navigation-shared-element": "^3.1.3"
},
"devDependencies": {
"@babel/core": "^7.12.9",
Expand Down
25 changes: 14 additions & 11 deletions src/components/reanimated2.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import {View, Text} from 'react-native-ui-lib';
import Animated, {withSpring, useAnimatedStyle, useSharedValue} from 'react-native-reanimated';
import {SharedElement} from 'react-navigation-shared-element';

import {Bounceable} from './bounceable';

Expand All @@ -20,16 +21,18 @@ export const Reanimated2: React.FC<Reanimated2Props> = ({stID}: Reanimated2Props
};

return (
<View padding-s1>
<Animated.View style={[animatedStyles]}>
<View center padding-s1>
<Bounceable onPress={moveObject} activeScale={0.9}>
<View center bg-primary padding-s8 br40>
<Text whitish>Bounceable</Text>
</View>
</Bounceable>
</View>
</Animated.View>
</View>
<SharedElement id={stID ?? ''}>
<View padding-s1>
<Animated.View style={[animatedStyles]}>
<View center padding-s1>
<Bounceable onPress={moveObject} activeScale={0.9}>
<View center bg-primary padding-s8 br40>
<Text whitish>Bounceable</Text>
</View>
</Bounceable>
</View>
</Animated.View>
</View>
</SharedElement>
);
};
68 changes: 63 additions & 5 deletions src/screens/index.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,44 @@
import {ModalScreenLayouts, ScreenLayouts, TabScreenLayouts} from '../services/navigation/types';
import {
ModalScreenLayouts,
ScreenLayouts,
SharedScreenLayouts,
SharedTransitionScreenLayouts,
TabScreenLayouts,
} from '../services/navigation/types';

import {Main} from './main';
import {Settings} from './settings';
import {Example} from './screen-sample';
import {genRootNavigator, genStackNavigator, genTabNavigator} from '../services/navigation/help';
import {
genRootNavigator,
genSharedStackNavigator,
genStackNavigator,
genTabNavigator,
} from '../services/navigation/help';
import {Shared} from './shared';
import {SharedTo} from './sharedTo';

// Describe your screens here
export type Tabs = 'Main' | 'WIP' | 'Settings';
export type Modal = 'ExampleModal';
export type SharedTransitionScreen = 'Shared' | 'SharedTo';
export type SharedScreen = 'ExampleShared';
export type Screen = 'Main' | 'Example' | 'Settings';

export type ModalProps = {
ExampleModal: undefined;
};
export type SharedScreenProps = {
ExampleShared: undefined;
};
export type ScreenProps = {
Main: undefined;
Example: ExampleScreenProps;
Settings: undefined;
} & ModalProps;
Shared: undefined;
SharedTo: undefined;
} & ModalProps &
SharedScreenProps;

// Screens
const screens: ScreenLayouts = {
Expand All @@ -43,8 +64,45 @@ const screens: ScreenLayouts = {
}),
},
};
const HomeStack = () => genStackNavigator([screens.Main, screens.Example]);
const ExampleStack = () => genStackNavigator([screens.Example]);
const sharedTransitionScreens: SharedTransitionScreenLayouts = {
Shared: {
name: 'Shared',
component: Shared,
options: () => ({
title: 'Shared Transition',
}),
},
SharedTo: {
name: 'SharedTo',
component: SharedTo,
options: () => ({
title: 'SharedTo Transition',
}),
sharedElements: (route, otherRoute, showing) => {
// const {item} = route.params;
// return [`item.${item.id}.photo`];
console.log('showing', showing);
return ['image', 'reanimated2'];
},
},
};
const ExampleSharedStack = () =>
genSharedStackNavigator([sharedTransitionScreens.Shared, sharedTransitionScreens.SharedTo]);
const sharedScreens: SharedScreenLayouts = {
ExampleShared: {
name: 'ExampleShared',
component: ExampleSharedStack,
options: () => ({
title: 'Shared Transition',
headerLargeTitle: false,
headerTransparent: false,
headerBlurEffect: undefined,
}),
},
};
const HomeStack = () =>
genStackNavigator([screens.Main, screens.Example, sharedScreens.ExampleShared]);
const ExampleStack = () => genStackNavigator([screens.Example, sharedScreens.ExampleShared]);
const SettingsStack = () => genStackNavigator([screens.Settings]);
const ExampleModalStack = () => genStackNavigator([screens.Main, screens.Example]);

Expand Down
4 changes: 2 additions & 2 deletions src/screens/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,12 @@ export const Main: React.FC = observer(({}) => {
<Button
marginV-s1
label={t.do('section.navigation.button.sharedTransition')}
onPress={() => Alert.alert('future feature: shared transition')}
onPress={() => nav.push('ExampleShared')}
/>
</Section>

<Section title="Reanimated 2">
<Reanimated2 stID="reanimated2" />
<Reanimated2 />
</Section>

<Section title="MobX">
Expand Down
2 changes: 1 addition & 1 deletion src/screens/screen-sample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const Example: React.FC<Props> = observer(({route}) => {
<Button
marginV-s1
label={t.do('section.navigation.button.sharedTransition')}
onPress={() => Alert.alert('future feature: shared transition')}
onPress={() => nav.push('ExampleShared')}
/>
</View>

Expand Down
66 changes: 66 additions & 0 deletions src/screens/shared.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React from 'react';
import {Alert, Image, ScrollView} from 'react-native';
import {View, Button, Text} from 'react-native-ui-lib';
import {observer} from 'mobx-react';
import {NativeStackScreenProps} from '@react-navigation/native-stack';

import {ScreenProps} from '.';
import {useServices} from '../services';
// import { useStores } from '../stores';
// import { useConstants } from '../utils/constants';

import {Section} from '../components/section';
import {randomNum} from '../utils/help';
import {Reanimated2} from '../components/reanimated2';
import {useNavigation} from '@react-navigation/core';
import {SharedElement} from 'react-navigation-shared-element';
import {Bounceable} from '../components/bounceable';

type Props = NativeStackScreenProps<ScreenProps, 'Shared'>;

export const Shared: React.FC<Props> = observer(({route}) => {
const {value} = route.params ?? {value: randomNum()};
const {nav, t} = useServices();
const navv = useNavigation();
// const {} = useStores();
// const {} = useConstants();

return (
<View flex bg-bgColor>
<ScrollView contentInsetAdjustmentBehavior="automatic">
{/* <Text textColor center text50R>
Shared screen
</Text> */}
<View padding-s4>
<Section title="Transition">
{/* <Reanimated2 stID="reanimated2" /> */}

<View marginV-s2>
<Bounceable onPress={() => navv.push('SharedTo')}>
<SharedElement id={'image'}>
<Image
source={{
uri: 'https://www.thesprucepets.com/thmb/BKNJkoyr-qyvfaYVRVCuEHNmOXU=/1155x1155/smart/filters:no_upscale()/Stocksy_txp14acff329Kw100_Medium_1360769-5aec7baefa6bcc00373c6cb7.jpg',
}}
style={{height: 120, width: 120, borderRadius: 120}}
/>
</SharedElement>
</Bounceable>
</View>

<View marginT-s8>
{/* <Button marginV-s1 label="Start transition" onPress={() => nav.push('SharedTo')} /> */}
<Button marginV-s1 label="Start transition" onPress={() => navv.push('SharedTo')} />
</View>
</Section>

{/* <Button marginV-s1 label={t.do('section.navigation.button.back')} onPress={navv.goBack} /> */}

{/* <Text textColor center>
localized with i18n-js
</Text> */}
</View>
</ScrollView>
</View>
);
});
Loading