1import { Accelerometer } from 'expo-sensors'; 2import React from 'react'; 3import { Animated, Dimensions, StyleSheet, Text, View, ActivityIndicator } from 'react-native'; 4import { Colors } from '../constants'; 5 6const COUNT = 5; 7const ITEM_SIZE = Dimensions.get('window').width / COUNT; 8 9interface State { 10 items: any[]; 11 error: string | null; 12 isSetup: boolean; 13} 14 15interface Props { 16 numItems: number; 17 perspective: number; 18} 19 20export default class AccelerometerScreen extends React.Component<Props, State> { 21 static navigationOptions = { 22 title: 'Accelerometer', 23 }; 24 25 static defaultProps: Props = { 26 perspective: 200, 27 numItems: COUNT, 28 }; 29 30 readonly state: State; 31 32 constructor(props: Props) { 33 super(props); 34 35 const items = []; 36 for (let i = 0; i < this.props.numItems; i++) { 37 items.push({ position: new Animated.ValueXY() }); 38 } 39 this.state = { items, error: null, isSetup: false }; 40 } 41 42 componentWillUnmount() { 43 Accelerometer.removeAllListeners(); 44 } 45 46 async componentDidMount() { 47 if (!(await Accelerometer.isAvailableAsync())) { 48 this.setState({ 49 error: 50 'Cannot start demo!' + 51 '\nEnable device orientation in Settings > Safari > Motion & Orientation Access' + 52 '\nalso ensure that you are hosting with https as DeviceMotion is now a secure API on iOS Safari.', 53 isSetup: false, 54 }); 55 return; 56 } 57 58 Accelerometer.addListener(({ x, y }) => { 59 this.state.items.forEach((_, index) => { 60 // All that matters is that the values are the same on iOS, Android, Web, ect... 61 62 const { perspective } = this.props; 63 const nIndex = index + 1; 64 65 Animated.spring(this.state.items[index].position, { 66 toValue: { 67 x: (Number(x.toFixed(1)) * perspective * nIndex) / COUNT, 68 y: (-y.toFixed(1) * perspective * nIndex) / COUNT, 69 }, 70 friction: 7, 71 }).start(); 72 }); 73 }); 74 75 this.setState({ isSetup: true }); 76 } 77 78 render() { 79 const { error, items, isSetup } = this.state; 80 81 if (error) { 82 return ( 83 <Container> 84 <Text style={[styles.text, { color: 'red' }]}>{error}</Text> 85 </Container> 86 ); 87 } 88 89 if (!isSetup) { 90 return ( 91 <Container> 92 <ActivityIndicator size="large" color={Colors.tintColor} /> 93 <Text 94 style={[ 95 styles.text, 96 { 97 marginTop: 16, 98 }, 99 ]}> 100 Checking Permissions 101 </Text> 102 </Container> 103 ); 104 } 105 106 return ( 107 <Container> 108 <Text style={[styles.text, styles.message]}> 109 {`The stack should move against the orientation of the device. 110 If you lift the bottom of the phone up, the stack should translate down towards the bottom of the screen. 111 The balls all line up when the phone is in "display up" mode.`} 112 </Text> 113 {items.map((val, index) => { 114 return ( 115 <Animated.View 116 key={`item-${index}`} 117 style={[ 118 styles.ball, 119 { 120 opacity: (index + 1) / COUNT, 121 transform: [ 122 { translateX: items[index].position.x }, 123 { translateY: items[index].position.y }, 124 ], 125 }, 126 ]} 127 /> 128 ); 129 })} 130 </Container> 131 ); 132 } 133} 134 135const Container = (props: any) => <View {...props} style={styles.container} />; 136 137const styles = StyleSheet.create({ 138 container: { 139 flex: 1, 140 alignItems: 'center', 141 justifyContent: 'center', 142 }, 143 text: { 144 zIndex: 1, 145 fontWeight: '800', 146 color: Colors.tintColor, 147 textAlign: 'center', 148 }, 149 message: { 150 position: 'absolute', 151 top: 24, 152 left: 24, 153 right: 24, 154 }, 155 ball: { 156 position: 'absolute', 157 width: ITEM_SIZE, 158 height: ITEM_SIZE, 159 borderRadius: ITEM_SIZE, 160 backgroundColor: 'red', 161 }, 162}); 163