1import { ImageContentFit, ImageContentPosition, Image } from 'expo-image'; 2import React from 'react'; 3import { 4 Dimensions, 5 Image as RNImage, 6 ImageResizeMode, 7 ScrollView, 8 StyleSheet, 9 Text, 10 View, 11} from 'react-native'; 12import { PanGestureHandler, PanGestureHandlerGestureEvent } from 'react-native-gesture-handler'; 13import Animated, { 14 useAnimatedGestureHandler, 15 useAnimatedStyle, 16 useSharedValue, 17} from 'react-native-reanimated'; 18 19import { FunctionParameter, useArguments } from '../../components/FunctionDemo'; 20import Configurator from '../../components/FunctionDemo/Configurator'; 21import { Colors } from '../../constants'; 22 23type CustomViewProps = React.PropsWithChildren<object>; 24 25type ContextType = { 26 x: number; 27 y: number; 28}; 29 30const PADDING = 20; 31const HANDLE_SIZE = 25; 32const HANDLE_SLOP = 10; 33const WINDOW_DIMENSIONS = Dimensions.get('window'); 34const MAX_WIDTH = WINDOW_DIMENSIONS.width - 2 * PADDING; 35const MAX_HEIGHT = WINDOW_DIMENSIONS.height - 330; 36 37const ResizableView: React.FC<CustomViewProps> = ({ children }) => { 38 const width = useSharedValue(300); 39 const height = useSharedValue(300); 40 41 const panGestureEvent = useAnimatedGestureHandler<PanGestureHandlerGestureEvent, ContextType>({ 42 onStart: (_, context) => { 43 context.x = width.value; 44 context.y = height.value; 45 }, 46 onActive: (event, context) => { 47 width.value = Math.max(HANDLE_SIZE, Math.min(event.translationX + context.x, MAX_WIDTH)); 48 height.value = Math.max(HANDLE_SIZE, Math.min(event.translationY + context.y, MAX_HEIGHT)); 49 }, 50 }); 51 52 const canvasStyle = useAnimatedStyle(() => { 53 return { 54 width: width.value, 55 height: height.value, 56 }; 57 }, [width, height]); 58 59 return ( 60 <View style={styles.resizableView}> 61 <Text style={styles.hintText}> 62 Move the handle above to resize the image canvas and see how it lays out in different 63 components, sizes and resize modes 64 </Text> 65 66 <Animated.View style={[styles.canvas, canvasStyle]}> 67 {children} 68 69 <PanGestureHandler onGestureEvent={panGestureEvent}> 70 <Animated.View style={styles.resizeHandle}> 71 <View style={styles.resizeHandleChild} /> 72 </Animated.View> 73 </PanGestureHandler> 74 </Animated.View> 75 </View> 76 ); 77}; 78 79const parameters: FunctionParameter[] = [ 80 { 81 name: 'Use React Native Image', 82 type: 'boolean', 83 initial: false, 84 }, 85 { 86 name: 'Size', 87 type: 'enum', 88 values: [ 89 { name: '1500x1000', value: '1500/1000' }, 90 { name: '1000x1500', value: '1000/1500' }, 91 { name: '300x300', value: '300/300' }, 92 { name: '100x100', value: '100/100' }, 93 ], 94 }, 95 { 96 name: 'Content fit', 97 type: 'enum', 98 values: [ 99 { name: 'cover', value: ImageContentFit.COVER }, 100 { name: 'contain', value: ImageContentFit.CONTAIN }, 101 { name: 'fill', value: ImageContentFit.FILL }, 102 { name: 'none', value: ImageContentFit.NONE }, 103 { name: 'scale-down', value: ImageContentFit.SCALE_DOWN }, 104 ], 105 }, 106 { 107 name: 'Content position', 108 type: 'enum', 109 values: [ 110 { name: 'top 50%, left 50%', value: { top: '50%', left: '50%' } }, 111 { name: 'top 0, right 0', value: { top: 0, right: 0 } }, 112 { name: 'top 100, left 50', value: { top: 100, left: 50 } }, 113 { name: 'bottom 10%, right 25%', value: { bottom: '10%', right: '25%' } }, 114 { name: 'bottom 0, right 10', value: { bottom: 0, right: 10 } }, 115 ], 116 }, 117]; 118 119function mapContentFitToResizeMode(contentFit: ImageContentFit): ImageResizeMode { 120 switch (contentFit) { 121 case ImageContentFit.COVER: 122 case ImageContentFit.CONTAIN: 123 return contentFit; 124 case ImageContentFit.FILL: 125 return 'stretch'; 126 case ImageContentFit.NONE: 127 case ImageContentFit.SCALE_DOWN: 128 return 'center'; 129 } 130} 131 132export default function ImageResizableScreen() { 133 const [seed] = React.useState(1 + Math.round(Math.random() * 10)); 134 const [args, updateArgument] = useArguments(parameters); 135 const [showReactNativeComponent, size, contentFit, contentPosition] = args as [ 136 boolean, 137 string, 138 ImageContentFit, 139 ImageContentPosition 140 ]; 141 const ImageComponent: React.ElementType = showReactNativeComponent ? RNImage : Image; 142 143 return ( 144 <ScrollView style={styles.container}> 145 <ResizableView> 146 <ImageComponent 147 style={styles.image} 148 source={{ uri: `https://picsum.photos/seed/${seed}/${size}` }} 149 contentFit={contentFit} 150 contentPosition={contentPosition} 151 resizeMode={mapContentFitToResizeMode(contentFit)} 152 /> 153 </ResizableView> 154 155 <View style={styles.configurator}> 156 <Configurator parameters={parameters} onChange={updateArgument} value={args} /> 157 </View> 158 </ScrollView> 159 ); 160} 161 162const styles = StyleSheet.create({ 163 container: { 164 flex: 1, 165 }, 166 resizableView: { 167 margin: PADDING, 168 width: MAX_WIDTH, 169 height: MAX_HEIGHT, 170 borderWidth: 1, 171 borderColor: Colors.border, 172 backgroundColor: '#eef', 173 }, 174 configurator: { 175 flex: 1, 176 paddingHorizontal: 15, 177 }, 178 canvas: { 179 margin: -1, 180 minWidth: HANDLE_SIZE, 181 minHeight: HANDLE_SIZE, 182 maxWidth: MAX_WIDTH, 183 maxHeight: MAX_HEIGHT, 184 backgroundColor: '#00f2', 185 borderWidth: 2, 186 borderStyle: 'dotted', 187 borderColor: Colors.tintColor, 188 borderRadius: 3, 189 }, 190 resizeHandle: { 191 padding: HANDLE_SLOP, 192 position: 'absolute', 193 bottom: -HANDLE_SIZE / 2 - HANDLE_SLOP, 194 right: -HANDLE_SIZE / 2 - HANDLE_SLOP, 195 }, 196 resizeHandleChild: { 197 width: HANDLE_SIZE, 198 height: HANDLE_SIZE, 199 borderRadius: HANDLE_SIZE, 200 borderWidth: 3, 201 borderColor: Colors.tintColor, 202 backgroundColor: '#fff', 203 }, 204 hintText: { 205 color: Colors.secondaryText, 206 textAlign: 'center', 207 position: 'absolute', 208 right: 10, 209 left: 10, 210 bottom: 16, 211 }, 212 image: { 213 flex: 1, 214 }, 215}); 216