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