-
Notifications
You must be signed in to change notification settings - Fork 3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #33756 from margelo/@chrispader/restructure-multi-…
…gesture-canvas Extract gesture logic from `MultiGestureCanvas` to hooks and improve code
- Loading branch information
Showing
33 changed files
with
1,442 additions
and
1,223 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
5 changes: 0 additions & 5 deletions
5
src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.js
This file was deleted.
Oops, something went wrong.
17 changes: 17 additions & 0 deletions
17
src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import type {ForwardedRef} from 'react'; | ||
import {createContext} from 'react'; | ||
import type PagerView from 'react-native-pager-view'; | ||
import type {SharedValue} from 'react-native-reanimated'; | ||
|
||
type AttachmentCarouselPagerContextValue = { | ||
pagerRef: ForwardedRef<PagerView>; | ||
isPagerScrolling: SharedValue<boolean>; | ||
isScrollEnabled: SharedValue<boolean>; | ||
onTap: () => void; | ||
onScaleChanged: (scale: number) => void; | ||
}; | ||
|
||
const AttachmentCarouselPagerContext = createContext<AttachmentCarouselPagerContextValue | null>(null); | ||
|
||
export default AttachmentCarouselPagerContext; | ||
export type {AttachmentCarouselPagerContextValue}; |
172 changes: 0 additions & 172 deletions
172
src/components/Attachments/AttachmentCarousel/Pager/index.js
This file was deleted.
Oops, something went wrong.
150 changes: 150 additions & 0 deletions
150
src/components/Attachments/AttachmentCarousel/Pager/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
import type {ForwardedRef} from 'react'; | ||
import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; | ||
import {View} from 'react-native'; | ||
import type {NativeViewGestureHandlerProps} from 'react-native-gesture-handler'; | ||
import {createNativeWrapper} from 'react-native-gesture-handler'; | ||
import type {PagerViewProps} from 'react-native-pager-view'; | ||
import PagerView from 'react-native-pager-view'; | ||
import Animated, {useAnimatedProps, useSharedValue} from 'react-native-reanimated'; | ||
import useThemeStyles from '@hooks/useThemeStyles'; | ||
import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext'; | ||
import usePageScrollHandler from './usePageScrollHandler'; | ||
|
||
const WrappedPagerView = createNativeWrapper(PagerView) as React.ForwardRefExoticComponent< | ||
PagerViewProps & NativeViewGestureHandlerProps & React.RefAttributes<React.Component<PagerViewProps>> | ||
>; | ||
const AnimatedPagerView = Animated.createAnimatedComponent(WrappedPagerView); | ||
|
||
type AttachmentCarouselPagerHandle = { | ||
setPage: (selectedPage: number) => void; | ||
}; | ||
|
||
type PagerItem = { | ||
key: string; | ||
url: string; | ||
source: string; | ||
}; | ||
|
||
type AttachmentCarouselPagerProps = { | ||
items: PagerItem[]; | ||
renderItem: (props: {item: PagerItem; index: number; isActive: boolean}) => React.ReactNode; | ||
initialIndex: number; | ||
onPageSelected: () => void; | ||
onRequestToggleArrows: (showArrows?: boolean) => void; | ||
}; | ||
|
||
function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelected, onRequestToggleArrows}: AttachmentCarouselPagerProps, ref: ForwardedRef<AttachmentCarouselPagerHandle>) { | ||
const styles = useThemeStyles(); | ||
const pagerRef = useRef<PagerView>(null); | ||
|
||
const scale = useRef(1); | ||
const isPagerScrolling = useSharedValue(false); | ||
const isScrollEnabled = useSharedValue(true); | ||
|
||
const activePage = useSharedValue(initialIndex); | ||
const [activePageState, setActivePageState] = useState(initialIndex); | ||
|
||
const pageScrollHandler = usePageScrollHandler((e) => { | ||
'worklet'; | ||
|
||
activePage.value = e.position; | ||
isPagerScrolling.value = e.offset !== 0; | ||
}, []); | ||
|
||
useEffect(() => { | ||
setActivePageState(initialIndex); | ||
activePage.value = initialIndex; | ||
}, [activePage, initialIndex]); | ||
|
||
/** | ||
* This callback is passed to the MultiGestureCanvas/Lightbox through the AttachmentCarouselPagerContext. | ||
* It is used to react to zooming/pinching and (mostly) enabling/disabling scrolling on the pager, | ||
* as well as enabling/disabling the carousel buttons. | ||
*/ | ||
const handleScaleChange = useCallback( | ||
(newScale: number) => { | ||
if (newScale === scale.current) { | ||
return; | ||
} | ||
|
||
scale.current = newScale; | ||
|
||
const newIsScrollEnabled = newScale === 1; | ||
if (isScrollEnabled.value === newIsScrollEnabled) { | ||
return; | ||
} | ||
|
||
isScrollEnabled.value = newIsScrollEnabled; | ||
onRequestToggleArrows(newIsScrollEnabled); | ||
}, | ||
[isScrollEnabled, onRequestToggleArrows], | ||
); | ||
|
||
/** | ||
* This callback is passed to the MultiGestureCanvas/Lightbox through the AttachmentCarouselPagerContext. | ||
* It is used to trigger touch events on the pager when the user taps on the MultiGestureCanvas/Lightbox. | ||
*/ | ||
const handleTap = useCallback(() => { | ||
if (!isScrollEnabled.value) { | ||
return; | ||
} | ||
|
||
onRequestToggleArrows(); | ||
}, [isScrollEnabled.value, onRequestToggleArrows]); | ||
|
||
const contextValue = useMemo( | ||
() => ({ | ||
pagerRef, | ||
isPagerScrolling, | ||
isScrollEnabled, | ||
onTap: handleTap, | ||
onScaleChanged: handleScaleChange, | ||
}), | ||
[isPagerScrolling, isScrollEnabled, handleTap, handleScaleChange], | ||
); | ||
|
||
const animatedProps = useAnimatedProps(() => ({ | ||
scrollEnabled: isScrollEnabled.value, | ||
})); | ||
|
||
/** | ||
* This "useImperativeHandle" call is needed to expose certain imperative methods via the pager's ref. | ||
* setPage: can be used to programmatically change the page from a parent component | ||
*/ | ||
useImperativeHandle<AttachmentCarouselPagerHandle, AttachmentCarouselPagerHandle>( | ||
ref, | ||
() => ({ | ||
setPage: (selectedPage) => { | ||
pagerRef.current?.setPage(selectedPage); | ||
}, | ||
}), | ||
[], | ||
); | ||
|
||
return ( | ||
<AttachmentCarouselPagerContext.Provider value={contextValue}> | ||
<AnimatedPagerView | ||
pageMargin={40} | ||
offscreenPageLimit={1} | ||
onPageScroll={pageScrollHandler} | ||
onPageSelected={onPageSelected} | ||
ref={pagerRef} | ||
style={styles.flex1} | ||
initialPage={initialIndex} | ||
animatedProps={animatedProps} | ||
> | ||
{items.map((item, index) => ( | ||
<View | ||
key={item.source} | ||
style={styles.flex1} | ||
> | ||
{renderItem({item, index, isActive: index === activePageState})} | ||
</View> | ||
))} | ||
</AnimatedPagerView> | ||
</AttachmentCarouselPagerContext.Provider> | ||
); | ||
} | ||
AttachmentCarouselPager.displayName = 'AttachmentCarouselPager'; | ||
|
||
export default React.forwardRef(AttachmentCarouselPager); |
41 changes: 41 additions & 0 deletions
41
src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import type {PagerViewProps} from 'react-native-pager-view'; | ||
import {useEvent, useHandler} from 'react-native-reanimated'; | ||
|
||
type PageScrollHandler = NonNullable<PagerViewProps['onPageScroll']>; | ||
|
||
type PageScrollEventData = Parameters<PageScrollHandler>[0]['nativeEvent']; | ||
type PageScrollContext = Record<string, unknown>; | ||
|
||
// Reanimated doesn't expose the type for animated event handlers, therefore we must infer it from the useHandler hook. | ||
// The AnimatedPageScrollHandler type is the type of the onPageScroll prop from react-native-pager-view as an animated handler. | ||
type AnimatedHandlers = Parameters<typeof useHandler<PageScrollEventData, PageScrollContext>>[0]; | ||
type AnimatedPageScrollHandler = AnimatedHandlers[string]; | ||
|
||
type Dependencies = Parameters<typeof useHandler>[1]; | ||
|
||
/** | ||
* This hook is used to create a wrapped handler for the onPageScroll event from react-native-pager-view. | ||
* The produced handler can react to the onPageScroll event and allows to use it with animated shared values (from REA) | ||
* This hook is a wrapper around the useHandler and useEvent hooks from react-native-reanimated. | ||
* @param onPageScroll The handler for the onPageScroll event from react-native-pager-view | ||
* @param dependencies The dependencies for the useHandler hook | ||
* @returns A wrapped/animated handler for the onPageScroll event from react-native-pager-view | ||
*/ | ||
const usePageScrollHandler = (onPageScroll: AnimatedPageScrollHandler, dependencies: Dependencies): PageScrollHandler => { | ||
const {context, doDependenciesDiffer} = useHandler({onPageScroll}, dependencies); | ||
const subscribeForEvents = ['onPageScroll']; | ||
|
||
return useEvent( | ||
(event) => { | ||
'worklet'; | ||
|
||
if (onPageScroll && event.eventName.endsWith('onPageScroll')) { | ||
onPageScroll(event, context); | ||
} | ||
}, | ||
subscribeForEvents, | ||
doDependenciesDiffer, | ||
); | ||
}; | ||
|
||
export default usePageScrollHandler; |
Oops, something went wrong.