Improve animations for like button (#5074)

This commit is contained in:
Hailey 2024-09-02 01:37:24 -07:00 committed by GitHub
parent eb868a042a
commit 1225e84485
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 580 additions and 247 deletions

View file

@ -6,14 +6,6 @@ import {
View,
type ViewStyle,
} from 'react-native'
import Animated, {
Easing,
interpolate,
SharedValue,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated'
import * as Clipboard from 'expo-clipboard'
import {
AppBskyFeedDefs,
@ -31,8 +23,6 @@ import {makeProfileLink} from '#/lib/routes/links'
import {shareUrl} from '#/lib/sharing'
import {useGate} from '#/lib/statsig/statsig'
import {toShareUrl} from '#/lib/strings/url-helpers'
import {s} from '#/lib/styles'
import {isWeb} from '#/platform/detection'
import {Shadow} from '#/state/cache/types'
import {useFeedFeedbackContext} from '#/state/feed-feedback'
import {
@ -45,16 +35,13 @@ import {
ProgressGuideAction,
useProgressGuideControls,
} from '#/state/shell/progress-guide'
import {CountWheel} from 'lib/custom-animations/CountWheel'
import {AnimatedLikeIcon} from 'lib/custom-animations/LikeIcon'
import {atoms as a, useTheme} from '#/alf'
import {useDialogControl} from '#/components/Dialog'
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as ArrowOutOfBox} from '#/components/icons/ArrowOutOfBox'
import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '#/components/icons/Bubble'
import {
Heart2_Filled_Stroke2_Corner0_Rounded as HeartIconFilled,
Heart2_Stroke2_Corner0_Rounded as HeartIconOutline,
} from '#/components/icons/Heart2'
import * as Prompt from '#/components/Prompt'
import {PlatformInfo} from '../../../../../modules/expo-bluesky-swiss-army'
import {PostDropdownBtn} from '../forms/PostDropdownBtn'
import {formatCount} from '../numeric/format'
import {Text} from '../text/Text'
@ -120,17 +107,7 @@ let PostCtrls = ({
) as StyleProp<ViewStyle>
const likeValue = post.viewer?.like ? 1 : 0
const likeIconAnimValue = useSharedValue(likeValue)
const likeTextAnimValue = useSharedValue(likeValue)
const nextExpectedLikeValue = React.useRef(likeValue)
React.useEffect(() => {
// Catch nonlocal changes (e.g. shadow update) and always reflect them.
if (likeValue !== nextExpectedLikeValue.current) {
nextExpectedLikeValue.current = likeValue
likeIconAnimValue.value = likeValue
likeTextAnimValue.value = likeValue
}
}, [likeValue, likeIconAnimValue, likeTextAnimValue])
const onPressToggleLike = React.useCallback(async () => {
if (isBlocked) {
@ -144,19 +121,6 @@ let PostCtrls = ({
try {
if (!post.viewer?.like) {
nextExpectedLikeValue.current = 1
if (PlatformInfo.getIsReducedMotionEnabled()) {
likeIconAnimValue.value = 1
likeTextAnimValue.value = 1
} else {
likeIconAnimValue.value = withTiming(1, {
duration: 400,
easing: Easing.out(Easing.cubic),
})
likeTextAnimValue.value = withTiming(1, {
duration: 400,
easing: Easing.out(Easing.cubic),
})
}
playHaptic()
sendInteraction({
item: post.uri,
@ -167,15 +131,6 @@ let PostCtrls = ({
await queueLike()
} else {
nextExpectedLikeValue.current = 0
likeIconAnimValue.value = 0 // Intentionally not animated
if (PlatformInfo.getIsReducedMotionEnabled()) {
likeTextAnimValue.value = 0
} else {
likeTextAnimValue.value = withTiming(0, {
duration: 400,
easing: Easing.out(Easing.cubic),
})
}
await queueUnlike()
}
} catch (e: any) {
@ -185,8 +140,6 @@ let PostCtrls = ({
}
}, [
_,
likeIconAnimValue,
likeTextAnimValue,
playHaptic,
post.uri,
post.viewer?.like,
@ -291,8 +244,8 @@ let PostCtrls = ({
a.gap_xs,
a.rounded_full,
a.flex_row,
a.align_center,
a.justify_center,
a.align_center,
{padding: 5},
(pressed || hovered) && t.atoms.bg_contrast_25,
],
@ -364,13 +317,11 @@ let PostCtrls = ({
}
accessibilityHint=""
hitSlop={POST_CTRL_HITSLOP}>
<AnimatedLikeIcon
big={big ?? false}
likeIconAnimValue={likeIconAnimValue}
likeTextAnimValue={likeTextAnimValue}
defaultCtrlColor={defaultCtrlColor}
isLiked={Boolean(post.viewer?.like)}
<AnimatedLikeIcon isLiked={Boolean(post.viewer?.like)} big={big} />
<CountWheel
likeCount={post.likeCount ?? 0}
big={big}
isLiked={Boolean(post.viewer?.like)}
/>
</Pressable>
</View>
@ -450,194 +401,3 @@ let PostCtrls = ({
}
PostCtrls = memo(PostCtrls)
export {PostCtrls}
function AnimatedLikeIcon({
big,
likeIconAnimValue,
likeTextAnimValue,
defaultCtrlColor,
isLiked,
likeCount,
}: {
big: boolean
likeIconAnimValue: SharedValue<number>
likeTextAnimValue: SharedValue<number>
defaultCtrlColor: StyleProp<ViewStyle>
isLiked: boolean
likeCount: number
}) {
const t = useTheme()
const {i18n} = useLingui()
const likeStyle = useAnimatedStyle(() => ({
transform: [
{
scale: interpolate(
likeIconAnimValue.value,
[0, 0.1, 0.4, 1],
[1, 0.7, 1.2, 1],
'clamp',
),
},
],
}))
const circle1Style = useAnimatedStyle(() => ({
opacity: interpolate(
likeIconAnimValue.value,
[0, 0.1, 0.95, 1],
[0, 0.4, 0.4, 0],
'clamp',
),
transform: [
{
scale: interpolate(
likeIconAnimValue.value,
[0, 0.4, 1],
[0, 1.5, 1.5],
'clamp',
),
},
],
}))
const circle2Style = useAnimatedStyle(() => ({
opacity: interpolate(
likeIconAnimValue.value,
[0, 0.1, 0.95, 1],
[0, 1, 1, 0],
'clamp',
),
transform: [
{
scale: interpolate(
likeIconAnimValue.value,
[0, 0.4, 1],
[0, 0, 1.5],
'clamp',
),
},
],
}))
const countStyle = useAnimatedStyle(() => ({
transform: [
{
translateY: interpolate(
likeTextAnimValue.value,
[0, 1],
[0, big ? -22 : -18],
'clamp',
),
},
],
}))
const prevFormattedCount = formatCount(
i18n,
isLiked ? likeCount - 1 : likeCount,
)
const nextFormattedCount = formatCount(
i18n,
isLiked ? likeCount : likeCount + 1,
)
const shouldRollLike =
prevFormattedCount !== nextFormattedCount && prevFormattedCount !== '0'
return (
<>
<View>
<Animated.View
style={[
{
position: 'absolute',
backgroundColor: s.likeColor.color,
top: 0,
left: 0,
width: big ? 22 : 18,
height: big ? 22 : 18,
zIndex: -1,
pointerEvents: 'none',
borderRadius: (big ? 22 : 18) / 2,
},
circle1Style,
]}
/>
<Animated.View
style={[
{
position: 'absolute',
backgroundColor: isWeb
? t.atoms.bg_contrast_25.backgroundColor
: t.atoms.bg.backgroundColor,
top: 0,
left: 0,
width: big ? 22 : 18,
height: big ? 22 : 18,
zIndex: -1,
pointerEvents: 'none',
borderRadius: (big ? 22 : 18) / 2,
},
circle2Style,
]}
/>
<Animated.View style={likeStyle}>
{isLiked ? (
<HeartIconFilled style={s.likeColor} width={big ? 22 : 18} />
) : (
<HeartIconOutline
style={[defaultCtrlColor, {pointerEvents: 'none'}]}
width={big ? 22 : 18}
/>
)}
</Animated.View>
</View>
<View style={{overflow: 'hidden'}}>
<Text
testID="likeCount"
style={[
[
big ? a.text_md : {fontSize: 15},
a.user_select_none,
isLiked ? [a.font_bold, s.likeColor] : defaultCtrlColor,
{opacity: shouldRollLike ? 0 : 1},
],
]}>
{likeCount > 0 ? formatCount(i18n, likeCount) : ''}
</Text>
<Animated.View
aria-hidden={true}
style={[
countStyle,
{
position: 'absolute',
top: 0,
left: 0,
opacity: shouldRollLike ? 1 : 0,
},
]}>
<Text
testID="likeCount"
style={[
[
big ? a.text_md : {fontSize: 15},
a.user_select_none,
isLiked ? [a.font_bold, s.likeColor] : defaultCtrlColor,
{height: big ? 22 : 18},
],
]}>
{prevFormattedCount}
</Text>
<Text
testID="likeCount"
style={[
[
big ? a.text_md : {fontSize: 15},
a.user_select_none,
isLiked ? [a.font_bold, s.likeColor] : defaultCtrlColor,
{height: big ? 22 : 18},
],
]}>
{nextFormattedCount}
</Text>
</Animated.View>
</View>
</>
)
}