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