1import React from 'react';
2import { View } from 'react-native';
3
4type Props = {
5  colors: number[];
6  locations?: number[] | null;
7  startPoint?: Point | null;
8  endPoint?: Point | null;
9  onLayout?: Function;
10} & React.ComponentProps<typeof View>;
11
12type State = {
13  width?: number;
14  height?: number;
15};
16
17type Point = [number, number];
18
19const PI_2 = Math.PI / 2;
20
21export default class NativeLinearGradient extends React.PureComponent<Props, State> {
22  state = {
23    width: undefined,
24    height: undefined,
25  };
26
27  onLayout = event => {
28    this.setState({
29      width: event.nativeEvent.layout.width,
30      height: event.nativeEvent.layout.height,
31    });
32    if (this.props.onLayout) {
33      this.props.onLayout(event);
34    }
35  };
36
37  getControlPoints = (): Point[] => {
38    const { startPoint, endPoint } = this.props;
39
40    let correctedStartPoint: Point = [0, 0];
41    if (Array.isArray(startPoint)) {
42      correctedStartPoint = [
43        startPoint[0] != null ? startPoint[0] : 0.0,
44        startPoint[1] != null ? startPoint[1] : 0.0,
45      ];
46    }
47    let correctedEndPoint: Point = [0.0, 1.0];
48    if (Array.isArray(endPoint)) {
49      correctedEndPoint = [
50        endPoint[0] != null ? endPoint[0] : 0.0,
51        endPoint[1] != null ? endPoint[1] : 1.0,
52      ];
53    }
54    return [correctedStartPoint, correctedEndPoint];
55  };
56
57  calculateGradientAngleFromControlPoints = (): number => {
58    const [start, end] = this.getControlPoints();
59    const { width = 1, height = 1 } = this.state;
60    start[0] *= width;
61    end[0] *= width;
62    start[1] *= height;
63    end[1] *= height;
64    const py = end[1] - start[1];
65    const px = end[0] - start[0];
66    return 90 + (Math.atan2(py, px) * 180) / Math.PI;
67  };
68
69  getWebGradientColorStyle = (): string => {
70    return this.getGradientValues().join(',');
71  };
72
73  convertJSColorToGradientSafeColor = (color: number, index: number): string => {
74    const { locations } = this.props;
75    const hexColor = hexStringFromProcessedColor(color);
76    let output = hexColor;
77    if (locations && locations[index]) {
78      const location = Math.max(0, Math.min(1, locations[index]));
79      // Convert 0...1 to 0...100
80      const percentage = location * 100;
81      output += ` ${percentage}%`;
82    }
83    return output;
84  };
85
86  getGradientValues = (): string[] => {
87    return this.props.colors.map(this.convertJSColorToGradientSafeColor);
88  };
89
90  getBackgroundImage = (): string => {
91    return `linear-gradient(${this.calculateGradientAngleFromControlPoints()}deg, ${this.getWebGradientColorStyle()})`;
92  };
93
94  render() {
95    const { colors, locations, startPoint, endPoint, onLayout, style, ...props } = this.props;
96    const backgroundImage = this.getBackgroundImage();
97    // TODO: Bacon: In the future we could consider adding `backgroundRepeat: "no-repeat"`. For more browser support.
98    return (
99      <View
100        style={[
101          style,
102          // @ts-ignore: [ts] Property 'backgroundImage' does not exist on type 'ViewStyle'.
103          { backgroundImage },
104        ]}
105        onLayout={this.onLayout}
106        {...props}
107      />
108    );
109  }
110}
111
112function hexStringFromProcessedColor(argbColor: number): string {
113  if (argbColor === 0) {
114    return `rgba(0,0,0,0)`;
115  }
116  const hexColorString = argbColor.toString(16);
117  const withoutAlpha = hexColorString.substring(2);
118  const alpha = hexColorString.substring(0, 2);
119  return `#${withoutAlpha}${alpha}`;
120}
121