Skip to content

Commit

Permalink
chore: Refactor lightbox (#2452)
Browse files Browse the repository at this point in the history
* chore: Refactor lightbox

* feat: Lightbox soft fade on press (#2453)
  • Loading branch information
jimhunty authored Jul 10, 2024
1 parent 9de248a commit 5744fa3
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 97 deletions.
2 changes: 1 addition & 1 deletion projects/Mallard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"@guardian/pasteup": "^1.0.0-alpha.11",
"@guardian/renditions": "^0.2.0",
"@guardian/src-foundations": "^2.5.0-rc.1",
"@likashefqet/react-native-image-zoom": "^3.0.0",
"@okta/okta-react-native": "^2.8.0",
"@react-native-async-storage/async-storage": "^1.22.0",
"@react-native-clipboard/clipboard": "^1.14.0",
Expand Down Expand Up @@ -74,7 +75,6 @@
"react-native-fs": "^2.20.0",
"react-native-gesture-handler": "2.14.0",
"react-native-iap": "^12.13.0",
"react-native-image-zoom-viewer": "^3.0.1",
"react-native-in-app-utils": "^6.0.2",
"react-native-inappbrowser-reborn": "^3.3.3",
"react-native-keychain": "8.1.2",
Expand Down
238 changes: 142 additions & 96 deletions projects/Mallard/src/screens/lightbox.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { palette } from '@guardian/pasteup/palette';
import { ImageZoom } from '@likashefqet/react-native-image-zoom';
import type { RouteProp } from '@react-navigation/native';
import { useNavigation, useRoute } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import React, { useEffect, useState } from 'react';
import { StatusBar, StyleSheet, View } from 'react-native';
import ImageViewer from 'react-native-image-zoom-viewer';
import React, { useCallback, useRef, useState } from 'react';
import {
Animated,
FlatList,
SafeAreaView,
StatusBar,
StyleSheet,
View,
} from 'react-native';
import { ArticleTheme } from 'src/components/article/article';
import { themeColors } from 'src/components/article/helpers/css';
import {
Expand All @@ -15,6 +22,7 @@ import {
import { ButtonAppearance } from 'src/components/Button/Button';
import { CloseButton } from 'src/components/Button/CloseButton';
import { LightboxCaption } from 'src/components/Lightbox/LightboxCaption';
import { clamp } from 'src/helpers/math';
import { getPillarColors } from 'src/helpers/transform';
import { useDimensions } from 'src/hooks/use-config-provider';
import type {
Expand Down Expand Up @@ -63,8 +71,8 @@ const styles = StyleSheet.create({
position: 'absolute',
zIndex: 1,
right: 0,
paddingTop: 20,
paddingRight: 20,
paddingVertical: 10,
paddingHorizontal: 10,
top: 0,
},
progressWrapper: {
Expand All @@ -85,130 +93,168 @@ const LightboxScreen = () => {
const images = route.params?.images ?? [];
const index = route.params?.index ?? 0;
const pillar = route.params?.pillar ?? 'news';
const numDots = images.length < 6 ? images.length : 6;
const pillarColors = getPillarColors(pillar);
const [windowStart, setWindowsStart] = useState(0);
const [windowStart, setWindowsStart] = useState(
getWindowStart(index, numDots, images.length),
);
const [currentIndex, setCurrentIndex] = useState(index);

const numDots = images.length < 6 ? images.length : 6;
const showProgressIndicator = images.length > 1;

const [captionVisible, setCaptionVisible] = useState(false);
const [showControls, setShowControls] = useState(true);

const [dotsVisible, setDotsVisible] = useState(false);
const [scrollInProgress, setScrollInProgress] = useState(false);

const showProgressIndicator = images.length > 1 ? dotsVisible : false;
const { width } = useDimensions();

const [closeButtonVisible, setCloseButtonVisible] = useState(false);
const sliderPosition = useRef(new Animated.Value(0)).current;

const [scrollInProgress, setScrollInProgress] = useState(false);
const fadeAnim = useRef(new Animated.Value(1)).current;

const { width } = useDimensions();
const toggleControls = useCallback(
(toggle: boolean) => {
if (toggle) {
Animated.timing(fadeAnim, {
toValue: 1,
duration: 100,
useNativeDriver: true,
}).start();
} else {
Animated.timing(fadeAnim, {
toValue: 0,
duration: 100,
useNativeDriver: true,
}).start();
}
setShowControls(toggle);
},
[fadeAnim],
);

const handleScrollStartEvent = () => {
const handleScrollStartEvent = useCallback(() => {
setScrollInProgress(true);
};
const handleOnMoveEvent = (index: number) => {
setCurrentIndex(index);
setWindowsStart(
getNewWindowStart(index, windowStart, images.length, numDots),
);
setScrollInProgress(false);
};
}, [setScrollInProgress]);

const focusOnImageComponent = () => {
setCaptionVisible(!captionVisible);
setDotsVisible(!dotsVisible);
setCloseButtonVisible(!closeButtonVisible);
};
const focusOnImageComponent = useCallback(() => {
toggleControls(!showControls);
}, [toggleControls, showControls]);

const lightboxImages = [];
for (let i = 0; i < images.length; i++) {
lightboxImages.push({
url: imagePaths[i],
props: {
alignSelf: 'center',
height: '100%',
width: '100%',
resizeMode: 'contain',
},
});
}

useEffect(() => {
setCaptionVisible(true);
setDotsVisible(true);
setCloseButtonVisible(true);
setCurrentIndex(index);
setWindowsStart(getWindowStart(index, numDots, images.length));
}, [index, numDots, images.length]);
const onScrollListener = useCallback(
(ev: any) => {
const newPos = ev.nativeEvent.contentOffset.x / width;
const newIndex = clamp(Math.round(newPos), 0, images.length - 1);
if (currentIndex !== newIndex) {
setCurrentIndex(newIndex);
setWindowsStart(
getNewWindowStart(
newIndex,
windowStart,
images.length,
numDots,
),
);
}
},
[setCurrentIndex, currentIndex, windowStart, numDots, images.length],
);

return (
<View style={styles.background}>
<SafeAreaView style={styles.background}>
<StatusBar hidden={true} />
<View style={styles.lightboxPage}>
<View style={styles.closeButton}>
{closeButtonVisible && (
<Animated.View style={{ opacity: fadeAnim }}>
<CloseButton
onPress={() => {
navigation.goBack();
showControls && navigation.goBack();
}}
accessibilityLabel="Close Image Gallery"
accessibilityHint="This will close the Image Gallery which is currently open"
appearance={ButtonAppearance.Pillar}
pillar={pillar}
/>
)}
</Animated.View>
</View>
<View style={styles.imageWrapper}>
<ImageViewer
imageUrls={lightboxImages}
index={index}
renderIndicator={() => <View />} // empty indicator
onClick={focusOnImageComponent}
onMove={handleScrollStartEvent}
onChange={(index) => handleOnMoveEvent(index ?? 0)} // seems that first index is nil?
saveToLocalByLongPress={false}
maxOverflow={width}
enablePreload={true}
footerContainerStyle={{
position: 'absolute',
bottom: 0,
width: '100%',
<FlatList
initialScrollIndex={index}
contentContainerStyle={{
alignItems: 'center',
}}
renderFooter={() => (
<View>
<View style={styles.progressWrapper}>
{showProgressIndicator && (
<ProgressIndicator
currentIndex={currentIndex}
imageCount={images.length}
windowSize={numDots}
windowStart={windowStart}
scrollInProgress={scrollInProgress}
/>
)}
</View>
{captionVisible && (
<LightboxCaption
caption={
images[currentIndex].caption ?? ''
}
pillarColor={
pillar === 'neutral'
? palette.neutral[100]
: pillarColors.bright //bright since always on a dark background
}
displayCredit={
images[currentIndex].displayCredit
}
credit={images[currentIndex].credit}
/>
)}
</View>
onScroll={Animated.event(
[
{
nativeEvent: {
contentOffset: {
x: sliderPosition,
},
},
},
],
{
useNativeDriver: false,
listener: onScrollListener,
},
)}
horizontal
pagingEnabled
onScrollBeginDrag={handleScrollStartEvent}
onScrollEndDrag={() => setScrollInProgress(false)}
renderItem={({ item }) => {
console.log(item);
return (
<View style={{ width, height: '100%' }}>
<ImageZoom
uri={item}
onSingleTap={focusOnImageComponent}
isSingleTapEnabled
isDoubleTapEnabled
/>
</View>
);
}}
data={imagePaths}
getItemLayout={(_, index) => ({
length: width,
offset: width * index,
index,
})}
keyExtractor={(item) => item}
/>

<View>
<View style={styles.progressWrapper}>
{showProgressIndicator && (
<Animated.View style={{ opacity: fadeAnim }}>
<ProgressIndicator
currentIndex={currentIndex}
imageCount={images.length}
windowSize={numDots}
windowStart={windowStart}
scrollInProgress={scrollInProgress}
/>
</Animated.View>
)}
</View>
<Animated.View style={{ opacity: fadeAnim }}>
<LightboxCaption
caption={images[currentIndex].caption ?? ''}
pillarColor={
pillar === 'neutral'
? palette.neutral[100]
: pillarColors.bright //bright since always on a dark background
}
displayCredit={
images[currentIndex].displayCredit
}
credit={images[currentIndex].credit}
/>
</Animated.View>
</View>
</View>
</View>
</View>
</SafeAreaView>
);
};

Expand Down
5 changes: 5 additions & 0 deletions projects/Mallard/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1686,6 +1686,11 @@
resolved "https://registry.yarnpkg.com/@jsamr/react-native-li/-/react-native-li-2.3.1.tgz#12a5b5f6e3971cec77b96bee58104eed0ae9314a"
integrity sha512-Qbo4NEj48SQ4k8FZJHFE2fgZDKTWaUGmVxcIQh3msg5JezLdTMMHuRRDYctfdHI6L0FZGObmEv3haWbIvmol8w==

"@likashefqet/react-native-image-zoom@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@likashefqet/react-native-image-zoom/-/react-native-image-zoom-3.0.0.tgz#e7bc652238c1acb36c87663b29268556ecb037a0"
integrity sha512-vDg7j9wIYiMsYM8qymdPIC9+ZHJGZ3FvcN8Ob1zTlNSaEFybH9+7Qw7AbO4MaNEAFqhF7C8j09jw7BPGpXI29w==

"@native-html/[email protected]":
version "1.11.0"
resolved "https://registry.yarnpkg.com/@native-html/css-processor/-/css-processor-1.11.0.tgz#27d02e5123b0849f4986d44060ba3f235a15f552"
Expand Down

0 comments on commit 5744fa3

Please sign in to comment.