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 labelzio/stable
parent
d98e3a8b45
commit
c2a1cf4e56
|
@ -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',
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue