Fix layout shift for multi-image posts (#1673)

* Fix layout shift for multi-image posts

* Add a comment for the hack

* Use margins instead of gap

* Move alt label
zio/stable
dan 2023-10-12 19:14:27 +01:00 committed by GitHub
parent d98e3a8b45
commit c2a1cf4e56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 65 additions and 66 deletions

View File

@ -23,19 +23,19 @@ export const GalleryItem: FC<GalleryItemProps> = ({
onLongPress, onLongPress,
}) => { }) => {
const image = images[index] const image = images[index]
return ( return (
<View> <View style={styles.fullWidth}>
<Pressable <Pressable
onPress={onPress ? () => onPress(index) : undefined} onPress={onPress ? () => onPress(index) : undefined}
onPressIn={onPressIn ? () => onPressIn(index) : undefined} onPressIn={onPressIn ? () => onPressIn(index) : undefined}
onLongPress={onLongPress ? () => onLongPress(index) : undefined} onLongPress={onLongPress ? () => onLongPress(index) : undefined}
style={styles.fullWidth}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={image.alt || 'Image'} accessibilityLabel={image.alt || 'Image'}
accessibilityHint=""> accessibilityHint="">
<Image <Image
source={{uri: image.thumb}} source={{uri: image.thumb}}
style={imageStyle} style={[styles.image, imageStyle]}
accessible={true} accessible={true}
accessibilityLabel={image.alt} accessibilityLabel={image.alt}
accessibilityHint="" accessibilityHint=""
@ -54,14 +54,21 @@ export const GalleryItem: FC<GalleryItemProps> = ({
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
fullWidth: {
flex: 1,
},
image: {
flex: 1,
borderRadius: 4,
},
altContainer: { altContainer: {
backgroundColor: 'rgba(0, 0, 0, 0.75)', backgroundColor: 'rgba(0, 0, 0, 0.75)',
borderRadius: 6, borderRadius: 6,
paddingHorizontal: 6, paddingHorizontal: 6,
paddingVertical: 3, paddingVertical: 3,
position: 'absolute', position: 'absolute',
left: 6, left: 8,
bottom: 6, bottom: 8,
}, },
alt: { alt: {
color: 'white', color: 'white',

View File

@ -1,13 +1,5 @@
import React, {useMemo, useState} from 'react' import React from 'react'
import { import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
LayoutChangeEvent,
StyleProp,
StyleSheet,
View,
ViewStyle,
} from 'react-native'
import {ImageStyle} from 'expo-image'
import {Dimensions} from 'lib/media/types'
import {AppBskyEmbedImages} from '@atproto/api' import {AppBskyEmbedImages} from '@atproto/api'
import {GalleryItem} from './Gallery' import {GalleryItem} from './Gallery'
@ -20,21 +12,11 @@ interface ImageLayoutGridProps {
} }
export function ImageLayoutGrid({style, ...props}: ImageLayoutGridProps) { export function ImageLayoutGrid({style, ...props}: ImageLayoutGridProps) {
const [containerInfo, setContainerInfo] = useState<Dimensions | undefined>()
const onLayout = (evt: LayoutChangeEvent) => {
const {width, height} = evt.nativeEvent.layout
setContainerInfo({
width,
height,
})
}
return ( return (
<View style={style} onLayout={onLayout}> <View style={style}>
{containerInfo ? ( <View style={styles.container}>
<ImageLayoutGridInner {...props} containerInfo={containerInfo} /> <ImageLayoutGridInner {...props} />
) : undefined} </View>
</View> </View>
) )
} }
@ -44,70 +26,80 @@ interface ImageLayoutGridInnerProps {
onPress?: (index: number) => void onPress?: (index: number) => void
onLongPress?: (index: number) => void onLongPress?: (index: number) => void
onPressIn?: (index: number) => void onPressIn?: (index: number) => void
containerInfo: Dimensions
} }
function ImageLayoutGridInner({ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
containerInfo,
...props
}: ImageLayoutGridInnerProps) {
const count = props.images.length const count = props.images.length
const size1 = useMemo<ImageStyle>(() => {
if (count === 3) {
const size = (containerInfo.width - 10) / 3
return {width: size, height: size, resizeMode: 'cover', borderRadius: 4}
} else {
const size = (containerInfo.width - 5) / 2
return {width: size, height: size, resizeMode: 'cover', borderRadius: 4}
}
}, [count, containerInfo])
const size2 = React.useMemo<ImageStyle>(() => {
if (count === 3) {
const size = ((containerInfo.width - 10) / 3) * 2 + 5
return {width: size, height: size, resizeMode: 'cover', borderRadius: 4}
} else {
const size = (containerInfo.width - 5) / 2
return {width: size, height: size, resizeMode: 'cover', borderRadius: 4}
}
}, [count, containerInfo])
switch (count) { switch (count) {
case 2: case 2:
return ( return (
<View style={styles.flexRow}> <View style={styles.flexRow}>
<GalleryItem index={0} {...props} imageStyle={size1} /> <View style={styles.smallItem}>
<GalleryItem index={1} {...props} imageStyle={size1} /> <GalleryItem {...props} index={0} imageStyle={styles.image} />
</View>
<View style={styles.smallItem}>
<GalleryItem {...props} index={1} imageStyle={styles.image} />
</View>
</View> </View>
) )
case 3: case 3:
return ( return (
<View style={styles.flexRow}> <View style={styles.flexRow}>
<GalleryItem index={0} {...props} imageStyle={size2} /> <View style={{flex: 2, aspectRatio: 1}}>
<View style={styles.flexColumn}> <GalleryItem {...props} index={0} imageStyle={styles.image} />
<GalleryItem index={1} {...props} imageStyle={size1} /> </View>
<GalleryItem index={2} {...props} imageStyle={size1} /> <View style={{flex: 1}}>
<View style={styles.smallItem}>
<GalleryItem {...props} index={1} imageStyle={styles.image} />
</View>
<View style={styles.smallItem}>
<GalleryItem {...props} index={2} imageStyle={styles.image} />
</View>
</View> </View>
</View> </View>
) )
case 4: case 4:
return ( return (
<View style={styles.flexRow}> <View style={styles.flexRow}>
<View style={styles.flexColumn}> <View style={{flex: 1}}>
<GalleryItem index={0} {...props} imageStyle={size1} /> <View style={styles.smallItem}>
<GalleryItem index={2} {...props} imageStyle={size1} /> <GalleryItem {...props} index={0} imageStyle={styles.image} />
</View>
<View style={styles.smallItem}>
<GalleryItem {...props} index={2} imageStyle={styles.image} />
</View>
</View> </View>
<View style={styles.flexColumn}> <View style={{flex: 1}}>
<GalleryItem index={1} {...props} imageStyle={size1} /> <View style={styles.smallItem}>
<GalleryItem index={3} {...props} imageStyle={size1} /> <GalleryItem {...props} index={1} imageStyle={styles.image} />
</View>
<View style={styles.smallItem}>
<GalleryItem {...props} index={3} imageStyle={styles.image} />
</View>
</View> </View>
</View> </View>
) )
default: default:
return null return null
} }
} }
// This is used to compute margins (rather than flexbox gap) due to Yoga bugs:
// https://github.com/facebook/yoga/issues/1418
const IMAGE_GAP = 5
const styles = StyleSheet.create({ const styles = StyleSheet.create({
flexRow: {flexDirection: 'row', gap: 5}, container: {
flexColumn: {flexDirection: 'column', gap: 5}, marginHorizontal: -IMAGE_GAP / 2,
marginVertical: -IMAGE_GAP / 2,
},
flexRow: {flexDirection: 'row'},
smallItem: {flex: 1, aspectRatio: 1},
image: {
margin: IMAGE_GAP / 2,
},
}) })