From ed1ad25ceacc714b9e68c7fb4e98ca3947ce93af Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 26 Sep 2024 00:48:45 +0100 Subject: [PATCH] scroll animation on logo --- src/view/com/feeds/FeedPage.tsx | 14 +- src/view/com/home/HomeHeader.tsx | 10 +- src/view/com/home/HomeHeaderLayout.web.tsx | 4 +- src/view/com/home/HomeHeaderLayoutMobile.tsx | 264 +++++++++++++------ src/view/com/util/MainScrollProvider.tsx | 13 +- src/view/screens/Home.tsx | 45 +++- 6 files changed, 247 insertions(+), 103 deletions(-) diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx index f1d03a7ca3..340986eb5e 100644 --- a/src/view/com/feeds/FeedPage.tsx +++ b/src/view/com/feeds/FeedPage.tsx @@ -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 ( - + void feeds: FeedSourceInfo[] + scrollY: SharedValue }, ) { - const {feeds} = props + const {feeds, scrollY} = props const {hasSession} = useSession() const navigation = useNavigation() const pal = usePalette('default') @@ -53,7 +55,7 @@ export function HomeHeader( ) return ( - + }) { 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 ( { - headerHeight.value = e.nativeEvent.layout.height - }}> - - - - - - - - - {ctx => ( - - )} - - {/* */} - + pointerEvents="box-none" + style={[styles.tabBar, animatedContainerStyle]}> + {native( - {IS_DEV && ( - <> - - + a.absolute, + {height: frameHeight}, + {bottom: '100%', left: 0, right: 0}, + t.atoms.bg, + ]} + />, + )} + { + headerHeight.value = e.nativeEvent.layout.height + }}> + + + + + + + + + {showSpinner ? ( + + + + ) : ( + + + + {ctx => ( + + )} + + + {/* */} + + )} + + + + {IS_DEV && ( + <> + + + + + )} + {hasSession && ( + + - - )} - {hasSession && ( - - - - )} + )} + - - {children} + {children} + ) } @@ -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}) { + 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 +} diff --git a/src/view/com/util/MainScrollProvider.tsx b/src/view/com/util/MainScrollProvider.tsx index c87ee209ec..b78672e437 100644 --- a/src/view/com/util/MainScrollProvider.tsx +++ b/src/view/com/util/MainScrollProvider.tsx @@ -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(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, ], ) diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index c790a815b6..c949e17416 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -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 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 ? ( ) } @@ -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} /> )