Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support expo image #968

Merged
merged 15 commits into from
Dec 4, 2024
2 changes: 2 additions & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ import ThemeExample from "./ThemeExample";
import LoadingIndicatorExample from "./LoadingIndicatorExample";
import TimerExample from "./TimerExample";
import LottieAnimationExample from "./LottieAnimationExample";
import ExpoImageExample from "./ExpoImageExample";

const ROUTES = {
LottieAnimationExample: LottieAnimationExample,
Expand Down Expand Up @@ -120,6 +121,7 @@ const ROUTES = {
VideoPlayer: VideoPlayerExample,
PinInput: PinInputExample,
KeyboardAvoidingView: KeyboardAvoidingViewExample,
ExpoImage: ExpoImageExample,
};

let customFonts = {
Expand Down
171 changes: 171 additions & 0 deletions example/src/ExpoImageExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import * as React from "react";
import { View, StyleSheet, Text } from "react-native";
import { ExpoImage, withTheme } from "@draftbit/ui";
import Section, { Container } from "./Section";

interface WrapperProps {
label: string;
children: React.ReactNode;
}

const Wrapper: React.FC<WrapperProps> = ({ label, children }) => {
return (
<View style={styles.wrapper}>
<View style={styles.boxLabel}>
<Text>{label}</Text>
</View>
<View>{children}</View>
</View>
);
};

const ExpoImageExample: React.FC = () => {
return (
<Container style={{}}>
<Section title="Image Examples" style={{}}>
<View style={{ flexDirection: "row" }}>
<Wrapper label="Basic remote image">
<ExpoImage
source={{
uri: "https://picsum.photos/1100",
}}
style={{
width: 200,
height: 200,
}}
/>
</Wrapper>
<Wrapper label="Local image">
<ExpoImage
source={require("./assets/images/hamburger.png")}
style={{
width: 200,
height: 200,
}}
/>
</Wrapper>
</View>
<View style={{ flexDirection: "row" }}>
<Wrapper label="With aspectRatio">
<ExpoImage
source={{
uri: "https://picsum.photos/1200",
}}
style={{
width: 300,
aspectRatio: 16 / 9,
}}
/>
</Wrapper>
</View>
<View style={{ flexDirection: "row" }}>
<Wrapper label="Content fit: contain">
<ExpoImage
source={{
uri: "https://picsum.photos/1300",
}}
contentFit="contain"
style={{
width: 200,
height: 200,
backgroundColor: "#f0f0f0",
}}
/>
</Wrapper>
<Wrapper label="With blur hash">
<ExpoImage
source={{
uri: "https://picsum.photos/seed/696/3000/2000",
}}
blurhash="LEHLk~WB2yk8pyo0adR*.7kCMdnj"
transition={5000}
style={{
width: 200,
height: 200,
}}
/>
</Wrapper>
</View>
</Section>
<Section title="SVG Image" style={styles.wrapper}>
<Wrapper label="Remote SVG Image">
<ExpoImage
source={{
uri: "https://upload.wikimedia.org/wikipedia/commons/3/30/Vector-based_example.svg",
}}
style={{
width: 200,
height: 200,
}}
/>
</Wrapper>
<Wrapper label="Local SVG Image">
<ExpoImage
source={require("./assets/images/example.svg")}
style={{
width: 200,
height: 200,
}}
/>
</Wrapper>
</Section>
<Section title="Transition Effects" style={{}}>
<View style={{ flexDirection: "row", flexWrap: "wrap" }}>
<Wrapper label="Cross Dissolve - Ease In Out">
<ExpoImage
source={{ uri: "https://picsum.photos/1400" }}
transitionDuration={3000}
transitionEffect="cross-dissolve"
transitionTiming="ease-in-out"
style={{ width: 200, height: 200 }}
/>
</Wrapper>
<Wrapper label="Flip from Top - Ease Out">
<ExpoImage
source={{ uri: "https://picsum.photos/1500" }}
transitionDuration={3000}
transitionEffect="flip-from-top"
transitionTiming="ease-out"
style={{ width: 200, height: 200 }}
/>
</Wrapper>
<Wrapper label="Curl Up - Linear">
<ExpoImage
source={{ uri: "https://picsum.photos/1600" }}
transitionDuration={3000}
transitionEffect="curl-up"
transitionTiming="linear"
style={{ width: 200, height: 200 }}
/>
</Wrapper>
<Wrapper label="Cross Dissolve - Ease In Out">
<ExpoImage
source={{ uri: "https://picsum.photos/1700" }}
transitionDuration={3000}
transitionEffect="cross-dissolve"
transitionTiming="ease-in-out"
style={{ width: 200, height: 200 }}
/>
</Wrapper>
</View>
</Section>
</Container>
);
};

const styles = StyleSheet.create({
wrapper: {
flex: 1,
display: "flex",
flexDirection: "column",
flexWrap: "wrap",
justifyContent: "center",
alignItems: "center",
},
boxLabel: {
margin: 10,
flex: 1,
},
});

export default withTheme(ExpoImageExample);
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"date-fns": "^2.16.1",
"dateformat": "^3.0.3",
"expo-av": "~13.10.6",
"expo-image": "1.10.6",
"lodash.isequal": "^4.5.0",
"lodash.isnumber": "^3.0.3",
"lodash.omit": "^4.5.0",
Expand Down
165 changes: 165 additions & 0 deletions packages/core/src/components/ExpoImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import React from "react";
import { StyleSheet, ImageSourcePropType, DimensionValue } from "react-native";
import {
Image,
ImageContentPosition,
ImageProps as ExpoImageProps,
ImageContentFit,
} from "expo-image";
import Config from "./Config";
import AspectRatio from "./AspectRatio";

type ImageStyleProp = {
width?: number;
height?: number;
aspectRatio?: number;
};

interface ExtendedImageProps extends ExpoImageProps {
transitionDuration?: number;
transitionEffect?:
| "cross-dissolve"
| "flip-from-top"
| "flip-from-right"
| "flip-from-bottom"
| "flip-from-left"
| "curl-up"
| "curl-down";
transitionTiming?: "ease-in-out" | "ease-in" | "ease-out" | "linear";
contentFit?: "cover" | "contain" | "fill" | "none" | "scale-down";
contentPosition?: ImageContentPosition;
cachePolicy?: "none" | "disk" | "memory" | "memory-disk";
allowDownscaling?: boolean;
blurRadius?: number;
blurhash?: string;
}

const generateDimensions = ({
aspectRatio,
width,
height,
}: ImageStyleProp): {
aspectRatio?: number;
width?: DimensionValue;
height?: DimensionValue;
} => {
if (aspectRatio && !width && !height) {
return {
aspectRatio,
width: "100%",
};
}

if (aspectRatio && height) {
return {
aspectRatio,
height,
width: aspectRatio * height,
};
}

if (aspectRatio && width) {
return {
aspectRatio,
width,
height: width / aspectRatio,
};
}

return { width, height };
};

const resizeModeToContentFit = (
resizeMode: "cover" | "contain" | "stretch" | "repeat" | "center"
): ImageContentFit => {
const mapping: Record<typeof resizeMode, ImageContentFit> = {
cover: "cover",
contain: "contain",
stretch: "fill",
repeat: "none",
center: "scale-down",
} as const;
return mapping[resizeMode] ?? "cover";
};

const ExpoImage: React.FC<ExtendedImageProps> = ({
source,
resizeMode = "cover",
style,
transitionDuration = 300,
transitionEffect = "cross-dissolve",
transitionTiming = "ease-in-out",
contentFit = "cover",
contentPosition = "center",
cachePolicy = "memory-disk",
allowDownscaling = true,
blurRadius,
blurhash,
...props
}) => {
let imageSource =
source === null || source === undefined
? Config.placeholderImageURL
: source;
sieu-db marked this conversation as resolved.
Show resolved Hide resolved

const styles = StyleSheet.flatten(style || {});
const { aspectRatio, width, height } = generateDimensions(
styles as ImageStyleProp
);

const finalContentFit = resizeMode
? resizeModeToContentFit(resizeMode)
: contentFit;

const transition = {
timing: transitionTiming,
duration: transitionDuration,
effect: transitionEffect,
};

if (aspectRatio) {
return (
<AspectRatio style={[style, { width, height, aspectRatio }]}>
sieu-db marked this conversation as resolved.
Show resolved Hide resolved
<Image
{...props}
source={imageSource as ImageSourcePropType}
contentFit={finalContentFit}
placeholder={{
blurhash,
}}
transition={transition}
contentPosition={contentPosition}
cachePolicy={cachePolicy}
allowDownscaling={allowDownscaling}
blurRadius={blurRadius}
style={[
style,
{
height: "100%",
width: "100%",
},
]}
/>
</AspectRatio>
);
}

return (
<Image
{...props}
source={source as ImageSourcePropType}
contentFit={finalContentFit}
placeholder={{
blurhash,
}}
transition={transition}
contentPosition={contentPosition}
cachePolicy={cachePolicy}
allowDownscaling={allowDownscaling}
blurRadius={blurRadius}
style={style}
/>
);
};

export default ExpoImage;
1 change: 1 addition & 0 deletions packages/core/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export { default as SimpleStyleSwipeableList } from "./components/SimpleStyleScr
export { default as LoadingIndicator } from "./components/LoadingIndicator";
export { default as LottieAnimation } from "./components/LottieAnimation";
export { default as Timer } from "./components/Timer";
export { default as ExpoImage } from "./components/ExpoImage";

/* Deprecated: Fix or Delete! */
export { default as AccordionItem } from "./deprecated-components/AccordionItem";
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ export {
LoadingIndicator,
LottieAnimation,
Timer,
Image,
ExpoImage,
} from "@draftbit/core";

export {
Expand Down
7 changes: 7 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7529,6 +7529,13 @@ expo-font@~11.10.3:
dependencies:
fontfaceobserver "^2.1.0"

[email protected]:
version "1.10.6"
resolved "https://registry.yarnpkg.com/expo-image/-/expo-image-1.10.6.tgz#b0e54d31d97742505296c076a5f18d094ba9a8cc"
integrity sha512-vcnAIym1eU8vQgV1re1E7rVQZStJimBa4aPDhjFfzMzbddAF7heJuagyewiUkTzbZUwYzPaZAie6VJPyWx9Ueg==
dependencies:
"@react-native/assets-registry" "~0.73.1"

expo-keep-awake@~12.8.2:
version "12.8.2"
resolved "https://registry.yarnpkg.com/expo-keep-awake/-/expo-keep-awake-12.8.2.tgz#6cfdf8ad02b5fa130f99d4a1eb98e459d5b4332e"
Expand Down
Loading