1import * as ScreenOrientation from 'expo-screen-orientation';
2import { Accelerometer } from 'expo-sensors';
3import React from 'react';
4import {
5  ActivityIndicator,
6  Animated,
7  Button,
8  Dimensions,
9  StyleSheet,
10  Text,
11  View,
12} from 'react-native';
13
14import { Colors } from '../constants';
15
16const COUNT = 5;
17const ITEM_SIZE = Dimensions.get('window').width / COUNT;
18
19interface Props {
20  numItems: number;
21  perspective: number;
22}
23
24function useLockedScreenOrientation() {
25  React.useEffect(() => {
26    ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => null);
27    return () => {
28      ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.ALL).catch(() => null);
29    };
30  }, []);
31}
32
33export default function AccelerometerScreen({ numItems = COUNT, perspective = 200 }: Props) {
34  useLockedScreenOrientation();
35
36  const [items, setItems] = React.useState<any[]>([]);
37  const [error, setError] = React.useState<string | null>(null);
38  const [isSetup, setSetup] = React.useState<boolean>(false);
39
40  React.useEffect(() => {
41    const items = [];
42    for (let i = 0; i < numItems; i++) {
43      items.push({ position: new Animated.ValueXY() });
44    }
45    setItems(items);
46  }, [numItems]);
47
48  React.useEffect(() => {
49    (async () => {
50      const { status } = await Accelerometer.getPermissionsAsync();
51      if (status === 'denied') {
52        setError(`Cannot start demo!\nMotion permission is ${status}.`);
53      } else if (status === 'undetermined') {
54        return;
55      }
56      if (!(await Accelerometer.isAvailableAsync())) {
57        setError('Accelerometer is not available on this device!');
58        return;
59      }
60
61      setSetup(true);
62    })();
63  }, []);
64
65  React.useEffect(() => {
66    if (!isSetup) return;
67
68    const sub = Accelerometer.addListener(({ x, y }) => {
69      // console.log('event');
70      items.forEach((_, index) => {
71        // All that matters is that the values are the same on iOS, Android, Web, ect...
72        const nIndex = index + 1;
73
74        Animated.spring(items[index].position, {
75          toValue: {
76            x: (Number(x.toFixed(1)) * perspective * nIndex) / COUNT,
77            y: (-y.toFixed(1) * perspective * nIndex) / COUNT,
78          },
79          useNativeDriver: false,
80          friction: 7,
81        }).start();
82      });
83    });
84    return () => sub.remove();
85  }, [isSetup]);
86
87  if (error) {
88    return (
89      <Container>
90        <Text style={[styles.text, { color: 'red' }]}>{error}</Text>
91      </Container>
92    );
93  }
94
95  if (!isSetup) {
96    return (
97      <Container>
98        <ActivityIndicator size="large" color={Colors.tintColor} />
99        <Text
100          style={[
101            styles.text,
102            {
103              marginTop: 16,
104            },
105          ]}>
106          Checking Permissions
107        </Text>
108
109        <Button
110          title="Ask Permission"
111          onPress={async () => {
112            const { status } = await Accelerometer.requestPermissionsAsync();
113            if (status !== 'granted') {
114              setError(`Cannot start demo!\nMotion permission is ${status}.`);
115            }
116            if (!(await Accelerometer.isAvailableAsync())) {
117              setError('Accelerometer is not available on this device!');
118              return;
119            }
120
121            setSetup(true);
122          }}
123        />
124      </Container>
125    );
126  }
127
128  return (
129    <Container>
130      <Text style={[styles.text, styles.message]}>
131        {`The stack should move against the orientation of the device.
132          If you lift the bottom of the phone up, the stack should translate down towards the bottom of the screen.
133          The balls all line up when the phone is in "display up" mode.`}
134      </Text>
135      {items.map((val, index) => {
136        return (
137          <Animated.View
138            key={`item-${index}`}
139            style={[
140              styles.ball,
141              {
142                opacity: (index + 1) / COUNT,
143                transform: [
144                  { translateX: items[index].position.x },
145                  { translateY: items[index].position.y },
146                ],
147              },
148            ]}
149          />
150        );
151      })}
152    </Container>
153  );
154}
155
156AccelerometerScreen.navigationOptions = {
157  title: 'Accelerometer',
158};
159
160const Container = (props: any) => <View {...props} style={styles.container} />;
161
162const styles = StyleSheet.create({
163  container: {
164    flex: 1,
165    alignItems: 'center',
166    justifyContent: 'center',
167  },
168  text: {
169    zIndex: 1,
170    fontWeight: '800',
171    color: Colors.tintColor,
172    textAlign: 'center',
173  },
174  message: {
175    position: 'absolute',
176    top: 24,
177    left: 24,
178    right: 24,
179  },
180  ball: {
181    position: 'absolute',
182    width: ITEM_SIZE,
183    height: ITEM_SIZE,
184    borderRadius: ITEM_SIZE,
185    backgroundColor: 'red',
186  },
187});
188