Reduce render cost of post controls and improve perceived responsiveness (#132)

* Move post control animations into conditional render and increase perceived responsiveness

* Remove log
This commit is contained in:
Paul Frazee
2023-02-01 13:09:20 -06:00
committed by GitHub
parent ad511bc574
commit 9889035a20
6 changed files with 165 additions and 79 deletions
+2 -2
View File
@@ -70,12 +70,12 @@ export const PostThreadItem = observer(function PostThreadItem({
})
}
const onPressToggleRepost = () => {
item
return item
.toggleRepost()
.catch(e => store.log.error('Failed to toggle repost', e))
}
const onPressToggleUpvote = () => {
item
return item
.toggleUpvote()
.catch(e => store.log.error('Failed to toggle upvote', e))
}
+2 -2
View File
@@ -105,12 +105,12 @@ export const Post = observer(function Post({
})
}
const onPressToggleRepost = () => {
item
return item
.toggleRepost()
.catch(e => store.log.error('Failed to toggle repost', e))
}
const onPressToggleUpvote = () => {
item
return item
.toggleUpvote()
.catch(e => store.log.error('Failed to toggle upvote', e))
}
+2 -2
View File
@@ -61,12 +61,12 @@ export const FeedItem = observer(function ({
})
}
const onPressToggleRepost = () => {
item
return item
.toggleRepost()
.catch(e => store.log.error('Failed to toggle repost', e))
}
const onPressToggleUpvote = () => {
item
return item
.toggleUpvote()
.catch(e => store.log.error('Failed to toggle upvote', e))
}
+84 -73
View File
@@ -9,6 +9,10 @@ import {
} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import ReactNativeHapticFeedback from 'react-native-haptic-feedback'
import {
TriggerableAnimated,
TriggerableAnimatedRef,
} from './anim/TriggerableAnimated'
import {Text} from './text/Text'
import {PostDropdownBtn} from './forms/DropdownButton'
import {
@@ -19,7 +23,6 @@ import {
} from '../../lib/icons'
import {s, colors} from '../../lib/styles'
import {useTheme} from '../../lib/ThemeContext'
import {useAnimatedValue} from '../../lib/hooks/useAnimatedValue'
interface PostCtrlsOpts {
itemHref: string
@@ -33,14 +36,47 @@ interface PostCtrlsOpts {
isReposted: boolean
isUpvoted: boolean
onPressReply: () => void
onPressToggleRepost: () => void
onPressToggleUpvote: () => void
onPressToggleRepost: () => Promise<void>
onPressToggleUpvote: () => Promise<void>
onCopyPostText: () => void
onDeletePost: () => void
}
const HITSLOP = {top: 5, left: 5, bottom: 5, right: 5}
function ctrlAnimStart(interp: Animated.Value) {
return Animated.sequence([
Animated.timing(interp, {
toValue: 1,
duration: 250,
useNativeDriver: true,
}),
Animated.delay(50),
Animated.timing(interp, {
toValue: 0,
duration: 20,
useNativeDriver: true,
}),
])
}
function ctrlAnimStyle(interp: Animated.Value) {
return {
transform: [
{
scale: interp.interpolate({
inputRange: [0, 1.0],
outputRange: [1.0, 4.0],
}),
},
],
opacity: interp.interpolate({
inputRange: [0, 1.0],
outputRange: [1.0, 0.0],
}),
}
}
export function PostCtrls(opts: PostCtrlsOpts) {
const theme = useTheme()
const defaultCtrlColor = React.useMemo(
@@ -49,75 +85,47 @@ export function PostCtrls(opts: PostCtrlsOpts) {
}),
[theme],
)
const interp1 = useAnimatedValue(0)
const interp2 = useAnimatedValue(0)
const anim1Style = {
transform: [
{
scale: interp1.interpolate({
inputRange: [0, 1.0],
outputRange: [1.0, 4.0],
}),
},
],
opacity: interp1.interpolate({
inputRange: [0, 1.0],
outputRange: [1.0, 0.0],
}),
}
const anim2Style = {
transform: [
{
scale: interp2.interpolate({
inputRange: [0, 1.0],
outputRange: [1.0, 4.0],
}),
},
],
opacity: interp2.interpolate({
inputRange: [0, 1.0],
outputRange: [1.0, 0.0],
}),
}
const [repostMod, setRepostMod] = React.useState<number>(0)
const [likeMod, setLikeMod] = React.useState<number>(0)
const repostRef = React.useRef<TriggerableAnimatedRef | null>(null)
const likeRef = React.useRef<TriggerableAnimatedRef | null>(null)
const onPressToggleRepostWrapper = () => {
if (!opts.isReposted) {
ReactNativeHapticFeedback.trigger('impactMedium')
Animated.sequence([
Animated.timing(interp1, {
toValue: 1,
duration: 400,
useNativeDriver: true,
}),
Animated.delay(100),
Animated.timing(interp1, {
toValue: 0,
duration: 20,
useNativeDriver: true,
}),
]).start()
setRepostMod(1)
repostRef.current?.trigger(
{start: ctrlAnimStart, style: ctrlAnimStyle},
async () => {
await opts.onPressToggleRepost().catch(_e => undefined)
setRepostMod(0)
},
)
} else {
setRepostMod(-1)
opts
.onPressToggleRepost()
.catch(_e => undefined)
.then(() => setRepostMod(0))
}
opts.onPressToggleRepost()
}
const onPressToggleUpvoteWrapper = () => {
if (!opts.isUpvoted) {
ReactNativeHapticFeedback.trigger('impactMedium')
Animated.sequence([
Animated.timing(interp2, {
toValue: 1,
duration: 400,
useNativeDriver: true,
}),
Animated.delay(100),
Animated.timing(interp2, {
toValue: 0,
duration: 20,
useNativeDriver: true,
}),
]).start()
setLikeMod(1)
likeRef.current?.trigger(
{start: ctrlAnimStart, style: ctrlAnimStyle},
async () => {
await opts.onPressToggleUpvote().catch(_e => undefined)
setLikeMod(0)
},
)
} else {
setLikeMod(-1)
opts
.onPressToggleUpvote()
.catch(_e => undefined)
.then(() => setLikeMod(0))
}
opts.onPressToggleUpvote()
}
return (
@@ -144,23 +152,26 @@ export function PostCtrls(opts: PostCtrlsOpts) {
hitSlop={HITSLOP}
onPress={onPressToggleRepostWrapper}
style={styles.ctrl}>
<Animated.View style={anim1Style}>
<TriggerableAnimated ref={repostRef}>
<RepostIcon
style={
opts.isReposted ? styles.ctrlIconReposted : defaultCtrlColor
opts.isReposted || repostMod > 0
? styles.ctrlIconReposted
: defaultCtrlColor
}
strokeWidth={2.4}
size={opts.big ? 24 : 20}
/>
</Animated.View>
</TriggerableAnimated>
{typeof opts.repostCount !== 'undefined' ? (
<Text
style={
opts.isReposted
opts.isReposted || repostMod > 0
? [s.bold, s.green3, s.f15, s.ml5]
: [defaultCtrlColor, s.f15, s.ml5]
}>
{opts.repostCount}
{opts.repostCount + repostMod}
</Text>
) : undefined}
</TouchableOpacity>
@@ -170,8 +181,8 @@ export function PostCtrls(opts: PostCtrlsOpts) {
style={styles.ctrl}
hitSlop={HITSLOP}
onPress={onPressToggleUpvoteWrapper}>
<Animated.View style={anim2Style}>
{opts.isUpvoted ? (
<TriggerableAnimated ref={likeRef}>
{opts.isUpvoted || likeMod > 0 ? (
<HeartIconSolid
style={[styles.ctrlIconUpvoted]}
size={opts.big ? 22 : 16}
@@ -183,15 +194,15 @@ export function PostCtrls(opts: PostCtrlsOpts) {
size={opts.big ? 20 : 16}
/>
)}
</Animated.View>
</TriggerableAnimated>
{typeof opts.upvoteCount !== 'undefined' ? (
<Text
style={
opts.isUpvoted
opts.isUpvoted || likeMod > 0
? [s.bold, s.red3, s.f15, s.ml5]
: [defaultCtrlColor, s.f15, s.ml5]
}>
{opts.upvoteCount}
{opts.upvoteCount + likeMod}
</Text>
) : undefined}
</TouchableOpacity>
@@ -0,0 +1,73 @@
import React from 'react'
import {Animated, StyleProp, View, ViewStyle} from 'react-native'
import {useAnimatedValue} from '../../../lib/hooks/useAnimatedValue'
type CreateAnimFn = (interp: Animated.Value) => Animated.CompositeAnimation
type FinishCb = () => void
interface TriggeredAnimation {
start: CreateAnimFn
style: (
interp: Animated.Value,
) => Animated.WithAnimatedValue<StyleProp<ViewStyle>>
}
export interface TriggerableAnimatedRef {
trigger: (anim: TriggeredAnimation, onFinish?: FinishCb) => void
}
type TriggerableAnimatedProps = React.PropsWithChildren<{}>
type PropsInner = TriggerableAnimatedProps & {
anim: TriggeredAnimation
onFinish: () => void
}
export const TriggerableAnimated = React.forwardRef<
TriggerableAnimatedRef,
TriggerableAnimatedProps
>(({children, ...props}, ref) => {
const [anim, setAnim] = React.useState<TriggeredAnimation | undefined>(
undefined,
)
const [finishCb, setFinishCb] = React.useState<FinishCb | undefined>(
undefined,
)
React.useImperativeHandle(ref, () => ({
trigger(v: TriggeredAnimation, cb?: FinishCb) {
setFinishCb(() => cb) // note- wrap in function due to react behaviors around setstate
setAnim(v)
},
}))
const onFinish = () => {
finishCb?.()
setAnim(undefined)
setFinishCb(undefined)
}
return (
<View key="triggerable">
{anim ? (
<AnimatingView anim={anim} onFinish={onFinish} {...props}>
{children}
</AnimatingView>
) : (
children
)}
</View>
)
})
function AnimatingView({
anim,
onFinish,
children,
}: React.PropsWithChildren<PropsInner>) {
const interp = useAnimatedValue(0)
React.useEffect(() => {
anim?.start(interp).start(() => {
onFinish()
})
})
const animStyle = anim?.style(interp)
return <Animated.View style={animStyle}>{children}</Animated.View>
}
+2
View File
@@ -175,12 +175,14 @@ export const MobileShell: React.FC = observer(() => {
toValue: 1,
duration: 100,
useNativeDriver: true,
isInteraction: false,
}).start()
} else {
Animated.timing(minimalShellInterp, {
toValue: 0,
duration: 100,
useNativeDriver: true,
isInteraction: false,
}).start()
}
}, [minimalShellInterp, store.shell.minimalShellMode])