scroll animation on logo

This commit is contained in:
Samuel Newman
2024-09-26 00:48:45 +01:00
parent ff9be546e7
commit ed1ad25cea
6 changed files with 247 additions and 103 deletions
+8 -6
View File
@@ -1,13 +1,17 @@
import React from 'react'
import {View} from 'react-native'
import {NativeScrollEvent, View} from 'react-native'
import {AppBskyActorDefs} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {NavigationProp, useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {useAnalytics} from '#/lib/analytics/analytics'
import {ComposeIcon2} from '#/lib/icons'
import {getRootNavigation, getTabState, TabState} from '#/lib/routes/helpers'
import {AllNavigatorParams} from '#/lib/routes/types'
import {logEvent} from '#/lib/statsig/statsig'
import {s} from '#/lib/styles'
import {isNative} from '#/platform/detection'
import {listenSoftReset} from '#/state/events'
import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
@@ -17,10 +21,6 @@ import {truncateAndInvalidate} from '#/state/queries/util'
import {useSession} from '#/state/session'
import {useSetMinimalShellMode} from '#/state/shell'
import {useComposerControls} from '#/state/shell/composer'
import {useAnalytics} from 'lib/analytics/analytics'
import {ComposeIcon2} from 'lib/icons'
import {AllNavigatorParams} from 'lib/routes/types'
import {s} from 'lib/styles'
import {useHeaderOffset} from '#/components/hooks/useHeaderOffset'
import {Feed} from '../posts/Feed'
import {FAB} from '../util/fab/FAB'
@@ -38,6 +38,7 @@ export function FeedPage({
renderEmptyState,
renderEndOfFeed,
savedFeedConfig,
onScroll,
}: {
testID?: string
feed: FeedDescriptor
@@ -46,6 +47,7 @@ export function FeedPage({
renderEmptyState: () => JSX.Element
renderEndOfFeed?: () => JSX.Element
savedFeedConfig?: AppBskyActorDefs.SavedFeed
onScroll: (e: NativeScrollEvent) => void
}) {
const {hasSession} = useSession()
const {_} = useLingui()
@@ -111,7 +113,7 @@ export function FeedPage({
return (
<View testID={testID} style={s.h100pct}>
<MainScrollProvider>
<MainScrollProvider onScroll={onScroll}>
<FeedFeedbackProvider value={feedFeedback}>
<Feed
testID={testID ? `${testID}-feed` : undefined}
+6 -4
View File
@@ -1,11 +1,12 @@
import React from 'react'
import {SharedValue} from 'react-native-reanimated'
import {useNavigation} from '@react-navigation/native'
import {usePalette} from '#/lib/hooks/usePalette'
import {NavigationProp} from '#/lib/routes/types'
import {FeedSourceInfo} from '#/state/queries/feed'
import {useSession} from '#/state/session'
import {NavigationProp} from 'lib/routes/types'
import {RenderTabBarFnProps} from 'view/com/pager/Pager'
import {RenderTabBarFnProps} from '#/view/com/pager/Pager'
import {TabBar} from '../pager/TabBar'
import {HomeHeaderLayout} from './HomeHeaderLayout'
@@ -14,9 +15,10 @@ export function HomeHeader(
testID?: string
onPressSelected: () => void
feeds: FeedSourceInfo[]
scrollY: SharedValue<number>
},
) {
const {feeds} = props
const {feeds, scrollY} = props
const {hasSession} = useSession()
const navigation = useNavigation<NavigationProp>()
const pal = usePalette('default')
@@ -53,7 +55,7 @@ export function HomeHeader(
)
return (
<HomeHeaderLayout tabBarAnchor={props.tabBarAnchor}>
<HomeHeaderLayout tabBarAnchor={props.tabBarAnchor} scrollY={scrollY}>
<TabBar
key={items.join(',')}
onPressSelected={props.onPressSelected}
+2 -2
View File
@@ -13,10 +13,10 @@ import Animated, {
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useMinimalShellHeaderTransform} from '#/lib/hooks/useMinimalShellTransform'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {useSession} from '#/state/session'
import {useShellLayout} from '#/state/shell/shell-layout'
import {useMinimalShellHeaderTransform} from 'lib/hooks/useMinimalShellTransform'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
// import {Logo} from '#/view/icons/Logo'
import {atoms as a, useTheme} from '#/alf'
import {Icon, Trigger} from '#/components/dialogs/nuxs/TenMillion/Trigger'
+186 -78
View File
@@ -1,6 +1,15 @@
import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import React, {useState} from 'react'
import {
ActivityIndicator,
StyleSheet,
TouchableOpacity,
View,
} from 'react-native'
import Animated, {
Extrapolation,
interpolate,
LayoutAnimationConfig,
SharedValue,
useAnimatedStyle,
useReducedMotion,
useSharedValue,
@@ -9,22 +18,23 @@ import Animated, {
withSequence,
withSpring,
withTiming,
ZoomIn,
ZoomOut,
} from 'react-native-reanimated'
import {useSafeAreaFrame} from 'react-native-safe-area-context'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {HITSLOP_10} from '#/lib/constants'
import {useMinimalShellHeaderTransform} from '#/lib/hooks/useMinimalShellTransform'
import {isWeb} from '#/platform/detection'
import {useSession} from '#/state/session'
import {useSetDrawerOpen} from '#/state/shell/drawer-open'
import {useShellLayout} from '#/state/shell/shell-layout'
import {HITSLOP_10} from 'lib/constants'
import {useMinimalShellHeaderTransform} from 'lib/hooks/useMinimalShellTransform'
import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection'
// import {Logo} from '#/view/icons/Logo'
import {atoms} from '#/alf'
import {useTheme} from '#/alf'
import {atoms as a} from '#/alf'
import {atoms as a, native, useTheme} from '#/alf'
import {Icon, Trigger} from '#/components/dialogs/nuxs/TenMillion/Trigger'
import {useOverscrolled} from '#/components/hooks/useOverscrolled'
import {ColorPalette_Stroke2_Corner0_Rounded as ColorPalette} from '#/components/icons/ColorPalette'
import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag'
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
@@ -33,17 +43,20 @@ import {IS_DEV} from '#/env'
export function HomeHeaderLayoutMobile({
children,
scrollY,
}: {
children: React.ReactNode
tabBarAnchor: JSX.Element | null | undefined
scrollY?: SharedValue<number>
}) {
const t = useTheme()
const pal = usePalette('default')
const {_} = useLingui()
const setDrawerOpen = useSetDrawerOpen()
const {headerHeight} = useShellLayout()
const {height: frameHeight} = useSafeAreaFrame()
const headerMinimalShellTransform = useMinimalShellHeaderTransform()
const {hasSession} = useSession()
const showSpinner = useShowSpinner({scrollY})
const onPressAvi = React.useCallback(() => {
setDrawerOpen(true)
@@ -78,7 +91,34 @@ export function HomeHeaderLayoutMobile({
)
}, [rotate, reducedMotion])
const animatedStyle = useAnimatedStyle(() => ({
const animatedContainerStyle = useAnimatedStyle(() => ({
transform: [
{
translateY: isWeb
? 0
: interpolate(
scrollY?.value ?? -headerHeight.value,
[-headerHeight.value, 0],
[headerHeight.value, 0],
{extrapolateRight: Extrapolation.CLAMP},
),
},
],
}))
const animatedLogoContainerStyle = useAnimatedStyle(() => ({
transform: [
{
translateY: isWeb
? 0
: interpolate(scrollY?.value ?? 0, [-2, 0], [-1, 0], {
extrapolateRight: Extrapolation.CLAMP,
}),
},
],
}))
const animatedJiggleStyle = useAnimatedStyle(() => ({
transform: [
{
rotateZ: `${rotate.value}deg`,
@@ -86,78 +126,125 @@ export function HomeHeaderLayoutMobile({
],
}))
console.log(headerHeight.value)
return (
<Animated.View
style={[pal.view, pal.border, styles.tabBar, headerMinimalShellTransform]}
onLayout={e => {
headerHeight.value = e.nativeEvent.layout.height
}}>
<View style={[pal.view, styles.topBar]}>
<View style={[pal.view, {width: 100}]}>
<TouchableOpacity
testID="viewHeaderDrawerBtn"
onPress={onPressAvi}
accessibilityRole="button"
accessibilityLabel={_(msg`Open navigation`)}
accessibilityHint={_(
msg`Access profile and other navigation links`,
)}
hitSlop={HITSLOP_10}>
<Menu size="lg" fill={t.atoms.text_contrast_medium.color} />
</TouchableOpacity>
</View>
<Animated.View style={animatedStyle}>
<Trigger>
{ctx => (
<Icon
width={28}
style={{
opacity: ctx.pressed ? 0.8 : 1,
}}
/>
)}
</Trigger>
{/* <Logo width={30} /> */}
</Animated.View>
pointerEvents="box-none"
style={[styles.tabBar, animatedContainerStyle]}>
{native(
<View
style={[
atoms.flex_row,
atoms.justify_end,
atoms.align_center,
atoms.gap_md,
pal.view,
{width: 100},
]}>
{IS_DEV && (
<>
<Link label="View storybook" to="/sys/debug">
<ColorPalette size="md" />
a.absolute,
{height: frameHeight},
{bottom: '100%', left: 0, right: 0},
t.atoms.bg,
]}
/>,
)}
<Animated.View
style={[
t.atoms.bg,
t.atoms.border_contrast_medium,
headerMinimalShellTransform,
]}
onLayout={e => {
headerHeight.value = e.nativeEvent.layout.height
}}>
<View style={[styles.topBar]}>
<View style={[{width: 100}]}>
<TouchableOpacity
testID="viewHeaderDrawerBtn"
onPress={onPressAvi}
accessibilityRole="button"
accessibilityLabel={_(msg`Open navigation`)}
accessibilityHint={_(
msg`Access profile and other navigation links`,
)}
hitSlop={HITSLOP_10}>
<Menu size="lg" fill={t.atoms.text_contrast_medium.color} />
</TouchableOpacity>
</View>
<Animated.View style={[animatedLogoContainerStyle]}>
<LayoutAnimationConfig skipEntering skipExiting>
{showSpinner ? (
<Animated.View
key={1}
entering={ZoomIn.delay(200)}
exiting={ZoomOut}
pointerEvents="none"
style={[a.absolute, a.inset_0, a.align_center]}>
<ActivityIndicator
size="small"
color={t.atoms.text_contrast_low.color}
/>
</Animated.View>
) : (
<Animated.View
key={2}
entering={ZoomIn.delay(300)}
exiting={ZoomOut}
style={[a.absolute, a.inset_0, a.align_center]}>
<Animated.View style={[animatedJiggleStyle]}>
<Trigger>
{ctx => (
<Icon
width={28}
style={{
opacity: ctx.pressed ? 0.8 : 1,
}}
/>
)}
</Trigger>
</Animated.View>
{/* <Logo width={30} /> */}
</Animated.View>
)}
</LayoutAnimationConfig>
</Animated.View>
<View
style={[
a.flex_row,
a.justify_end,
a.align_center,
a.gap_md,
t.atoms.bg,
{width: 100},
]}>
{IS_DEV && (
<>
<Link label="View storybook" to="/sys/debug">
<ColorPalette size="md" />
</Link>
</>
)}
{hasSession && (
<Link
testID="viewHeaderHomeFeedPrefsBtn"
to="/feeds"
hitSlop={HITSLOP_10}
label={_(msg`View your feeds and explore more`)}
size="small"
variant="ghost"
color="secondary"
shape="square"
style={[
a.justify_center,
{
marginTop: 2,
marginRight: -6,
},
]}>
<FeedsIcon
size="lg"
fill={t.atoms.text_contrast_medium.color}
/>
</Link>
</>
)}
{hasSession && (
<Link
testID="viewHeaderHomeFeedPrefsBtn"
to="/feeds"
hitSlop={HITSLOP_10}
label={_(msg`View your feeds and explore more`)}
size="small"
variant="ghost"
color="secondary"
shape="square"
style={[
a.justify_center,
{
marginTop: 2,
marginRight: -6,
},
]}>
<FeedsIcon size="lg" fill={t.atoms.text_contrast_medium.color} />
</Link>
)}
)}
</View>
</View>
</View>
{children}
{children}
</Animated.View>
</Animated.View>
)
}
@@ -170,7 +257,6 @@ const styles = StyleSheet.create({
left: 0,
right: 0,
top: 0,
flexDirection: 'column',
},
topBar: {
flexDirection: 'row',
@@ -185,3 +271,25 @@ const styles = StyleSheet.create({
fontSize: 21,
},
})
function useShowSpinner({scrollY}: {scrollY?: SharedValue<number>}) {
console.log(scrollY?.value)
const isOverscrolled = useOverscrolled({scrollY, threshold: -15})
const isSignificantlyOverscrolled = useOverscrolled({scrollY, threshold: -65})
const [
hasBeenSignificantlyOverscrolled,
setHasBeenSignificantlyOverscrolled,
] = useState(isSignificantlyOverscrolled)
if (isSignificantlyOverscrolled && !hasBeenSignificantlyOverscrolled) {
setHasBeenSignificantlyOverscrolled(true)
} else if (
!isOverscrolled &&
!isSignificantlyOverscrolled &&
hasBeenSignificantlyOverscrolled
) {
setHasBeenSignificantlyOverscrolled(false)
}
return isOverscrolled && hasBeenSignificantlyOverscrolled
}
+11 -2
View File
@@ -9,9 +9,9 @@ import {
import EventEmitter from 'eventemitter3'
import {ScrollProvider} from '#/lib/ScrollContext'
import {isNative, isWeb} from '#/platform/detection'
import {useMinimalShellMode} from '#/state/shell'
import {useShellLayout} from '#/state/shell/shell-layout'
import {isNative, isWeb} from 'platform/detection'
const WEB_HIDE_SHELL_THRESHOLD = 200
@@ -20,7 +20,13 @@ function clamp(num: number, min: number, max: number) {
return Math.min(Math.max(num, min), max)
}
export function MainScrollProvider({children}: {children: React.ReactNode}) {
export function MainScrollProvider({
children,
onScroll: onScrollProp,
}: {
children: React.ReactNode
onScroll?: (e: NativeScrollEvent) => void
}) {
const {headerHeight} = useShellLayout()
const {headerMode} = useMinimalShellMode()
const startDragOffset = useSharedValue<number | null>(null)
@@ -112,6 +118,8 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
(e: NativeScrollEvent) => {
'worklet'
if (isNative) {
if (onScrollProp) onScrollProp(e)
if (startDragOffset.value === null || startMode.value === null) {
if (
headerMode.value !== 0 &&
@@ -164,6 +172,7 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
startDragOffset,
startMode,
didJustRestoreScroll,
onScrollProp,
],
)
+34 -11
View File
@@ -1,10 +1,22 @@
import React from 'react'
import {ActivityIndicator, StyleSheet, View} from 'react-native'
import {
ActivityIndicator,
NativeScrollEvent,
StyleSheet,
View,
} from 'react-native'
import {useSharedValue} from 'react-native-reanimated'
import {useFocusEffect} from '@react-navigation/native'
import {PROD_DEFAULT_FEED} from '#/lib/constants'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {useOTAUpdates} from '#/lib/hooks/useOTAUpdates'
import {useSetTitle} from '#/lib/hooks/useSetTitle'
import {useRequestNotificationsPermission} from '#/lib/notifications/notifications'
import {
HomeTabNavigatorParams,
NativeStackScreenProps,
} from '#/lib/routes/types'
import {logEvent, LogEvents} from '#/lib/statsig/statsig'
import {emitSoftReset} from '#/state/events'
import {SavedFeedSourceInfo, usePinnedFeedsInfos} from '#/state/queries/feed'
@@ -14,16 +26,13 @@ import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
import {useSession} from '#/state/session'
import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell'
import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed'
import {useOTAUpdates} from 'lib/hooks/useOTAUpdates'
import {useRequestNotificationsPermission} from 'lib/notifications/notifications'
import {HomeTabNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
import {FeedPage} from 'view/com/feeds/FeedPage'
import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState'
import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState'
import {FollowingEndOfFeed} from 'view/com/posts/FollowingEndOfFeed'
import {FeedPage} from '#/view/com/feeds/FeedPage'
import {HomeHeader} from '#/view/com/home/HomeHeader'
import {Pager, PagerRef, RenderTabBarFnProps} from '#/view/com/pager/Pager'
import {CustomFeedEmptyState} from '#/view/com/posts/CustomFeedEmptyState'
import {FollowingEmptyState} from '#/view/com/posts/FollowingEmptyState'
import {FollowingEndOfFeed} from '#/view/com/posts/FollowingEndOfFeed'
import {NoFeedsPinned} from '#/screens/Home/NoFeedsPinned'
import {HomeHeader} from '../com/home/HomeHeader'
type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home' | 'Start'>
export function HomeScreen(props: Props) {
@@ -81,6 +90,7 @@ function HomeScreenReady({
const selectedIndex = Math.max(0, maybeFoundIndex)
const selectedFeed = allFeeds[selectedIndex]
const requestNotificationsPermission = useRequestNotificationsPermission()
const scrollY = useSharedValue(0)
useSetTitle(pinnedFeedInfos[selectedIndex]?.displayName)
useOTAUpdates()
@@ -176,10 +186,11 @@ function HomeScreenReady({
testID="homeScreenFeedTabs"
onPressSelected={onPressSelected}
feeds={pinnedFeedInfos}
scrollY={scrollY}
/>
)
},
[onPressSelected, pinnedFeedInfos],
[onPressSelected, pinnedFeedInfos, scrollY],
)
const renderFollowingEmptyState = React.useCallback(() => {
@@ -201,6 +212,15 @@ function HomeScreenReady({
}
}, [preferences])
const onScrollWorklet = React.useCallback(
(e: NativeScrollEvent) => {
'worklet'
const nextScrollY = e.contentOffset.y
scrollY.value = nextScrollY
},
[scrollY],
)
return hasSession ? (
<Pager
key={allFeeds.join(',')}
@@ -224,6 +244,7 @@ function HomeScreenReady({
feedParams={homeFeedParams}
renderEmptyState={renderFollowingEmptyState}
renderEndOfFeed={FollowingEndOfFeed}
onScroll={onScrollWorklet}
/>
)
}
@@ -236,6 +257,7 @@ function HomeScreenReady({
feed={feed}
renderEmptyState={renderCustomFeedEmptyState}
savedFeedConfig={savedFeedConfig}
onScroll={onScrollWorklet}
/>
)
})
@@ -254,6 +276,7 @@ function HomeScreenReady({
isPageFocused
feed={`feedgen|${PROD_DEFAULT_FEED('whats-hot')}`}
renderEmptyState={renderCustomFeedEmptyState}
onScroll={onScrollWorklet}
/>
</Pager>
)