1import React from 'react';
2import { StyleSheet, 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
21function radToDeg(radians: number): number {
22  return (radians * 180.0) / Math.PI;
23}
24
25export default class NativeLinearGradient extends React.PureComponent<Props, State> {
26  state = {
27    width: undefined,
28    height: undefined,
29  };
30
31  onLayout = event => {
32    this.setState({
33      width: event.nativeEvent.layout.width,
34      height: event.nativeEvent.layout.height,
35    });
36    if (this.props.onLayout) {
37      this.props.onLayout(event);
38    }
39  };
40
41  getControlPoints = (): Point[] => {
42    const { startPoint, endPoint } = this.props;
43
44    let correctedStartPoint: Point = [0.5, 0.0];
45    if (Array.isArray(startPoint)) {
46      correctedStartPoint = [
47        startPoint[0] != null ? startPoint[0] : 0.5,
48        startPoint[1] != null ? startPoint[1] : 0.0,
49      ];
50    }
51    let correctedEndPoint: Point = [0.5, 1.0];
52    if (Array.isArray(endPoint)) {
53      correctedEndPoint = [
54        endPoint[0] != null ? endPoint[0] : 0.5,
55        endPoint[1] != null ? endPoint[1] : 1.0,
56      ];
57    }
58    return [correctedStartPoint, correctedEndPoint];
59  };
60
61  calculateGradientAngleFromControlPoints = (): number => {
62    const [start, end] = this.getControlPoints();
63    const { width = 0, height = 0 } = this.state;
64    const radians = Math.atan2(height * (end[0] - start[0]), width * (end[1] - start[1])) + PI_2;
65    const degrees = radToDeg(radians);
66    return degrees;
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    if (this.state.width && this.state.height) {
92      return `linear-gradient(${this.calculateGradientAngleFromControlPoints()}deg, ${this.getWebGradientColorStyle()})`;
93    }
94    return `transparent`;
95  };
96
97  render() {
98    const { colors, locations, startPoint, endPoint, onLayout, style, ...props } = this.props;
99
100    let flatStyle = style;
101    const backgroundImage = this.getBackgroundImage();
102    if (backgroundImage) {
103      let compiledStyle = StyleSheet.flatten(style) || {};
104      flatStyle = {
105        ...compiledStyle,
106        // @ts-ignore: [ts] Property 'backgroundImage' does not exist on type 'ViewStyle'.
107        backgroundImage: this.getBackgroundImage(),
108      };
109    }
110    // TODO: Bacon: In the future we could consider adding `backgroundRepeat: "no-repeat"`. For more browser support.
111    return <View style={flatStyle} onLayout={this.onLayout} {...props} />;
112  }
113}
114
115function hexStringFromProcessedColor(argbColor: number): string {
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