1import * as React from 'react'; 2import { LayoutRectangle, View } from 'react-native'; 3 4import { NativeLinearGradientPoint, NativeLinearGradientProps } from './NativeLinearGradient.types'; 5import { normalizeColor } from './normalizeColor'; 6 7export default function NativeLinearGradient({ 8 colors, 9 locations, 10 startPoint, 11 endPoint, 12 ...props 13}: NativeLinearGradientProps): React.ReactElement { 14 const [layout, setLayout] = React.useState<LayoutRectangle | null>(null); 15 16 const { width = 1, height = 1 } = layout ?? {}; 17 18 // TODO(Bacon): In the future we could consider adding `backgroundRepeat: "no-repeat"`. For more 19 // browser support. 20 const linearGradientBackgroundImage = React.useMemo(() => { 21 return getLinearGradientBackgroundImage(colors, locations, startPoint, endPoint, width, height); 22 }, [colors, locations, startPoint, endPoint, width, height]); 23 24 return ( 25 <View 26 {...props} 27 style={[ 28 props.style, 29 // @ts-ignore: [ts] Property 'backgroundImage' does not exist on type 'ViewStyle'. 30 { backgroundImage: linearGradientBackgroundImage }, 31 ]} 32 onLayout={(event) => { 33 const { x, y, width, height } = event.nativeEvent.layout; 34 const oldLayout = layout ?? { x: 0, y: 0, width: 1, height: 1 }; 35 // don't set new layout state unless the layout has actually changed 36 if ( 37 x !== oldLayout.x || 38 y !== oldLayout.y || 39 width !== oldLayout.width || 40 height !== oldLayout.height 41 ) { 42 setLayout({ x, y, width, height }); 43 } 44 45 if (props.onLayout) { 46 props.onLayout(event); 47 } 48 }} 49 /> 50 ); 51} 52 53/** 54 * Extracted to a separate function in order to be able to test logic independently. 55 */ 56export function getLinearGradientBackgroundImage( 57 colors: number[] | string[], 58 locations?: number[] | null, 59 startPoint?: NativeLinearGradientPoint | null, 60 endPoint?: NativeLinearGradientPoint | null, 61 width: number = 1, 62 height: number = 1 63) { 64 const gradientColors = calculateGradientColors(colors, locations); 65 const angle = calculatePseudoAngle(width, height, startPoint, endPoint); 66 return `linear-gradient(${angle}deg, ${gradientColors.join(', ')})`; 67} 68 69function calculatePseudoAngle( 70 width: number, 71 height: number, 72 startPoint?: NativeLinearGradientPoint | null, 73 endPoint?: NativeLinearGradientPoint | null 74) { 75 const getControlPoints = (): NativeLinearGradientPoint[] => { 76 let correctedStartPoint: NativeLinearGradientPoint = [0, 0]; 77 if (Array.isArray(startPoint)) { 78 correctedStartPoint = [ 79 startPoint[0] != null ? startPoint[0] : 0.0, 80 startPoint[1] != null ? startPoint[1] : 0.0, 81 ]; 82 } 83 let correctedEndPoint: NativeLinearGradientPoint = [0.0, 1.0]; 84 if (Array.isArray(endPoint)) { 85 correctedEndPoint = [ 86 endPoint[0] != null ? endPoint[0] : 0.0, 87 endPoint[1] != null ? endPoint[1] : 1.0, 88 ]; 89 } 90 return [correctedStartPoint, correctedEndPoint]; 91 }; 92 93 const [start, end] = getControlPoints(); 94 start[0] *= width; 95 end[0] *= width; 96 start[1] *= height; 97 end[1] *= height; 98 const py = end[1] - start[1]; 99 const px = end[0] - start[0]; 100 101 return 90 + (Math.atan2(py, px) * 180) / Math.PI; 102} 103 104function calculateGradientColors(colors: number[] | string[], locations?: number[] | null) { 105 return colors.map((color: number | string, index: number): string | void => { 106 const hexColor = normalizeColor(color); 107 let output = hexColor; 108 if (locations && locations[index]) { 109 const location = Math.max(0, Math.min(1, locations[index])); 110 // Convert 0...1 to 0...100 111 const percentage = location * 100; 112 output += ` ${percentage}%`; 113 } 114 return output; 115 }); 116} 117