react-native-image-collage 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +564 -0
  3. package/dist/CollageImage.d.ts +7 -0
  4. package/dist/CollageImage.d.ts.map +1 -0
  5. package/dist/CollageImage.js +49 -0
  6. package/dist/CollageTile.d.ts +18 -0
  7. package/dist/CollageTile.d.ts.map +1 -0
  8. package/dist/CollageTile.js +58 -0
  9. package/dist/CollageWithViewer.d.ts +4 -0
  10. package/dist/CollageWithViewer.d.ts.map +1 -0
  11. package/dist/CollageWithViewer.js +65 -0
  12. package/dist/ImageCollage.d.ts +4 -0
  13. package/dist/ImageCollage.d.ts.map +1 -0
  14. package/dist/ImageCollage.js +135 -0
  15. package/dist/ImageCollageWithViewer.d.ts +4 -0
  16. package/dist/ImageCollageWithViewer.d.ts.map +1 -0
  17. package/dist/ImageCollageWithViewer.js +59 -0
  18. package/dist/ImageViewer.d.ts +4 -0
  19. package/dist/ImageViewer.d.ts.map +1 -0
  20. package/dist/ImageViewer.js +76 -0
  21. package/dist/constants.d.ts +13 -0
  22. package/dist/constants.d.ts.map +1 -0
  23. package/dist/constants.js +13 -0
  24. package/dist/expo/createExpoImageRenderer.d.ts +9 -0
  25. package/dist/expo/createExpoImageRenderer.d.ts.map +1 -0
  26. package/dist/expo/createExpoImageRenderer.js +20 -0
  27. package/dist/expo/index.d.ts +15 -0
  28. package/dist/expo/index.d.ts.map +1 -0
  29. package/dist/expo/index.js +59 -0
  30. package/dist/hooks/useContainerWidth.d.ts +10 -0
  31. package/dist/hooks/useContainerWidth.d.ts.map +1 -0
  32. package/dist/hooks/useContainerWidth.js +22 -0
  33. package/dist/index.d.ts +12 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +35 -0
  36. package/dist/types.d.ts +93 -0
  37. package/dist/types.d.ts.map +1 -0
  38. package/dist/types.js +2 -0
  39. package/dist/utils/imageSources.d.ts +11 -0
  40. package/dist/utils/imageSources.d.ts.map +1 -0
  41. package/dist/utils/imageSources.js +131 -0
  42. package/dist/utils/layoutHeight.d.ts +9 -0
  43. package/dist/utils/layoutHeight.d.ts.map +1 -0
  44. package/dist/utils/layoutHeight.js +41 -0
  45. package/dist/utils/renderCollageLayouts.d.ts +97 -0
  46. package/dist/utils/renderCollageLayouts.d.ts.map +1 -0
  47. package/dist/utils/renderCollageLayouts.js +183 -0
  48. package/dist/viewer/ImageCollageWithViewer.d.ts +4 -0
  49. package/dist/viewer/ImageCollageWithViewer.d.ts.map +1 -0
  50. package/dist/viewer/ImageCollageWithViewer.js +43 -0
  51. package/dist/viewer/ImageViewer.d.ts +5 -0
  52. package/dist/viewer/ImageViewer.d.ts.map +1 -0
  53. package/dist/viewer/ImageViewer.js +85 -0
  54. package/dist/viewer/index.d.ts +4 -0
  55. package/dist/viewer/index.d.ts.map +1 -0
  56. package/dist/viewer/index.js +8 -0
  57. package/package.json +68 -0
  58. package/src/CollageImage.tsx +41 -0
  59. package/src/CollageTile.tsx +69 -0
  60. package/src/CollageWithViewer.tsx +53 -0
  61. package/src/ImageCollage.tsx +168 -0
  62. package/src/constants.ts +11 -0
  63. package/src/expo/createExpoImageRenderer.tsx +43 -0
  64. package/src/expo/index.tsx +99 -0
  65. package/src/hooks/useContainerWidth.ts +29 -0
  66. package/src/index.ts +42 -0
  67. package/src/types.ts +120 -0
  68. package/src/utils/imageSources.ts +170 -0
  69. package/src/utils/layoutHeight.ts +54 -0
  70. package/src/utils/renderCollageLayouts.tsx +329 -0
  71. package/src/viewer/ImageCollageWithViewer.tsx +24 -0
  72. package/src/viewer/ImageViewer.tsx +93 -0
  73. package/src/viewer/index.ts +10 -0
@@ -0,0 +1,329 @@
1
+ import React from "react";
2
+ import {
3
+ Pressable,
4
+ StyleProp,
5
+ StyleSheet,
6
+ Text,
7
+ View,
8
+ ViewStyle,
9
+ } from "react-native";
10
+ import { renderCollageImage } from "../CollageImage";
11
+ import { CollageTile } from "../CollageTile";
12
+ import { ANDROID_RIPPLE } from "../constants";
13
+ import type {
14
+ CollageImageRenderer,
15
+ ImagePriority,
16
+ NormalizedCollageImage,
17
+ } from "../types";
18
+
19
+ type SharedTileConfig = {
20
+ onPress?: (index: number) => void;
21
+ borderRadius: number;
22
+ placeholderColor: string;
23
+ getImagePriority?: (index: number) => ImagePriority;
24
+ renderImage?: CollageImageRenderer;
25
+ };
26
+
27
+ type RenderCollageContentOptions = {
28
+ images: NormalizedCollageImage[];
29
+ layoutHeight: number;
30
+ spacing: number;
31
+ borderRadius: number;
32
+ placeholderColor: string;
33
+ maxVisibleImages: number;
34
+ onImagePress?: (index: number) => void;
35
+ renderImage?: CollageImageRenderer;
36
+ sharedTileConfig: SharedTileConfig;
37
+ };
38
+
39
+ function renderTile(
40
+ image: NormalizedCollageImage,
41
+ index: number,
42
+ shared: SharedTileConfig,
43
+ style?: StyleProp<ViewStyle>,
44
+ priority?: ImagePriority,
45
+ transition?: number,
46
+ ) {
47
+ return (
48
+ <CollageTile
49
+ key={`${image.cacheKey}-${index}`}
50
+ source={image.source}
51
+ remoteUri={image.remoteUri}
52
+ index={index}
53
+ onPress={shared.onPress}
54
+ borderRadius={shared.borderRadius}
55
+ placeholderColor={shared.placeholderColor}
56
+ renderImage={shared.renderImage}
57
+ priority={priority ?? shared.getImagePriority?.(index) ?? "normal"}
58
+ transition={transition}
59
+ style={style}
60
+ />
61
+ );
62
+ }
63
+
64
+ function renderOverflowTile({
65
+ image,
66
+ tileIndex,
67
+ overflowCount,
68
+ borderRadius,
69
+ placeholderColor,
70
+ onImagePress,
71
+ renderImage,
72
+ }: {
73
+ image: NormalizedCollageImage;
74
+ tileIndex: number;
75
+ overflowCount: number;
76
+ borderRadius: number;
77
+ placeholderColor: string;
78
+ onImagePress?: (index: number) => void;
79
+ renderImage?: CollageImageRenderer;
80
+ }) {
81
+ return (
82
+ <Pressable
83
+ onPress={() => onImagePress?.(tileIndex)}
84
+ android_ripple={ANDROID_RIPPLE}
85
+ style={[
86
+ styles.overflowTile,
87
+ styles.flexTile,
88
+ {
89
+ borderRadius,
90
+ backgroundColor: placeholderColor,
91
+ },
92
+ ]}
93
+ >
94
+ {renderCollageImage(
95
+ {
96
+ source: image.source,
97
+ remoteUri: image.remoteUri,
98
+ priority: "normal",
99
+ transition: 0,
100
+ },
101
+ renderImage,
102
+ )}
103
+ {overflowCount > 0 && (
104
+ <View pointerEvents="none" style={styles.overflowOverlay}>
105
+ <Text style={styles.overflowText}>{`+${overflowCount}`}</Text>
106
+ </View>
107
+ )}
108
+ </Pressable>
109
+ );
110
+ }
111
+
112
+ function resolveVisibleLayout(count: number, maxVisibleImages: number) {
113
+ const hasOverflow = count > maxVisibleImages;
114
+ const visibleCount = hasOverflow ? maxVisibleImages : count;
115
+ const overflowCount = hasOverflow ? count - maxVisibleImages : 0;
116
+ const overflowTileIndex = visibleCount - 1;
117
+
118
+ return {
119
+ hasOverflow,
120
+ visibleCount,
121
+ overflowCount,
122
+ overflowTileIndex,
123
+ };
124
+ }
125
+
126
+ function renderSingleLayout(
127
+ images: NormalizedCollageImage[],
128
+ options: RenderCollageContentOptions,
129
+ layout: ReturnType<typeof resolveVisibleLayout>,
130
+ ) {
131
+ const { layoutHeight, sharedTileConfig } = options;
132
+ const image = images[0];
133
+
134
+ if (layout.hasOverflow) {
135
+ return renderOverflowTile({
136
+ image,
137
+ tileIndex: 0,
138
+ overflowCount: layout.overflowCount,
139
+ borderRadius: options.borderRadius,
140
+ placeholderColor: options.placeholderColor,
141
+ onImagePress: options.onImagePress,
142
+ renderImage: options.renderImage,
143
+ });
144
+ }
145
+
146
+ return renderTile(image, 0, sharedTileConfig, { height: layoutHeight });
147
+ }
148
+
149
+ function renderTwoLayout(
150
+ images: NormalizedCollageImage[],
151
+ options: RenderCollageContentOptions,
152
+ layout: ReturnType<typeof resolveVisibleLayout>,
153
+ ) {
154
+ const { sharedTileConfig } = options;
155
+
156
+ return (
157
+ <>
158
+ {renderTile(images[0], 0, sharedTileConfig, styles.flexTile)}
159
+ {layout.hasOverflow ? (
160
+ renderOverflowTile({
161
+ image: images[1],
162
+ tileIndex: 1,
163
+ overflowCount: layout.overflowCount,
164
+ borderRadius: options.borderRadius,
165
+ placeholderColor: options.placeholderColor,
166
+ onImagePress: options.onImagePress,
167
+ renderImage: options.renderImage,
168
+ })
169
+ ) : (
170
+ renderTile(images[1], 1, sharedTileConfig, styles.flexTile)
171
+ )}
172
+ </>
173
+ );
174
+ }
175
+
176
+ function renderThreeLayout(
177
+ images: NormalizedCollageImage[],
178
+ options: RenderCollageContentOptions,
179
+ layout: ReturnType<typeof resolveVisibleLayout>,
180
+ ) {
181
+ const { spacing, sharedTileConfig } = options;
182
+
183
+ return (
184
+ <>
185
+ {renderTile(images[0], 0, sharedTileConfig, styles.flexTile)}
186
+ <View style={[styles.flexColumn, { gap: spacing }]}>
187
+ {renderTile(images[1], 1, sharedTileConfig, styles.flexTile)}
188
+ {layout.hasOverflow ? (
189
+ renderOverflowTile({
190
+ image: images[2],
191
+ tileIndex: 2,
192
+ overflowCount: layout.overflowCount,
193
+ borderRadius: options.borderRadius,
194
+ placeholderColor: options.placeholderColor,
195
+ onImagePress: options.onImagePress,
196
+ renderImage: options.renderImage,
197
+ })
198
+ ) : (
199
+ renderTile(images[2], 2, sharedTileConfig, styles.flexTile)
200
+ )}
201
+ </View>
202
+ </>
203
+ );
204
+ }
205
+
206
+ function renderGridLayout(
207
+ images: NormalizedCollageImage[],
208
+ options: RenderCollageContentOptions,
209
+ layout: ReturnType<typeof resolveVisibleLayout>,
210
+ ) {
211
+ const { spacing, sharedTileConfig } = options;
212
+ const topRow = images.slice(0, 2);
213
+ const bottomLeft = images[2];
214
+ const bottomRight = images[3];
215
+
216
+ return (
217
+ <>
218
+ <View style={[styles.row, { flex: 1, gap: spacing }]}>
219
+ {topRow.map((image, index) =>
220
+ renderTile(image, index, sharedTileConfig, styles.flexTile),
221
+ )}
222
+ </View>
223
+ <View style={[styles.row, { flex: 1, gap: spacing }]}>
224
+ {bottomLeft
225
+ ? renderTile(bottomLeft, 2, sharedTileConfig, styles.flexTile)
226
+ : null}
227
+ {bottomRight ? (
228
+ layout.hasOverflow ? (
229
+ renderOverflowTile({
230
+ image: bottomRight,
231
+ tileIndex: 3,
232
+ overflowCount: layout.overflowCount,
233
+ borderRadius: options.borderRadius,
234
+ placeholderColor: options.placeholderColor,
235
+ onImagePress: options.onImagePress,
236
+ renderImage: options.renderImage,
237
+ })
238
+ ) : (
239
+ renderTile(bottomRight, 3, sharedTileConfig, styles.flexTile)
240
+ )
241
+ ) : null}
242
+ </View>
243
+ </>
244
+ );
245
+ }
246
+
247
+ export function renderCollageContent(options: RenderCollageContentOptions) {
248
+ const { images, maxVisibleImages } = options;
249
+ const count = images.length;
250
+ const layout = resolveVisibleLayout(count, maxVisibleImages);
251
+ const visibleImages = images.slice(0, layout.visibleCount);
252
+
253
+ if (layout.visibleCount === 1) {
254
+ return renderSingleLayout(visibleImages, options, layout);
255
+ }
256
+
257
+ if (layout.visibleCount === 2) {
258
+ return renderTwoLayout(visibleImages, options, layout);
259
+ }
260
+
261
+ if (layout.visibleCount === 3) {
262
+ return renderThreeLayout(visibleImages, options, layout);
263
+ }
264
+
265
+ return renderGridLayout(visibleImages, options, layout);
266
+ }
267
+
268
+ export function getCollageLayoutStyle({
269
+ count,
270
+ maxVisibleImages,
271
+ layoutHeight,
272
+ spacing,
273
+ borderRadius,
274
+ }: {
275
+ count: number;
276
+ maxVisibleImages: number;
277
+ layoutHeight: number;
278
+ spacing: number;
279
+ borderRadius: number;
280
+ }) {
281
+ const { visibleCount } = resolveVisibleLayout(count, maxVisibleImages);
282
+
283
+ if (visibleCount === 1) {
284
+ return {
285
+ containerStyle: {
286
+ height: layoutHeight,
287
+ borderRadius,
288
+ overflow: "hidden" as const,
289
+ },
290
+ row: false,
291
+ };
292
+ }
293
+
294
+ if (visibleCount === 2 || visibleCount === 3) {
295
+ return {
296
+ containerStyle: {
297
+ height: layoutHeight,
298
+ gap: spacing,
299
+ },
300
+ row: true,
301
+ };
302
+ }
303
+
304
+ return {
305
+ containerStyle: {
306
+ height: layoutHeight,
307
+ gap: spacing,
308
+ overflow: "hidden" as const,
309
+ minHeight: 0,
310
+ },
311
+ row: false,
312
+ };
313
+ }
314
+
315
+ const styles = StyleSheet.create({
316
+ row: { flexDirection: "row", overflow: "hidden", minHeight: 0, width: "100%" },
317
+ flexTile: { flex: 1, flexBasis: 0, minWidth: 0, minHeight: 0 },
318
+ flexColumn: { flex: 1, flexBasis: 0, minWidth: 0, minHeight: 0 },
319
+ overflowTile: { overflow: "hidden", minHeight: 0, minWidth: 0 },
320
+ overflowOverlay: {
321
+ ...StyleSheet.absoluteFillObject,
322
+ backgroundColor: "rgba(0,0,0,0.35)",
323
+ alignItems: "center",
324
+ justifyContent: "center",
325
+ },
326
+ overflowText: { color: "#fff", fontSize: 24, fontWeight: "800" },
327
+ });
328
+
329
+ export { styles as collageLayoutStyles };
@@ -0,0 +1,24 @@
1
+ import React, { memo, useMemo } from "react";
2
+ import { CollageWithViewer } from "../CollageWithViewer";
3
+ import type { ImageCollageWithViewerProps } from "../types";
4
+ import { createDefaultViewerRenderer } from "./ImageViewer";
5
+
6
+ export const ImageCollageWithViewer = memo(function ImageCollageWithViewer({
7
+ viewerProps,
8
+ renderViewer,
9
+ onImagePress,
10
+ ...collageProps
11
+ }: ImageCollageWithViewerProps) {
12
+ const defaultRenderer = useMemo(
13
+ () => createDefaultViewerRenderer(viewerProps),
14
+ [viewerProps],
15
+ );
16
+
17
+ return (
18
+ <CollageWithViewer
19
+ {...collageProps}
20
+ onImagePress={onImagePress}
21
+ renderViewer={renderViewer ?? defaultRenderer}
22
+ />
23
+ );
24
+ });
@@ -0,0 +1,93 @@
1
+ import React, { memo } from "react";
2
+ import { Platform, Pressable, Text, View } from "react-native";
3
+ import ImageView from "react-native-image-viewing";
4
+ import type { CollageViewerRenderProps, ImageViewerProps } from "../types";
5
+
6
+ export const ImageViewer = memo(function ImageViewer({
7
+ images,
8
+ visible,
9
+ imageIndex = 0,
10
+ onRequestClose,
11
+ swipeToCloseEnabled = true,
12
+ doubleTapToZoomEnabled = true,
13
+ presentationStyle = "fullScreen",
14
+ showCloseButton = true,
15
+ showIndexFooter = true,
16
+ closeButtonLabel = "Close",
17
+ }: ImageViewerProps) {
18
+ if (!visible) {
19
+ return null;
20
+ }
21
+
22
+ return (
23
+ <ImageView
24
+ images={images}
25
+ imageIndex={imageIndex}
26
+ visible={visible}
27
+ onRequestClose={onRequestClose}
28
+ presentationStyle={presentationStyle}
29
+ swipeToCloseEnabled={swipeToCloseEnabled}
30
+ doubleTapToZoomEnabled={doubleTapToZoomEnabled}
31
+ HeaderComponent={
32
+ showCloseButton
33
+ ? () => (
34
+ <View
35
+ style={{
36
+ position: "absolute",
37
+ top: Platform.OS === "android" ? 12 : 50,
38
+ right: 12,
39
+ }}
40
+ >
41
+ <Pressable
42
+ onPress={onRequestClose}
43
+ style={{
44
+ backgroundColor: "rgba(0,0,0,0.45)",
45
+ paddingHorizontal: 12,
46
+ paddingVertical: 8,
47
+ borderRadius: 12,
48
+ }}
49
+ >
50
+ <Text style={{ color: "#fff", fontWeight: "700" }}>
51
+ {closeButtonLabel}
52
+ </Text>
53
+ </Pressable>
54
+ </View>
55
+ )
56
+ : undefined
57
+ }
58
+ FooterComponent={
59
+ showIndexFooter
60
+ ? ({ imageIndex: currentIndex }) =>
61
+ images.length > 1 ? (
62
+ <View
63
+ style={{
64
+ position: "absolute",
65
+ bottom: 16,
66
+ alignSelf: "center",
67
+ backgroundColor: "rgba(0,0,0,0.35)",
68
+ paddingHorizontal: 12,
69
+ paddingVertical: 6,
70
+ borderRadius: 12,
71
+ }}
72
+ >
73
+ <Text style={{ color: "#fff", fontWeight: "700" }}>
74
+ {currentIndex + 1} / {images.length}
75
+ </Text>
76
+ </View>
77
+ ) : null
78
+ : undefined
79
+ }
80
+ />
81
+ );
82
+ });
83
+
84
+ export function createDefaultViewerRenderer(
85
+ viewerProps?: Omit<
86
+ ImageViewerProps,
87
+ "images" | "visible" | "imageIndex" | "onRequestClose"
88
+ >,
89
+ ) {
90
+ return function DefaultViewerRenderer(props: CollageViewerRenderProps) {
91
+ return <ImageViewer {...viewerProps} {...props} />;
92
+ };
93
+ }
@@ -0,0 +1,10 @@
1
+ export { ImageViewer, createDefaultViewerRenderer } from "./ImageViewer";
2
+ export { ImageCollageWithViewer } from "./ImageCollageWithViewer";
3
+
4
+ export type {
5
+ ImageViewerProps,
6
+ ImageCollageWithViewerProps,
7
+ CollageViewerRenderProps,
8
+ CollageViewerRenderer,
9
+ ImageViewerImage,
10
+ } from "../types";