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.
- package/LICENSE +21 -0
- package/README.md +564 -0
- package/dist/CollageImage.d.ts +7 -0
- package/dist/CollageImage.d.ts.map +1 -0
- package/dist/CollageImage.js +49 -0
- package/dist/CollageTile.d.ts +18 -0
- package/dist/CollageTile.d.ts.map +1 -0
- package/dist/CollageTile.js +58 -0
- package/dist/CollageWithViewer.d.ts +4 -0
- package/dist/CollageWithViewer.d.ts.map +1 -0
- package/dist/CollageWithViewer.js +65 -0
- package/dist/ImageCollage.d.ts +4 -0
- package/dist/ImageCollage.d.ts.map +1 -0
- package/dist/ImageCollage.js +135 -0
- package/dist/ImageCollageWithViewer.d.ts +4 -0
- package/dist/ImageCollageWithViewer.d.ts.map +1 -0
- package/dist/ImageCollageWithViewer.js +59 -0
- package/dist/ImageViewer.d.ts +4 -0
- package/dist/ImageViewer.d.ts.map +1 -0
- package/dist/ImageViewer.js +76 -0
- package/dist/constants.d.ts +13 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +13 -0
- package/dist/expo/createExpoImageRenderer.d.ts +9 -0
- package/dist/expo/createExpoImageRenderer.d.ts.map +1 -0
- package/dist/expo/createExpoImageRenderer.js +20 -0
- package/dist/expo/index.d.ts +15 -0
- package/dist/expo/index.d.ts.map +1 -0
- package/dist/expo/index.js +59 -0
- package/dist/hooks/useContainerWidth.d.ts +10 -0
- package/dist/hooks/useContainerWidth.d.ts.map +1 -0
- package/dist/hooks/useContainerWidth.js +22 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/types.d.ts +93 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/utils/imageSources.d.ts +11 -0
- package/dist/utils/imageSources.d.ts.map +1 -0
- package/dist/utils/imageSources.js +131 -0
- package/dist/utils/layoutHeight.d.ts +9 -0
- package/dist/utils/layoutHeight.d.ts.map +1 -0
- package/dist/utils/layoutHeight.js +41 -0
- package/dist/utils/renderCollageLayouts.d.ts +97 -0
- package/dist/utils/renderCollageLayouts.d.ts.map +1 -0
- package/dist/utils/renderCollageLayouts.js +183 -0
- package/dist/viewer/ImageCollageWithViewer.d.ts +4 -0
- package/dist/viewer/ImageCollageWithViewer.d.ts.map +1 -0
- package/dist/viewer/ImageCollageWithViewer.js +43 -0
- package/dist/viewer/ImageViewer.d.ts +5 -0
- package/dist/viewer/ImageViewer.d.ts.map +1 -0
- package/dist/viewer/ImageViewer.js +85 -0
- package/dist/viewer/index.d.ts +4 -0
- package/dist/viewer/index.d.ts.map +1 -0
- package/dist/viewer/index.js +8 -0
- package/package.json +68 -0
- package/src/CollageImage.tsx +41 -0
- package/src/CollageTile.tsx +69 -0
- package/src/CollageWithViewer.tsx +53 -0
- package/src/ImageCollage.tsx +168 -0
- package/src/constants.ts +11 -0
- package/src/expo/createExpoImageRenderer.tsx +43 -0
- package/src/expo/index.tsx +99 -0
- package/src/hooks/useContainerWidth.ts +29 -0
- package/src/index.ts +42 -0
- package/src/types.ts +120 -0
- package/src/utils/imageSources.ts +170 -0
- package/src/utils/layoutHeight.ts +54 -0
- package/src/utils/renderCollageLayouts.tsx +329 -0
- package/src/viewer/ImageCollageWithViewer.tsx +24 -0
- package/src/viewer/ImageViewer.tsx +93 -0
- package/src/viewer/index.ts +10 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ImageCollageWithViewer = void 0;
|
|
37
|
+
const react_1 = __importStar(require("react"));
|
|
38
|
+
const CollageWithViewer_1 = require("../CollageWithViewer");
|
|
39
|
+
const ImageViewer_1 = require("./ImageViewer");
|
|
40
|
+
exports.ImageCollageWithViewer = (0, react_1.memo)(function ImageCollageWithViewer({ viewerProps, renderViewer, onImagePress, ...collageProps }) {
|
|
41
|
+
const defaultRenderer = (0, react_1.useMemo)(() => (0, ImageViewer_1.createDefaultViewerRenderer)(viewerProps), [viewerProps]);
|
|
42
|
+
return (<CollageWithViewer_1.CollageWithViewer {...collageProps} onImagePress={onImagePress} renderViewer={renderViewer ?? defaultRenderer}/>);
|
|
43
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { CollageViewerRenderProps, ImageViewerProps } from "../types";
|
|
3
|
+
export declare const ImageViewer: React.NamedExoticComponent<ImageViewerProps>;
|
|
4
|
+
export declare function createDefaultViewerRenderer(viewerProps?: Omit<ImageViewerProps, "images" | "visible" | "imageIndex" | "onRequestClose">): (props: CollageViewerRenderProps) => React.JSX.Element;
|
|
5
|
+
//# sourceMappingURL=ImageViewer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImageViewer.d.ts","sourceRoot":"","sources":["../../src/viewer/ImageViewer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAe,MAAM,OAAO,CAAC;AAGpC,OAAO,KAAK,EAAE,wBAAwB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAE3E,eAAO,MAAM,WAAW,8CA4EtB,CAAC;AAEH,wBAAgB,2BAA2B,CACzC,WAAW,CAAC,EAAE,IAAI,CAChB,gBAAgB,EAChB,QAAQ,GAAG,SAAS,GAAG,YAAY,GAAG,gBAAgB,CACvD,IAEqC,OAAO,wBAAwB,uBAGtE"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.ImageViewer = void 0;
|
|
40
|
+
exports.createDefaultViewerRenderer = createDefaultViewerRenderer;
|
|
41
|
+
const react_1 = __importStar(require("react"));
|
|
42
|
+
const react_native_1 = require("react-native");
|
|
43
|
+
const react_native_image_viewing_1 = __importDefault(require("react-native-image-viewing"));
|
|
44
|
+
exports.ImageViewer = (0, react_1.memo)(function ImageViewer({ images, visible, imageIndex = 0, onRequestClose, swipeToCloseEnabled = true, doubleTapToZoomEnabled = true, presentationStyle = "fullScreen", showCloseButton = true, showIndexFooter = true, closeButtonLabel = "Close", }) {
|
|
45
|
+
if (!visible) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return (<react_native_image_viewing_1.default images={images} imageIndex={imageIndex} visible={visible} onRequestClose={onRequestClose} presentationStyle={presentationStyle} swipeToCloseEnabled={swipeToCloseEnabled} doubleTapToZoomEnabled={doubleTapToZoomEnabled} HeaderComponent={showCloseButton
|
|
49
|
+
? () => (<react_native_1.View style={{
|
|
50
|
+
position: "absolute",
|
|
51
|
+
top: react_native_1.Platform.OS === "android" ? 12 : 50,
|
|
52
|
+
right: 12,
|
|
53
|
+
}}>
|
|
54
|
+
<react_native_1.Pressable onPress={onRequestClose} style={{
|
|
55
|
+
backgroundColor: "rgba(0,0,0,0.45)",
|
|
56
|
+
paddingHorizontal: 12,
|
|
57
|
+
paddingVertical: 8,
|
|
58
|
+
borderRadius: 12,
|
|
59
|
+
}}>
|
|
60
|
+
<react_native_1.Text style={{ color: "#fff", fontWeight: "700" }}>
|
|
61
|
+
{closeButtonLabel}
|
|
62
|
+
</react_native_1.Text>
|
|
63
|
+
</react_native_1.Pressable>
|
|
64
|
+
</react_native_1.View>)
|
|
65
|
+
: undefined} FooterComponent={showIndexFooter
|
|
66
|
+
? ({ imageIndex: currentIndex }) => images.length > 1 ? (<react_native_1.View style={{
|
|
67
|
+
position: "absolute",
|
|
68
|
+
bottom: 16,
|
|
69
|
+
alignSelf: "center",
|
|
70
|
+
backgroundColor: "rgba(0,0,0,0.35)",
|
|
71
|
+
paddingHorizontal: 12,
|
|
72
|
+
paddingVertical: 6,
|
|
73
|
+
borderRadius: 12,
|
|
74
|
+
}}>
|
|
75
|
+
<react_native_1.Text style={{ color: "#fff", fontWeight: "700" }}>
|
|
76
|
+
{currentIndex + 1} / {images.length}
|
|
77
|
+
</react_native_1.Text>
|
|
78
|
+
</react_native_1.View>) : null
|
|
79
|
+
: undefined}/>);
|
|
80
|
+
});
|
|
81
|
+
function createDefaultViewerRenderer(viewerProps) {
|
|
82
|
+
return function DefaultViewerRenderer(props) {
|
|
83
|
+
return <exports.ImageViewer {...viewerProps} {...props}/>;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { ImageViewer, createDefaultViewerRenderer } from "./ImageViewer";
|
|
2
|
+
export { ImageCollageWithViewer } from "./ImageCollageWithViewer";
|
|
3
|
+
export type { ImageViewerProps, ImageCollageWithViewerProps, CollageViewerRenderProps, CollageViewerRenderer, ImageViewerImage, } from "../types";
|
|
4
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/viewer/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,2BAA2B,EAAE,MAAM,eAAe,CAAC;AACzE,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAElE,YAAY,EACV,gBAAgB,EAChB,2BAA2B,EAC3B,wBAAwB,EACxB,qBAAqB,EACrB,gBAAgB,GACjB,MAAM,UAAU,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ImageCollageWithViewer = exports.createDefaultViewerRenderer = exports.ImageViewer = void 0;
|
|
4
|
+
var ImageViewer_1 = require("./ImageViewer");
|
|
5
|
+
Object.defineProperty(exports, "ImageViewer", { enumerable: true, get: function () { return ImageViewer_1.ImageViewer; } });
|
|
6
|
+
Object.defineProperty(exports, "createDefaultViewerRenderer", { enumerable: true, get: function () { return ImageViewer_1.createDefaultViewerRenderer; } });
|
|
7
|
+
var ImageCollageWithViewer_1 = require("./ImageCollageWithViewer");
|
|
8
|
+
Object.defineProperty(exports, "ImageCollageWithViewer", { enumerable: true, get: function () { return ImageCollageWithViewer_1.ImageCollageWithViewer; } });
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-image-collage",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Responsive image collage layouts for React Native with an optional full-screen image viewer",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"react-native": "src/index.ts",
|
|
8
|
+
"source": "src/index.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"react-native": "./src/index.ts",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./viewer": {
|
|
20
|
+
"react-native": "./src/viewer/index.ts",
|
|
21
|
+
"types": "./dist/viewer/index.d.ts",
|
|
22
|
+
"default": "./dist/viewer/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./expo": {
|
|
25
|
+
"react-native": "./src/expo/index.tsx",
|
|
26
|
+
"types": "./dist/expo/index.d.ts",
|
|
27
|
+
"default": "./dist/expo/index.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc -p tsconfig.build.json",
|
|
32
|
+
"prepare": "npm run build",
|
|
33
|
+
"typecheck": "tsc --noEmit",
|
|
34
|
+
"example": "npm run build && npm install --prefix example && npm run start --prefix example -- --clear"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"react-native",
|
|
38
|
+
"image",
|
|
39
|
+
"collage",
|
|
40
|
+
"gallery",
|
|
41
|
+
"image-viewer",
|
|
42
|
+
"expo"
|
|
43
|
+
],
|
|
44
|
+
"author": "Faisal Khawaj",
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"expo-image": ">=1.0.0",
|
|
48
|
+
"react": ">=18.0.0",
|
|
49
|
+
"react-native": ">=0.72.0",
|
|
50
|
+
"react-native-image-viewing": ">=0.2.0"
|
|
51
|
+
},
|
|
52
|
+
"peerDependenciesMeta": {
|
|
53
|
+
"expo-image": {
|
|
54
|
+
"optional": true
|
|
55
|
+
},
|
|
56
|
+
"react-native-image-viewing": {
|
|
57
|
+
"optional": true
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@types/react": "^19.0.0",
|
|
62
|
+
"expo-image": "^3.0.10",
|
|
63
|
+
"react": "^19.0.0",
|
|
64
|
+
"react-native": "^0.81.0",
|
|
65
|
+
"react-native-image-viewing": "^0.2.2",
|
|
66
|
+
"typescript": "^5.8.0"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React, { memo } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Image,
|
|
4
|
+
Platform,
|
|
5
|
+
StyleProp,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
ImageStyle,
|
|
8
|
+
type ImageSourcePropType,
|
|
9
|
+
} from "react-native";
|
|
10
|
+
import type { CollageImageRenderProps, CollageImageRenderer } from "./types";
|
|
11
|
+
|
|
12
|
+
export const CollageImage = memo(function CollageImage({
|
|
13
|
+
source,
|
|
14
|
+
style,
|
|
15
|
+
transition = Platform.OS === "android" ? 80 : 150,
|
|
16
|
+
}: CollageImageRenderProps) {
|
|
17
|
+
return (
|
|
18
|
+
<Image
|
|
19
|
+
source={source}
|
|
20
|
+
resizeMode="cover"
|
|
21
|
+
fadeDuration={transition}
|
|
22
|
+
style={[StyleSheet.absoluteFill, style]}
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export function renderCollageImage(
|
|
28
|
+
props: CollageImageRenderProps,
|
|
29
|
+
renderImage?: CollageImageRenderer,
|
|
30
|
+
style?: StyleProp<ImageStyle>,
|
|
31
|
+
) {
|
|
32
|
+
const imageProps = style ? { ...props, style } : props;
|
|
33
|
+
|
|
34
|
+
if (renderImage) {
|
|
35
|
+
return renderImage(imageProps);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return <CollageImage {...imageProps} />;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type { ImageSourcePropType };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React, { memo, useCallback } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Platform,
|
|
4
|
+
Pressable,
|
|
5
|
+
StyleProp,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
ViewStyle,
|
|
8
|
+
type ImageSourcePropType,
|
|
9
|
+
} from "react-native";
|
|
10
|
+
import { renderCollageImage } from "./CollageImage";
|
|
11
|
+
import { ANDROID_RIPPLE } from "./constants";
|
|
12
|
+
import type {
|
|
13
|
+
CollageImageRenderer,
|
|
14
|
+
ImagePriority,
|
|
15
|
+
} from "./types";
|
|
16
|
+
|
|
17
|
+
type CollageTileProps = {
|
|
18
|
+
source: ImageSourcePropType;
|
|
19
|
+
remoteUri?: string;
|
|
20
|
+
index: number;
|
|
21
|
+
onPress?: (index: number) => void;
|
|
22
|
+
borderRadius: number;
|
|
23
|
+
style?: StyleProp<ViewStyle>;
|
|
24
|
+
priority?: ImagePriority;
|
|
25
|
+
placeholderColor: string;
|
|
26
|
+
renderImage?: CollageImageRenderer;
|
|
27
|
+
transition?: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const CollageTile = memo(function CollageTile({
|
|
31
|
+
source,
|
|
32
|
+
remoteUri,
|
|
33
|
+
index,
|
|
34
|
+
onPress,
|
|
35
|
+
borderRadius,
|
|
36
|
+
style,
|
|
37
|
+
priority = "normal",
|
|
38
|
+
placeholderColor,
|
|
39
|
+
renderImage,
|
|
40
|
+
transition,
|
|
41
|
+
}: CollageTileProps) {
|
|
42
|
+
const handlePress = useCallback(() => onPress?.(index), [onPress, index]);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<Pressable
|
|
46
|
+
onPress={handlePress}
|
|
47
|
+
android_ripple={ANDROID_RIPPLE}
|
|
48
|
+
style={[
|
|
49
|
+
style,
|
|
50
|
+
styles.tile,
|
|
51
|
+
{ borderRadius, backgroundColor: placeholderColor },
|
|
52
|
+
]}
|
|
53
|
+
>
|
|
54
|
+
{renderCollageImage(
|
|
55
|
+
{
|
|
56
|
+
source,
|
|
57
|
+
remoteUri,
|
|
58
|
+
priority,
|
|
59
|
+
transition: transition ?? (Platform.OS === "android" ? 80 : 150),
|
|
60
|
+
},
|
|
61
|
+
renderImage,
|
|
62
|
+
)}
|
|
63
|
+
</Pressable>
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const styles = StyleSheet.create({
|
|
68
|
+
tile: { overflow: "hidden", minHeight: 0, minWidth: 0 },
|
|
69
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React, { memo, useCallback, useMemo, useState } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { ImageCollage } from "./ImageCollage";
|
|
4
|
+
import type { CollageWithViewerProps } from "./types";
|
|
5
|
+
import { normalizeImages, toViewerImages } from "./utils/imageSources";
|
|
6
|
+
|
|
7
|
+
export const CollageWithViewer = memo(function CollageWithViewer({
|
|
8
|
+
images,
|
|
9
|
+
renderViewer,
|
|
10
|
+
onImagePress,
|
|
11
|
+
...collageProps
|
|
12
|
+
}: CollageWithViewerProps) {
|
|
13
|
+
const [viewerVisible, setViewerVisible] = useState(false);
|
|
14
|
+
const [viewerIndex, setViewerIndex] = useState(0);
|
|
15
|
+
|
|
16
|
+
const viewerImages = useMemo(
|
|
17
|
+
() => toViewerImages(normalizeImages(images)),
|
|
18
|
+
[images],
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const openViewerAt = useCallback(
|
|
22
|
+
(index: number) => {
|
|
23
|
+
onImagePress?.(index);
|
|
24
|
+
setViewerIndex(index);
|
|
25
|
+
setViewerVisible(true);
|
|
26
|
+
},
|
|
27
|
+
[onImagePress],
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const closeViewer = useCallback(() => {
|
|
31
|
+
setViewerVisible(false);
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
if (!viewerImages.length) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<View>
|
|
40
|
+
<ImageCollage
|
|
41
|
+
images={images}
|
|
42
|
+
onImagePress={openViewerAt}
|
|
43
|
+
{...collageProps}
|
|
44
|
+
/>
|
|
45
|
+
{renderViewer({
|
|
46
|
+
images: viewerImages,
|
|
47
|
+
visible: viewerVisible,
|
|
48
|
+
imageIndex: viewerIndex,
|
|
49
|
+
onRequestClose: closeViewer,
|
|
50
|
+
})}
|
|
51
|
+
</View>
|
|
52
|
+
);
|
|
53
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import React, { memo, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_BORDER_RADIUS,
|
|
5
|
+
DEFAULT_LAYOUT_MAX_HEIGHT,
|
|
6
|
+
DEFAULT_LAYOUT_MIN_HEIGHT,
|
|
7
|
+
DEFAULT_MAX_VISIBLE_IMAGES,
|
|
8
|
+
DEFAULT_PLACEHOLDER_COLOR,
|
|
9
|
+
DEFAULT_SPACING,
|
|
10
|
+
} from "./constants";
|
|
11
|
+
import { useContainerWidth } from "./hooks/useContainerWidth";
|
|
12
|
+
import type {
|
|
13
|
+
CollageImageRenderer,
|
|
14
|
+
ImageCollageProps,
|
|
15
|
+
ImagePriority,
|
|
16
|
+
} from "./types";
|
|
17
|
+
import { computeLayoutHeight } from "./utils/layoutHeight";
|
|
18
|
+
import {
|
|
19
|
+
normalizeImages,
|
|
20
|
+
resolveImagesWithAspectRatios,
|
|
21
|
+
} from "./utils/imageSources";
|
|
22
|
+
import {
|
|
23
|
+
collageLayoutStyles,
|
|
24
|
+
getCollageLayoutStyle,
|
|
25
|
+
renderCollageContent,
|
|
26
|
+
} from "./utils/renderCollageLayouts";
|
|
27
|
+
|
|
28
|
+
type SharedTileConfig = {
|
|
29
|
+
onPress?: (index: number) => void;
|
|
30
|
+
borderRadius: number;
|
|
31
|
+
placeholderColor: string;
|
|
32
|
+
getImagePriority?: (index: number) => ImagePriority;
|
|
33
|
+
renderImage?: CollageImageRenderer;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const ImageCollage = memo(function ImageCollage({
|
|
37
|
+
images,
|
|
38
|
+
height,
|
|
39
|
+
width,
|
|
40
|
+
horizontalInset = 0,
|
|
41
|
+
borderRadius = DEFAULT_BORDER_RADIUS,
|
|
42
|
+
spacing = DEFAULT_SPACING,
|
|
43
|
+
maxVisibleImages = DEFAULT_MAX_VISIBLE_IMAGES,
|
|
44
|
+
onImagePress,
|
|
45
|
+
layoutMinHeight = DEFAULT_LAYOUT_MIN_HEIGHT,
|
|
46
|
+
layoutMaxHeight = DEFAULT_LAYOUT_MAX_HEIGHT,
|
|
47
|
+
placeholderColor = DEFAULT_PLACEHOLDER_COLOR,
|
|
48
|
+
measureAspectRatios = true,
|
|
49
|
+
getImagePriority,
|
|
50
|
+
renderImage,
|
|
51
|
+
style,
|
|
52
|
+
}: ImageCollageProps) {
|
|
53
|
+
const effectiveMaxVisible = Math.max(1, maxVisibleImages);
|
|
54
|
+
const normalizedImages = useMemo(() => normalizeImages(images), [images]);
|
|
55
|
+
const [resolvedImages, setResolvedImages] = useState(normalizedImages);
|
|
56
|
+
const [isResolving, setIsResolving] = useState(false);
|
|
57
|
+
|
|
58
|
+
const { containerWidth, onLayout } = useContainerWidth({
|
|
59
|
+
width,
|
|
60
|
+
horizontalInset,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
let cancelled = false;
|
|
65
|
+
|
|
66
|
+
if (!measureAspectRatios) {
|
|
67
|
+
setResolvedImages(normalizedImages);
|
|
68
|
+
setIsResolving(false);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const needsMeasurement = normalizedImages.some(
|
|
73
|
+
(image) => image.aspectRatio == null,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
if (!needsMeasurement) {
|
|
77
|
+
setResolvedImages(normalizedImages);
|
|
78
|
+
setIsResolving(false);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
setIsResolving(true);
|
|
83
|
+
resolveImagesWithAspectRatios(normalizedImages)
|
|
84
|
+
.then((nextImages) => {
|
|
85
|
+
if (!cancelled) {
|
|
86
|
+
setResolvedImages(nextImages);
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
.finally(() => {
|
|
90
|
+
if (!cancelled) {
|
|
91
|
+
setIsResolving(false);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return () => {
|
|
96
|
+
cancelled = true;
|
|
97
|
+
};
|
|
98
|
+
}, [measureAspectRatios, normalizedImages]);
|
|
99
|
+
|
|
100
|
+
const layoutHeight = computeLayoutHeight({
|
|
101
|
+
contentWidth: containerWidth,
|
|
102
|
+
images: resolvedImages,
|
|
103
|
+
height,
|
|
104
|
+
layoutMinHeight,
|
|
105
|
+
layoutMaxHeight,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const sharedTileConfig: SharedTileConfig = {
|
|
109
|
+
onPress: onImagePress,
|
|
110
|
+
borderRadius,
|
|
111
|
+
placeholderColor,
|
|
112
|
+
getImagePriority,
|
|
113
|
+
renderImage,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
if (!resolvedImages.length) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (isResolving && measureAspectRatios) {
|
|
121
|
+
return (
|
|
122
|
+
<View
|
|
123
|
+
onLayout={onLayout}
|
|
124
|
+
style={[
|
|
125
|
+
style,
|
|
126
|
+
{
|
|
127
|
+
height: layoutHeight,
|
|
128
|
+
borderRadius,
|
|
129
|
+
backgroundColor: placeholderColor,
|
|
130
|
+
overflow: "hidden",
|
|
131
|
+
},
|
|
132
|
+
]}
|
|
133
|
+
/>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const count = resolvedImages.length;
|
|
138
|
+
const { containerStyle, row } = getCollageLayoutStyle({
|
|
139
|
+
count,
|
|
140
|
+
maxVisibleImages: effectiveMaxVisible,
|
|
141
|
+
layoutHeight,
|
|
142
|
+
spacing,
|
|
143
|
+
borderRadius,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<View
|
|
148
|
+
onLayout={onLayout}
|
|
149
|
+
style={[
|
|
150
|
+
style,
|
|
151
|
+
row ? collageLayoutStyles.row : undefined,
|
|
152
|
+
containerStyle,
|
|
153
|
+
]}
|
|
154
|
+
>
|
|
155
|
+
{renderCollageContent({
|
|
156
|
+
images: resolvedImages,
|
|
157
|
+
layoutHeight,
|
|
158
|
+
spacing,
|
|
159
|
+
borderRadius,
|
|
160
|
+
placeholderColor,
|
|
161
|
+
maxVisibleImages: effectiveMaxVisible,
|
|
162
|
+
onImagePress,
|
|
163
|
+
renderImage,
|
|
164
|
+
sharedTileConfig,
|
|
165
|
+
})}
|
|
166
|
+
</View>
|
|
167
|
+
);
|
|
168
|
+
});
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const DEFAULT_BLURHASH = "LEHV6nWB2yk8pyo0adR*.7kCMdnj";
|
|
2
|
+
export const DEFAULT_PLACEHOLDER_COLOR = "#E8E8E8";
|
|
3
|
+
export const DEFAULT_SPACING = 6;
|
|
4
|
+
export const DEFAULT_BORDER_RADIUS = 12;
|
|
5
|
+
export const DEFAULT_LAYOUT_MIN_HEIGHT = 200;
|
|
6
|
+
export const DEFAULT_LAYOUT_MAX_HEIGHT = 520;
|
|
7
|
+
export const DEFAULT_MAX_VISIBLE_IMAGES = 4;
|
|
8
|
+
export const ANDROID_RIPPLE = { color: "rgba(0,0,0,0.08)" } as const;
|
|
9
|
+
|
|
10
|
+
/** @deprecated Use `DEFAULT_PLACEHOLDER_COLOR` instead. */
|
|
11
|
+
export const DEFAULT_PLACEHOLDER_BG = DEFAULT_PLACEHOLDER_COLOR;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { StyleSheet } from "react-native";
|
|
3
|
+
import { Image } from "expo-image";
|
|
4
|
+
import { DEFAULT_BLURHASH } from "../constants";
|
|
5
|
+
import type { CollageImageRenderer } from "../types";
|
|
6
|
+
|
|
7
|
+
type ExpoImageRendererOptions = {
|
|
8
|
+
blurhash?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function createExpoImageRenderer(
|
|
12
|
+
options: ExpoImageRendererOptions = {},
|
|
13
|
+
): CollageImageRenderer {
|
|
14
|
+
const blurhash = options.blurhash ?? DEFAULT_BLURHASH;
|
|
15
|
+
|
|
16
|
+
return function ExpoCollageImage({
|
|
17
|
+
source,
|
|
18
|
+
remoteUri,
|
|
19
|
+
priority,
|
|
20
|
+
transition,
|
|
21
|
+
style,
|
|
22
|
+
}) {
|
|
23
|
+
const recyclingKey = remoteUri ?? undefined;
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Image
|
|
27
|
+
source={source}
|
|
28
|
+
recyclingKey={recyclingKey}
|
|
29
|
+
cachePolicy="memory-disk"
|
|
30
|
+
allowDownscaling
|
|
31
|
+
priority={priority}
|
|
32
|
+
placeholder={blurhash}
|
|
33
|
+
placeholderContentFit="cover"
|
|
34
|
+
contentFit="cover"
|
|
35
|
+
transition={transition}
|
|
36
|
+
style={[StyleSheet.absoluteFill, style]}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Pre-built renderer using `expo-image` and the default blurhash. */
|
|
43
|
+
export const expoImageRenderer = createExpoImageRenderer();
|