1import { LinearGradient } from 'expo-linear-gradient';
2import React from 'react';
3import { Image, Platform, Animated, ScrollView, StyleSheet, Text, View } from 'react-native';
4
5import MonoText from '../components/MonoText';
6
7// https://github.com/expo/expo/issues/10599
8const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient);
9
10function incrementColor(color: string, step: number) {
11  const intColor = parseInt(color.substr(1), 16);
12  const newIntColor = (intColor + step).toString(16);
13  return `#${'0'.repeat(6 - newIntColor.length)}${newIntColor}`;
14}
15
16type State = {
17  count: number;
18  colorTop: string;
19  colorBottom: string;
20};
21
22// See: https://github.com/expo/expo/pull/10229#discussion_r490961694
23// eslint-disable-next-line @typescript-eslint/ban-types
24export default class LinearGradientScreen extends React.Component<{}, State> {
25  static navigationOptions = {
26    title: 'LinearGradient',
27  };
28
29  state = {
30    count: 0,
31    colorTop: '#000000',
32    colorBottom: '#cccccc',
33  };
34
35  _interval?: number;
36
37  componentDidMount() {
38    // @ts-expect-error: TS resolves node types first
39    this._interval = setInterval(() => {
40      this.setState((state) => ({
41        count: state.count + 1,
42        colorTop: incrementColor(state.colorTop, 1),
43        colorBottom: incrementColor(state.colorBottom, -1),
44      }));
45    }, 100);
46  }
47
48  componentWillUnmount() {
49    clearInterval(this._interval);
50  }
51
52  render() {
53    const location = Math.sin(this.state.count / 100) * 0.5;
54    const position = Math.sin(this.state.count / 100);
55    return (
56      <ScrollView
57        style={{ flex: 1 }}
58        contentContainerStyle={{
59          alignItems: 'stretch',
60          paddingVertical: 10,
61        }}>
62        <AnimatedLinearGradient
63          style={{ display: 'none' }}
64          colors={[this.state.colorTop, this.state.colorBottom]}
65        />
66        <ColorsTest colors={[this.state.colorTop, this.state.colorBottom]} />
67        <LocationsTest locations={[location, 1.0 - location]} />
68        <ControlPointTest start={[position, 0]} />
69        {Platform.OS !== 'web' && <SnapshotTest />}
70      </ScrollView>
71    );
72  }
73}
74
75const Container: React.FunctionComponent<{ title: string; children?: React.ReactNode }> = ({
76  title,
77  children,
78}) => (
79  <View style={styles.container}>
80    <Text style={styles.containerTitle}>{title}</Text>
81    {children}
82  </View>
83);
84
85const SnapshotTest = () => (
86  <Container title="Snapshot">
87    <View style={{ flexDirection: 'row', alignSelf: 'stretch', justifyContent: 'space-evenly' }}>
88      <LinearGradient
89        colors={['white', 'red']}
90        start={[0.5, 0.5]}
91        end={[1, 1]}
92        style={{
93          width: 100,
94          maxHeight: 200,
95          minHeight: 200,
96          borderWidth: 1,
97          marginVertical: 20,
98          borderColor: 'black',
99        }}
100      />
101      <Image
102        source={require('../../assets/images/confusing_gradient.png')}
103        style={{ width: 100, height: 200, marginVertical: 20 }}
104      />
105    </View>
106    <Text style={{ marginHorizontal: 20 }}>The gradients above should look the same.</Text>
107  </Container>
108);
109
110const ControlPointTest: React.FunctionComponent<{
111  start?: [number, number];
112  end?: [number, number];
113}> = ({ start = [0.5, 0], end = [0, 1] }) => {
114  const startInfo = `start={[${start.map((point) => +point.toFixed(2)).join(', ')}]}`;
115  const endInfo = `end={[${end.map((point) => +point.toFixed(2)).join(', ')}]}`;
116
117  return (
118    <Container title="Control Points">
119      <View>
120        {[startInfo, endInfo].map((pointInfo, index) => (
121          <MonoText key={'--' + index}>{pointInfo}</MonoText>
122        ))}
123      </View>
124      <LinearGradient
125        start={start}
126        end={end}
127        locations={[0.5, 0.5]}
128        colors={['blue', 'lime']}
129        style={styles.gradient}
130      />
131    </Container>
132  );
133};
134
135const ColorsTest = ({ colors }: { colors: string[] }) => {
136  const info = colors.map((value) => `"${value}"`).join(', ');
137  return (
138    <Container title="Colors">
139      <MonoText>{`colors={[${info}]}`}</MonoText>
140      <LinearGradient colors={colors} style={styles.gradient} />
141    </Container>
142  );
143};
144
145const LocationsTest: React.FunctionComponent<{ locations: number[] }> = ({ locations }) => {
146  const locationsInfo = locations.map((location) => +location.toFixed(2)).join(', ');
147  return (
148    <Container title="Locations">
149      <MonoText>{`locations={[${locationsInfo}]}`}</MonoText>
150      <LinearGradient colors={['red', 'blue']} locations={locations} style={styles.gradient} />
151    </Container>
152  );
153};
154
155const styles = StyleSheet.create({
156  container: {
157    padding: 8,
158    flexShrink: 0,
159  },
160  containerTitle: {
161    fontSize: 14,
162    fontWeight: '600',
163    textAlign: 'center',
164    marginBottom: 8,
165  },
166  gradient: {
167    flex: 1,
168    flexShrink: 0,
169    minHeight: 200,
170    maxHeight: 200,
171  },
172});
173