Fix layout shift when liking a post on Android (#10190)
This commit is contained in:
@@ -8,10 +8,7 @@ import Animated, {
|
||||
} from 'react-native-reanimated'
|
||||
|
||||
import {decideShouldRoll} from '#/lib/custom-animations/util'
|
||||
import {s} from '#/lib/styles'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {useFormatPostStatCount} from '#/components/PostControls/util'
|
||||
import {atoms as a} from '#/alf'
|
||||
|
||||
const animationConfig = {
|
||||
duration: 400,
|
||||
@@ -87,89 +84,66 @@ function ExitingDown() {
|
||||
}
|
||||
|
||||
export function CountWheel({
|
||||
likeCount,
|
||||
big,
|
||||
isLiked,
|
||||
count,
|
||||
isToggled,
|
||||
hasBeenToggled,
|
||||
renderCount,
|
||||
}: {
|
||||
likeCount: number
|
||||
big?: boolean
|
||||
isLiked: boolean
|
||||
count: number
|
||||
isToggled: boolean
|
||||
hasBeenToggled: boolean
|
||||
renderCount: (props: {count: number}) => React.ReactNode
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const shouldAnimate = !useReducedMotion() && hasBeenToggled
|
||||
const shouldRoll = decideShouldRoll(isLiked, likeCount)
|
||||
const shouldRoll = decideShouldRoll(isToggled, count)
|
||||
|
||||
// Incrementing the key will cause the `Animated.View` to re-render, with the newly selected entering/exiting
|
||||
// animation
|
||||
// The initial entering/exiting animations will get skipped, since these will happen on screen mounts and would
|
||||
// be unnecessary
|
||||
const [key, setKey] = useState(0)
|
||||
const [prevCount, setPrevCount] = useState(likeCount)
|
||||
const prevIsLiked = useRef(isLiked)
|
||||
const formatPostStatCount = useFormatPostStatCount()
|
||||
const formattedCount = formatPostStatCount(likeCount)
|
||||
const formattedPrevCount = formatPostStatCount(prevCount)
|
||||
const [prevCount, setPrevCount] = useState(count)
|
||||
const prevIsToggled = useRef(isToggled)
|
||||
|
||||
useEffect(() => {
|
||||
if (isLiked === prevIsLiked.current) {
|
||||
if (isToggled === prevIsToggled.current) {
|
||||
return
|
||||
}
|
||||
|
||||
const newPrevCount = isLiked ? likeCount - 1 : likeCount + 1
|
||||
const newPrevCount = isToggled ? count - 1 : count + 1
|
||||
setKey(prev => prev + 1)
|
||||
setPrevCount(newPrevCount)
|
||||
prevIsLiked.current = isLiked
|
||||
}, [isLiked, likeCount])
|
||||
prevIsToggled.current = isToggled
|
||||
}, [isToggled, count])
|
||||
|
||||
const enteringAnimation =
|
||||
shouldAnimate && shouldRoll
|
||||
? isLiked
|
||||
? isToggled
|
||||
? EnteringUp
|
||||
: EnteringDown
|
||||
: undefined
|
||||
const exitingAnimation =
|
||||
shouldAnimate && shouldRoll
|
||||
? isLiked
|
||||
? isToggled
|
||||
? ExitingUp
|
||||
: ExitingDown
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<LayoutAnimationConfig skipEntering skipExiting>
|
||||
{likeCount > 0 ? (
|
||||
{count > 0 ? (
|
||||
<View style={[a.justify_center]}>
|
||||
<Animated.View entering={enteringAnimation} key={key}>
|
||||
<Text
|
||||
testID="likeCount"
|
||||
style={[
|
||||
big ? a.text_md : a.text_sm,
|
||||
a.user_select_none,
|
||||
isLiked
|
||||
? [a.font_semi_bold, s.likeColor]
|
||||
: {color: t.palette.contrast_500},
|
||||
]}>
|
||||
{formattedCount}
|
||||
</Text>
|
||||
{renderCount({count})}
|
||||
</Animated.View>
|
||||
{shouldAnimate && (likeCount > 1 || !isLiked) ? (
|
||||
{shouldAnimate && (count > 1 || !isToggled) ? (
|
||||
<Animated.View
|
||||
entering={exitingAnimation}
|
||||
// Add 2 to the key so there are never duplicates
|
||||
key={key + 2}
|
||||
style={[a.absolute, {width: 50, opacity: 0}]}
|
||||
aria-disabled={true}>
|
||||
<Text
|
||||
style={[
|
||||
big ? a.text_md : a.text_sm,
|
||||
a.user_select_none,
|
||||
isLiked
|
||||
? [a.font_semi_bold, s.likeColor]
|
||||
: {color: t.palette.contrast_500},
|
||||
]}>
|
||||
{formattedPrevCount}
|
||||
</Text>
|
||||
{renderCount({count: prevCount})}
|
||||
</Animated.View>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
@@ -3,10 +3,6 @@ import {View} from 'react-native'
|
||||
import {useReducedMotion} from 'react-native-reanimated'
|
||||
|
||||
import {decideShouldRoll} from '#/lib/custom-animations/util'
|
||||
import {s} from '#/lib/styles'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {useFormatPostStatCount} from '#/components/PostControls/util'
|
||||
|
||||
const animationConfig = {
|
||||
duration: 400,
|
||||
@@ -35,50 +31,46 @@ const exitingDownKeyframe = [
|
||||
]
|
||||
|
||||
export function CountWheel({
|
||||
likeCount,
|
||||
big,
|
||||
isLiked,
|
||||
count,
|
||||
isToggled,
|
||||
hasBeenToggled,
|
||||
renderCount,
|
||||
}: {
|
||||
likeCount: number
|
||||
big?: boolean
|
||||
isLiked: boolean
|
||||
count: number
|
||||
isToggled: boolean
|
||||
hasBeenToggled: boolean
|
||||
renderCount: (props: {count: number}) => React.ReactNode
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const shouldAnimate = !useReducedMotion() && hasBeenToggled
|
||||
const shouldRoll = decideShouldRoll(isLiked, likeCount)
|
||||
const shouldRoll = decideShouldRoll(isToggled, count)
|
||||
|
||||
const countView = useRef<HTMLDivElement>(null)
|
||||
const prevCountView = useRef<HTMLDivElement>(null)
|
||||
|
||||
const [prevCount, setPrevCount] = useState(likeCount)
|
||||
const prevIsLiked = useRef(isLiked)
|
||||
const formatPostStatCount = useFormatPostStatCount()
|
||||
const formattedCount = formatPostStatCount(likeCount)
|
||||
const formattedPrevCount = formatPostStatCount(prevCount)
|
||||
const [prevCount, setPrevCount] = useState(count)
|
||||
const prevIsToggled = useRef(isToggled)
|
||||
|
||||
useEffect(() => {
|
||||
if (isLiked === prevIsLiked.current) {
|
||||
if (isToggled === prevIsToggled.current) {
|
||||
return
|
||||
}
|
||||
|
||||
const newPrevCount = isLiked ? likeCount - 1 : likeCount + 1
|
||||
const newPrevCount = isToggled ? count - 1 : count + 1
|
||||
if (shouldAnimate && shouldRoll) {
|
||||
countView.current?.animate?.(
|
||||
isLiked ? enteringUpKeyframe : enteringDownKeyframe,
|
||||
isToggled ? enteringUpKeyframe : enteringDownKeyframe,
|
||||
animationConfig,
|
||||
)
|
||||
prevCountView.current?.animate?.(
|
||||
isLiked ? exitingUpKeyframe : exitingDownKeyframe,
|
||||
isToggled ? exitingUpKeyframe : exitingDownKeyframe,
|
||||
animationConfig,
|
||||
)
|
||||
setPrevCount(newPrevCount)
|
||||
}
|
||||
prevIsLiked.current = isLiked
|
||||
}, [isLiked, likeCount, shouldAnimate, shouldRoll])
|
||||
prevIsToggled.current = isToggled
|
||||
}, [isToggled, count, shouldAnimate, shouldRoll])
|
||||
|
||||
if (likeCount < 1) {
|
||||
if (count < 1) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -87,34 +79,15 @@ export function CountWheel({
|
||||
<View
|
||||
// @ts-expect-error is div
|
||||
ref={countView}>
|
||||
<Text
|
||||
testID="likeCount"
|
||||
style={[
|
||||
big ? a.text_md : a.text_sm,
|
||||
a.user_select_none,
|
||||
isLiked
|
||||
? [a.font_semi_bold, s.likeColor]
|
||||
: {color: t.palette.contrast_500},
|
||||
]}>
|
||||
{formattedCount}
|
||||
</Text>
|
||||
{renderCount({count})}
|
||||
</View>
|
||||
{shouldAnimate && (likeCount > 1 || !isLiked) ? (
|
||||
{shouldAnimate && (count > 1 || !isToggled) ? (
|
||||
<View
|
||||
style={{position: 'absolute', opacity: 0}}
|
||||
aria-disabled={true}
|
||||
// @ts-expect-error is div
|
||||
ref={prevCountView}>
|
||||
<Text
|
||||
style={[
|
||||
big ? a.text_md : a.text_sm,
|
||||
a.user_select_none,
|
||||
isLiked
|
||||
? [a.font_semi_bold, s.likeColor]
|
||||
: {color: t.palette.contrast_500},
|
||||
]}>
|
||||
{formattedPrevCount}
|
||||
</Text>
|
||||
{renderCount({count: prevCount})}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
@@ -5,7 +5,6 @@ import Animated, {
|
||||
useReducedMotion,
|
||||
} from 'react-native-reanimated'
|
||||
|
||||
import {s} from '#/lib/styles'
|
||||
import {useTheme} from '#/alf'
|
||||
import {
|
||||
Heart2_Filled_Stroke2_Corner0_Rounded as HeartIconFilled,
|
||||
@@ -86,7 +85,7 @@ export function AnimatedLikeIcon({
|
||||
{isLiked ? (
|
||||
<Animated.View
|
||||
entering={shouldAnimate ? keyframe.duration(300) : undefined}>
|
||||
<HeartIconFilled style={s.likeColor} width={size} />
|
||||
<HeartIconFilled style={{color: t.palette.pink}} width={size} />
|
||||
</Animated.View>
|
||||
) : (
|
||||
<HeartIconOutline
|
||||
@@ -100,7 +99,7 @@ export function AnimatedLikeIcon({
|
||||
entering={circle1Keyframe.duration(300)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
backgroundColor: s.likeColor.color,
|
||||
backgroundColor: t.palette.pink,
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: size,
|
||||
|
||||
@@ -2,7 +2,6 @@ import {useEffect, useRef} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {useReducedMotion} from 'react-native-reanimated'
|
||||
|
||||
import {s} from '#/lib/styles'
|
||||
import {useTheme} from '#/alf'
|
||||
import {
|
||||
Heart2_Filled_Stroke2_Corner0_Rounded as HeartIconFilled,
|
||||
@@ -74,7 +73,7 @@ export function AnimatedLikeIcon({
|
||||
{isLiked ? (
|
||||
// @ts-expect-error is div
|
||||
<View ref={likeIconRef}>
|
||||
<HeartIconFilled style={s.likeColor} width={size} />
|
||||
<HeartIconFilled style={{color: t.palette.pink}} width={size} />
|
||||
</View>
|
||||
) : (
|
||||
<HeartIconOutline
|
||||
@@ -87,7 +86,7 @@ export function AnimatedLikeIcon({
|
||||
ref={circle1Ref}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
backgroundColor: s.likeColor.color,
|
||||
backgroundColor: t.palette.pink,
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: size,
|
||||
|
||||
Reference in New Issue
Block a user