Skip to content

Commit

Permalink
refactor: update gallery example into image/video mix
Browse files Browse the repository at this point in the history
  • Loading branch information
Glazzes committed Jul 28, 2024
1 parent 5ea7999 commit 784d088
Show file tree
Hide file tree
Showing 9 changed files with 614 additions and 20 deletions.
46 changes: 42 additions & 4 deletions example/src/gallery/GalleryExample.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { View } from 'react-native';
import { useSharedValue } from 'react-native-reanimated';
import { useSharedValue, withTiming } from 'react-native-reanimated';
import {
stackTransition,
Gallery,
type GalleryType,
} from 'react-native-zoom-toolkit';
import {
getAssetsAsync,
MediaType,
requestPermissionsAsync,
type Asset,
} from 'expo-media-library';

import GalleryImage from './GalleryImage';
import { StyleSheet } from 'react-native';
import VideoControls from './controls/VideoControls';
import GalleryVideo from './GalleryVideo';

type SizeVector = { width: number; height: number };

Expand All @@ -23,28 +26,54 @@ const GalleryExample = () => {
const [assets, setAssets] = useState<Asset[]>([]);
const [scales, setScales] = useState<SizeVector[]>([]);

const progress = useSharedValue<number>(0);
const opacityControls = useSharedValue<number>(0);
const activeIndex = useSharedValue<number>(0);

// This value is used to prevent the timer to keep updating the current position
// when the user is dragging the bar to a position of their desire
const isSeeking = useSharedValue<boolean>(false);

const renderItem = useCallback(
(item: Asset, index: number) => {
if (item.mediaType === MediaType.video) {
return (
<GalleryVideo
asset={item}
index={index}
isLooping={true}
progress={progress}
isSeeking={isSeeking}
/>
);
}

return (
<GalleryImage asset={item} index={index} activeIndex={activeIndex} />
);
},
[activeIndex]
[activeIndex, progress, isSeeking]
);

const keyExtractor = useCallback((item, index) => `${item.uri}-${index}`, []);

const customTransition = useCallback(stackTransition, []);

// Toogle video controls opacity if the current item is a video
const onTap = useCallback(() => {
const isVideo = assets[activeIndex.value]?.mediaType === MediaType.video;
if (!isVideo) return;

const toValue = opacityControls.value > 0 ? 0 : 1;
opacityControls.value = withTiming(toValue);
}, [assets, activeIndex, opacityControls]);

useEffect(() => {
const requestAssets = async () => {
const { granted } = await requestPermissionsAsync();
if (granted) {
const page = await getAssetsAsync({
first: 100,
mediaType: 'photo',
mediaType: ['photo', 'video'],
sortBy: 'creationTime',
});

Expand Down Expand Up @@ -76,8 +105,17 @@ const GalleryExample = () => {
onIndexChange={(idx) => {
activeIndex.value = idx;
}}
onTap={onTap}
customTransition={customTransition}
/>

<VideoControls
assets={assets}
activeIndex={activeIndex}
progress={progress}
isSeeking={isSeeking}
opacity={opacityControls}
/>
</View>
);
};
Expand Down
22 changes: 6 additions & 16 deletions example/src/gallery/GalleryImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from 'react-native-reanimated';
import { type Asset } from 'expo-media-library';

import { getAspectRatioSize } from 'react-native-zoom-toolkit';
import { calculateItemSize } from './utils/utils';

type GalleryImageProps = {
asset: Asset;
Expand All @@ -24,21 +24,11 @@ const GalleryImage: React.FC<GalleryImageProps> = ({
const [downScale, setDownScale] = useState<boolean>(true);
const { width, height } = useWindowDimensions();

const phoneRatio = width / height;
const pictureRatio = asset.width / asset.height;
let size = getAspectRatioSize({
aspectRatio: pictureRatio,
width: height > width ? width : undefined,
height: height > width ? undefined : height,
});

if (pictureRatio > phoneRatio && phoneRatio > 1) {
size = getAspectRatioSize({ aspectRatio: pictureRatio, width });
}

if (pictureRatio < phoneRatio && phoneRatio < 1) {
size = getAspectRatioSize({ aspectRatio: pictureRatio, height });
}
const size = calculateItemSize(
{ width: asset.width, height: asset.height },
{ width, height },
width / height
);

const wrapper = (active: number) => {
if (index === active) setDownScale(false);
Expand Down
97 changes: 97 additions & 0 deletions example/src/gallery/GalleryVideo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React, { useEffect, useRef } from 'react';
import { useWindowDimensions } from 'react-native';
import { ResizeMode, Video, type AVPlaybackStatus } from 'expo-av';
import type { Asset } from 'expo-media-library';

import { calculateItemSize } from './utils/utils';
import {
listenToPauseVideoEvent,
listenToPlayVideoEvent,
listenToSeekVideoEvent,
listenToStopVideoEvent,
} from './utils/emitter';
import { type SharedValue } from 'react-native-reanimated';

type GalleryVideoProps = {
asset: Asset;
index: number;
isLooping: boolean;
isSeeking: SharedValue<boolean>;
progress: SharedValue<number>;
};

const GalleryVideo: React.FC<GalleryVideoProps> = ({
asset,
index,
isLooping,
isSeeking,
progress,
}) => {
const videoRef = useRef<Video>(null);
const { width, height } = useWindowDimensions();

const phoneAspectRatio = width / height;
const size = calculateItemSize(
{ width: asset.width, height: asset.height },
{ width, height },
phoneAspectRatio
);

const playback = (status: AVPlaybackStatus) => {
if (status.isLoaded && !isSeeking.value) {
progress.value = status.positionMillis / (asset.duration * 1000);
}
};

const play = (activeIndex: number) => {
if (activeIndex !== index) return;
videoRef.current?.playAsync();
};

const pause = (activeIndex: number) => {
if (activeIndex !== index) return;
videoRef.current?.pauseAsync();
};

const stop = () => {
videoRef.current?.stopAsync().finally(() => (isSeeking.value = false));
};

const seek = async (options: { positionMillis: number; index: number }) => {
const { positionMillis, index: activeIndex } = options;
if (index !== activeIndex) return;

await videoRef.current?.setPositionAsync(positionMillis);
progress.value = positionMillis / (asset.duration * 1000);
isSeeking.value = false;
};

useEffect(() => {
const playSub = listenToPlayVideoEvent(play);
const pauseSub = listenToPauseVideoEvent(pause);
const stopSub = listenToStopVideoEvent(stop);
const seekSub = listenToSeekVideoEvent(seek);

return () => {
playSub.remove();
pauseSub.remove();
stopSub.remove();
seekSub.remove();
};
});

return (
<Video
ref={videoRef}
source={{ uri: asset.uri }}
style={{ ...size }}
resizeMode={ResizeMode.COVER}
shouldPlay={false}
isLooping={isLooping}
progressUpdateIntervalMillis={50}
onPlaybackStatusUpdate={playback}
/>
);
};

export default GalleryVideo;
43 changes: 43 additions & 0 deletions example/src/gallery/controls/ReText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Taken from https://github.com/wcandillon/react-native-redash/blob/master/src/ReText.tsx
import React from 'react';
import type { TextProps as RNTextProps } from 'react-native';
import { StyleSheet, TextInput } from 'react-native';
import Animated, {
useAnimatedProps,
type SharedValue,
} from 'react-native-reanimated';

const styles = StyleSheet.create({
baseStyle: {
color: 'black',
},
});

type ReTextProps = {
text: SharedValue<string>;
style?: RNTextProps['style'];
};

const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);
Animated.addWhitelistedNativeProps({ text: true });

const ReText: React.FC<ReTextProps> = ({ text, style }) => {
const animatedProps = useAnimatedProps(() => {
return {
text: text.value,
// Here we use any because the text prop is not available in the type
} as any;
}, [text]);

return (
<AnimatedTextInput
animatedProps={animatedProps}
underlineColorAndroid="transparent"
editable={false}
value={text.value}
style={[styles.baseStyle, style || undefined]}
/>
);
};

export default ReText;
Loading

0 comments on commit 784d088

Please sign in to comment.