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