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