Skip to content

Instantly share code, notes, and snippets.

@catalinmiron
Last active August 13, 2019 19:23
Show Gist options
  • Save catalinmiron/f1a115a59bfb9ebba23cc3d152d3cf6e to your computer and use it in GitHub Desktop.
Save catalinmiron/f1a115a59bfb9ebba23cc3d152d3cf6e to your computer and use it in GitHub Desktop.
Mars planet expo implementation
// Inspiration: https://dribbble.com/shots/6520262-Mars
// Planet image: https://pngimg.com/download/61156
import React from 'react';
import {
StatusBar,
ScrollView,
TouchableWithoutFeedback,
Dimensions,
Image,
View,
TouchableOpacity,
Text
} from 'react-native';
import { createAppContainer } from 'react-navigation';
import * as Animatable from 'react-native-animatable';
import { AppLoading } from 'expo';
import { Asset } from 'expo-asset';
import { Ionicons } from '@expo/vector-icons';
import { createFluidNavigator, Transition } from 'react-navigation-fluid-transitions';
const { width, height } = Dimensions.get('screen');
function cacheImages(images) {
return images.map(image => {
if (typeof image === 'string') {
return Image.prefetch(image);
} else {
return Asset.fromModule(image).downloadAsync();
}
});
}
myCustomTransitionFunction = transitionInfo => {
const { progress, start, end } = transitionInfo;
const opacity = progress.interpolate({
inputRange: [0, start, end, 1],
outputRange: [1, 1, 0, 0]
});
const translateX = progress.interpolate({
inputRange: [0, start, end, 1],
outputRange: [0, 0, 100, 100]
});
return { opacity, transform: [{ translateX }] };
};
myCustomTransitionFunction2 = transitionInfo => {
const { progress, start, end } = transitionInfo;
const opacity = progress.interpolate({
inputRange: [0, start, end, 1],
outputRange: [1, 1, 0, 0]
});
const translateX = progress.interpolate({
inputRange: [0, start, end, 1],
outputRange: [0, 0, -100, -100]
});
return { opacity, transform: [{ translateX }] };
};
const smallMars = 300;
const bigMars = 900;
const ratio = bigMars / smallMars;
const rotation = '60deg';
const dotSize = 14;
const coordinates = [...Array(10).keys()].map(() => {
const a = Math.random() * 2 * Math.PI;
const maxRadius = smallMars / 2 - dotSize * 2;
const r = maxRadius * Math.sqrt(Math.random());
// If you need it in Cartesian coordinates
const x = r * Math.cos(a) + smallMars / 2;
const y = r * Math.sin(a) + smallMars / 2;
return {
x,
y
};
});
const minutes = [...Array(10).keys()].map(i => Math.round(Math.random() * 40) + 10);
class Screen1 extends React.Component {
render() {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Animatable.View
useNativeDriver
animation="fadeInUp"
delay={500}
duration={1500}
style={{ position: 'absolute', alignItems: 'center', justifyContent: 'center' }}
>
<Transition shared="image">
<Image
source={require('./assets/mars_planet.png')}
style={{
width: height,
height: height,
resizeMode: 'contain',
transform: [{ rotate: '0deg' }],
position: 'absolute',
bottom: -height
}}
/>
</Transition>
</Animatable.View>
<View
style={{
position: 'absolute',
bottom: 50,
left: 0,
right: 0,
alignItems: 'center',
justifyContent: 'center'
}}
>
<Transition delay appear="left" disappear={myCustomTransitionFunction}>
<TouchableOpacity onPress={() => this.props.navigation.navigate('Screen2')}>
<Ionicons name="ios-arrow-round-forward" size={72} color="white" />
</TouchableOpacity>
</Transition>
</View>
</View>
);
}
}
class Screen2 extends React.Component {
willFocusSubscription = null;
backButton = null;
componentDidMount() {
this.willFocusSubscription = this.props.navigation.addListener('willFocus', payload => {
if (!this.backButton) {
return;
}
this.backButton.fadeInRight(500, 1000);
// this.backButton.transitionTo({ opacity: 1 }, 1000, Easing.inOut, 2000);
});
}
render() {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<View style={{ position: 'relative' }}>
<Transition shared="image">
<Image
source={require('./assets/mars_planet.png')}
style={{
width: smallMars,
height: smallMars,
resizeMode: 'contain',
transform: [{ rotate: rotation }]
}}
onLayout={e => {}}
/>
</Transition>
{coordinates.map(({ x, y }, i) => {
return (
<TouchableWithoutFeedback
onPress={async () => {
await this.backButton.fadeOutLeft(400);
this.props.navigation.navigate('Screen3', { coordinates: { x, y } });
}}
hitSlop={{ top: 20, left: 20, bottom: 20, right: 20 }}
style={{ position: 'absolute' }}
key={i}
>
<Animatable.View
animation="fadeIn"
duration={1000}
useNativeDriver
delay={1000 + i * 150}
style={{
position: 'absolute',
width: dotSize,
height: dotSize,
borderRadius: dotSize / 2,
backgroundColor: '#fff',
transform: [
{
translateX: x - dotSize / 2
},
{
translateY: y - dotSize / 2
}
]
}}
/>
</TouchableWithoutFeedback>
);
})}
</View>
<Animatable.View
useNativeDriver
ref={ref => (this.backButton = ref)}
animation="fadeInRight"
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
alignItems: 'flex-start',
padding: 20,
justifyContent: 'center'
}}
>
<TouchableOpacity
onPress={async () => {
await this.backButton.fadeOutLeft(400);
this.props.navigation.navigate('Screen1');
}}
>
<Ionicons name="ios-arrow-round-back" size={52} color="white" />
</TouchableOpacity>
{/* <TouchableOpacity onPress={async () => {await this.backButton.fadeOutDown(400); this.props.navigation.navigate("Screen3", {coordinates})}}>
<Ionicons name="ios-arrow-round-forward" size={52} color="white"/>
</TouchableOpacity> */}
</Animatable.View>
<ScrollView horizontal style={{ position: 'absolute', bottom: 0, left: 0 }}>
{minutes.map((time, i) => {
return (
<Animatable.View
key={i}
animation="fadeInRight"
duration={600}
useNativeDriver
delay={2000 + i * 150}
style={{
marginRight: 20,
borderWidth: 1,
borderColor: '#fff',
borderRadius: 10,
padding: 10,
height: 120,
width: 110,
alignItems: 'flex-end',
justifyContent: 'flex-end'
}}
>
<Text style={{ fontSize: 24, color: 'white', fontFamily: 'monospace', fontWeight: 'bold' }}>{time}m</Text>
</Animatable.View>
);
})}
</ScrollView>
</View>
);
}
}
class Screen3 extends React.Component {
render() {
const { x, y } = this.props.navigation.state.params.coordinates;
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<View
style={{
position: 'relative',
transform: [{ translateX: -x * ratio + bigMars / 2 }, { translateY: -y * ratio + bigMars / 2 }]
}}
>
<Transition shared="image" delay>
<Image
source={require('./assets/mars_planet.png')}
style={{
resizeMode: 'contain',
width: bigMars,
height: bigMars,
transform: [{ rotate: rotation }]
}}
/>
</Transition>
</View>
<Animatable.View
useNativeDriver
animation="fadeIn"
delay={1000}
duration={500}
style={{
position: 'absolute',
left: width / 2,
top: 300,
width: dotSize,
height: dotSize,
borderRadius: dotSize / 2,
backgroundColor: '#fff'
}}
/>
<View
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
alignItems: 'flex-start',
justifyContent: 'center',
padding: 20
}}
>
<Transition delay appear="left" disappear={myCustomTransitionFunction}>
<TouchableOpacity onPress={() => this.props.navigation.navigate('Screen2')}>
<Ionicons name="ios-arrow-round-back" size={52} color="white" />
</TouchableOpacity>
</Transition>
</View>
</View>
);
}
}
const AppContainer = createAppContainer(
createFluidNavigator(
{
Screen1: {
screen: Screen1
},
Screen2: {
screen: Screen2
},
Screen3: {
screen: Screen3
}
},
{
headerMode: 'none',
initialRouteName: 'Screen1'
}
)
);
export default class App extends React.Component {
state = {
isReady: false
};
async _loadAssetsAsync() {
const imageAssets = cacheImages([require('./assets/mars_planet.png')]);
await Promise.all([...imageAssets]);
}
static router = AppContainer.router;
render() {
if (!this.state.isReady) {
return (
<AppLoading
startAsync={this._loadAssetsAsync}
onFinish={() => this.setState({ isReady: true })}
onError={console.warn}
/>
);
}
return (
<View style={{ backgroundColor: '#000', flex: 1 }}>
<StatusBar hidden={true} />
<AppContainer />
</View>
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment