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