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