From 15b35b34b614ed683effb4db754aa80a6d15ed2d Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 14 Dec 2023 17:16:36 +0100 Subject: [PATCH 001/107] start migration --- .../Pager/AttachmentCarouselPagerContext.js | 5 - .../AttachmentCarousel/Pager/index.js | 172 ----- src/components/Lightbox.js | 2 +- .../MultiGestureCanvas/getCanvasFitScale.ts | 22 - src/components/MultiGestureCanvas/index.js | 602 ------------------ 5 files changed, 1 insertion(+), 802 deletions(-) delete mode 100644 src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.js delete mode 100644 src/components/Attachments/AttachmentCarousel/Pager/index.js delete mode 100644 src/components/MultiGestureCanvas/getCanvasFitScale.ts delete mode 100644 src/components/MultiGestureCanvas/index.js diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.js b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.js deleted file mode 100644 index abaf06900853..000000000000 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.js +++ /dev/null @@ -1,5 +0,0 @@ -import {createContext} from 'react'; - -const AttachmentCarouselPagerContext = createContext(null); - -export default AttachmentCarouselPagerContext; diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.js b/src/components/Attachments/AttachmentCarousel/Pager/index.js deleted file mode 100644 index 553e963a3461..000000000000 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.js +++ /dev/null @@ -1,172 +0,0 @@ -import PropTypes from 'prop-types'; -import React, {useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; -import {View} from 'react-native'; -import {createNativeWrapper} from 'react-native-gesture-handler'; -import PagerView from 'react-native-pager-view'; -import Animated, {runOnJS, useAnimatedProps, useAnimatedReaction, useEvent, useHandler, useSharedValue} from 'react-native-reanimated'; -import _ from 'underscore'; -import refPropTypes from '@components/refPropTypes'; -import useThemeStyles from '@hooks/useThemeStyles'; -import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext'; - -const AnimatedPagerView = Animated.createAnimatedComponent(createNativeWrapper(PagerView)); - -function usePageScrollHandler(handlers, dependencies) { - const {context, doDependenciesDiffer} = useHandler(handlers, dependencies); - const subscribeForEvents = ['onPageScroll']; - - return useEvent( - (event) => { - 'worklet'; - - const {onPageScroll} = handlers; - if (onPageScroll && event.eventName.endsWith('onPageScroll')) { - onPageScroll(event, context); - } - }, - subscribeForEvents, - doDependenciesDiffer, - ); -} - -const noopWorklet = () => { - 'worklet'; - - // noop -}; - -const pagerPropTypes = { - items: PropTypes.arrayOf( - PropTypes.shape({ - key: PropTypes.string, - url: PropTypes.string, - }), - ).isRequired, - renderItem: PropTypes.func.isRequired, - initialIndex: PropTypes.number, - onPageSelected: PropTypes.func, - onTap: PropTypes.func, - onSwipe: PropTypes.func, - onSwipeSuccess: PropTypes.func, - onSwipeDown: PropTypes.func, - onPinchGestureChange: PropTypes.func, - forwardedRef: refPropTypes, -}; - -const pagerDefaultProps = { - initialIndex: 0, - onPageSelected: () => {}, - onTap: () => {}, - onSwipe: noopWorklet, - onSwipeSuccess: () => {}, - onSwipeDown: () => {}, - onPinchGestureChange: () => {}, - forwardedRef: null, -}; - -function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelected, onTap, onSwipe = noopWorklet, onSwipeSuccess, onSwipeDown, onPinchGestureChange, forwardedRef}) { - const styles = useThemeStyles(); - const shouldPagerScroll = useSharedValue(true); - const pagerRef = useRef(null); - - const isScrolling = useSharedValue(false); - const activeIndex = useSharedValue(initialIndex); - - const pageScrollHandler = usePageScrollHandler( - { - onPageScroll: (e) => { - 'worklet'; - - activeIndex.value = e.position; - isScrolling.value = e.offset !== 0; - }, - }, - [], - ); - - const [activePage, setActivePage] = useState(initialIndex); - - useEffect(() => { - setActivePage(initialIndex); - activeIndex.value = initialIndex; - }, [activeIndex, initialIndex]); - - // we use reanimated for this since onPageSelected is called - // in the middle of the pager animation - useAnimatedReaction( - () => isScrolling.value, - (stillScrolling) => { - if (stillScrolling) { - return; - } - - runOnJS(setActivePage)(activeIndex.value); - }, - ); - - useImperativeHandle( - forwardedRef, - () => ({ - setPage: (...props) => pagerRef.current.setPage(...props), - }), - [], - ); - - const animatedProps = useAnimatedProps(() => ({ - scrollEnabled: shouldPagerScroll.value, - })); - - const contextValue = useMemo( - () => ({ - isScrolling, - pagerRef, - shouldPagerScroll, - onPinchGestureChange, - onTap, - onSwipe, - onSwipeSuccess, - onSwipeDown, - }), - [isScrolling, pagerRef, shouldPagerScroll, onPinchGestureChange, onTap, onSwipe, onSwipeSuccess, onSwipeDown], - ); - - return ( - - - {_.map(items, (item, index) => ( - - {renderItem({item, index, isActive: index === activePage})} - - ))} - - - ); -} - -AttachmentCarouselPager.propTypes = pagerPropTypes; -AttachmentCarouselPager.defaultProps = pagerDefaultProps; -AttachmentCarouselPager.displayName = 'AttachmentCarouselPager'; - -const AttachmentCarouselPagerWithRef = React.forwardRef((props, ref) => ( - -)); - -AttachmentCarouselPagerWithRef.displayName = 'AttachmentCarouselPagerWithRef'; - -export default AttachmentCarouselPagerWithRef; diff --git a/src/components/Lightbox.js b/src/components/Lightbox.js index d0d5a1653242..0f570d6d0d01 100644 --- a/src/components/Lightbox.js +++ b/src/components/Lightbox.js @@ -6,8 +6,8 @@ import useStyleUtils from '@styles/useStyleUtils'; import * as AttachmentsPropTypes from './Attachments/propTypes'; import Image from './Image'; import MultiGestureCanvas from './MultiGestureCanvas'; -import getCanvasFitScale from './MultiGestureCanvas/getCanvasFitScale'; import {zoomRangeDefaultProps, zoomRangePropTypes} from './MultiGestureCanvas/propTypes'; +import getCanvasFitScale from './MultiGestureCanvas/utils'; // Increase/decrease this number to change the number of concurrent lightboxes // The more concurrent lighboxes, the worse performance gets (especially on low-end devices) diff --git a/src/components/MultiGestureCanvas/getCanvasFitScale.ts b/src/components/MultiGestureCanvas/getCanvasFitScale.ts deleted file mode 100644 index e3e402fb066b..000000000000 --- a/src/components/MultiGestureCanvas/getCanvasFitScale.ts +++ /dev/null @@ -1,22 +0,0 @@ -type GetCanvasFitScale = (props: { - canvasSize: { - width: number; - height: number; - }; - contentSize: { - width: number; - height: number; - }; -}) => {scaleX: number; scaleY: number; minScale: number; maxScale: number}; - -const getCanvasFitScale: GetCanvasFitScale = ({canvasSize, contentSize}) => { - const scaleX = canvasSize.width / contentSize.width; - const scaleY = canvasSize.height / contentSize.height; - - const minScale = Math.min(scaleX, scaleY); - const maxScale = Math.max(scaleX, scaleY); - - return {scaleX, scaleY, minScale, maxScale}; -}; - -export default getCanvasFitScale; diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js deleted file mode 100644 index c5fd2632c22d..000000000000 --- a/src/components/MultiGestureCanvas/index.js +++ /dev/null @@ -1,602 +0,0 @@ -import React, {useContext, useEffect, useMemo, useRef, useState} from 'react'; -import {View} from 'react-native'; -import {Gesture, GestureDetector} from 'react-native-gesture-handler'; -import Animated, { - cancelAnimation, - runOnJS, - runOnUI, - useAnimatedReaction, - useAnimatedStyle, - useDerivedValue, - useSharedValue, - useWorkletCallback, - withDecay, - withSpring, -} from 'react-native-reanimated'; -import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useThemeStyles from '@hooks/useThemeStyles'; -import getCanvasFitScale from './getCanvasFitScale'; -import {defaultZoomRange, multiGestureCanvasDefaultProps, multiGestureCanvasPropTypes} from './propTypes'; - -const DOUBLE_TAP_SCALE = 3; - -const zoomScaleBounceFactors = { - min: 0.7, - max: 1.5, -}; - -const SPRING_CONFIG = { - mass: 1, - stiffness: 1000, - damping: 500, -}; - -function clamp(value, lowerBound, upperBound) { - 'worklet'; - - return Math.min(Math.max(lowerBound, value), upperBound); -} - -function getDeepDefaultProps({contentSize: contentSizeProp = {}, zoomRange: zoomRangeProp = {}}) { - const contentSize = { - width: contentSizeProp.width == null ? 1 : contentSizeProp.width, - height: contentSizeProp.height == null ? 1 : contentSizeProp.height, - }; - - const zoomRange = { - min: zoomRangeProp.min == null ? defaultZoomRange.min : zoomRangeProp.min, - max: zoomRangeProp.max == null ? defaultZoomRange.max : zoomRangeProp.max, - }; - - return {contentSize, zoomRange}; -} - -function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, children, ...props}) { - const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); - const {contentSize, zoomRange} = getDeepDefaultProps(props); - - const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); - - const pagerRefFallback = useRef(null); - const {onTap, onSwipe, onSwipeSuccess, pagerRef, shouldPagerScroll, isScrolling, onPinchGestureChange} = attachmentCarouselPagerContext || { - onTap: () => undefined, - onSwipe: () => undefined, - onSwipeSuccess: () => undefined, - onPinchGestureChange: () => undefined, - pagerRef: pagerRefFallback, - shouldPagerScroll: false, - isScrolling: false, - ...props, - }; - - const {minScale: minContentScale, maxScale: maxContentScale} = useMemo(() => getCanvasFitScale({canvasSize, contentSize}), [canvasSize, contentSize]); - const scaledWidth = useMemo(() => contentSize.width * minContentScale, [contentSize.width, minContentScale]); - const scaledHeight = useMemo(() => contentSize.height * minContentScale, [contentSize.height, minContentScale]); - - // On double tap zoom to fill, but at least 3x zoom - const doubleTapScale = useMemo(() => Math.max(DOUBLE_TAP_SCALE, maxContentScale / minContentScale), [maxContentScale, minContentScale]); - - const zoomScale = useSharedValue(1); - // Adding together the pinch zoom scale and the initial scale to fit the content into the canvas - // Using the smaller content scale, so that the immage is not bigger than the canvas - // and not smaller than needed to fit - const totalScale = useDerivedValue(() => zoomScale.value * minContentScale, [minContentScale]); - - const zoomScaledContentWidth = useDerivedValue(() => contentSize.width * totalScale.value, [contentSize.width]); - const zoomScaledContentHeight = useDerivedValue(() => contentSize.height * totalScale.value, [contentSize.height]); - - // used for pan gesture - const translateY = useSharedValue(0); - const translateX = useSharedValue(0); - const offsetX = useSharedValue(0); - const offsetY = useSharedValue(0); - const isSwiping = useSharedValue(false); - - // used for moving fingers when pinching - const pinchTranslateX = useSharedValue(0); - const pinchTranslateY = useSharedValue(0); - const pinchBounceTranslateX = useSharedValue(0); - const pinchBounceTranslateY = useSharedValue(0); - - // storage for the the origin of the gesture - const origin = { - x: useSharedValue(0), - y: useSharedValue(0), - }; - - // storage for the pan velocity to calculate the decay - const panVelocityX = useSharedValue(0); - const panVelocityY = useSharedValue(0); - - // store scale in between gestures - const pinchScaleOffset = useSharedValue(1); - - // disable pan vertically when content is smaller than screen - const canPanVertically = useDerivedValue(() => canvasSize.height < zoomScaledContentHeight.value, [canvasSize.height]); - - // calculates bounds of the scaled content - // can we pan left/right/up/down - // can be used to limit gesture or implementing tension effect - const getBounds = useWorkletCallback(() => { - let rightBoundary = 0; - let topBoundary = 0; - - if (canvasSize.width < zoomScaledContentWidth.value) { - rightBoundary = Math.abs(canvasSize.width - zoomScaledContentWidth.value) / 2; - } - - if (canvasSize.height < zoomScaledContentHeight.value) { - topBoundary = Math.abs(zoomScaledContentHeight.value - canvasSize.height) / 2; - } - - const maxVector = {x: rightBoundary, y: topBoundary}; - const minVector = {x: -rightBoundary, y: -topBoundary}; - - const target = { - x: clamp(offsetX.value, minVector.x, maxVector.x), - y: clamp(offsetY.value, minVector.y, maxVector.y), - }; - - const isInBoundaryX = target.x === offsetX.value; - const isInBoundaryY = target.y === offsetY.value; - - return { - target, - isInBoundaryX, - isInBoundaryY, - minVector, - maxVector, - canPanLeft: target.x < maxVector.x, - canPanRight: target.x > minVector.x, - }; - }, [canvasSize.width, canvasSize.height]); - - const afterPanGesture = useWorkletCallback(() => { - const {target, isInBoundaryX, isInBoundaryY, minVector, maxVector} = getBounds(); - - if (!canPanVertically.value) { - offsetY.value = withSpring(target.y, SPRING_CONFIG); - } - - if (zoomScale.value === 1 && offsetX.value === 0 && offsetY.value === 0 && translateX.value === 0 && translateY.value === 0) { - // we don't need to run any animations - return; - } - - if (zoomScale.value <= 1) { - // just center it - offsetX.value = withSpring(0, SPRING_CONFIG); - offsetY.value = withSpring(0, SPRING_CONFIG); - return; - } - - const deceleration = 0.9915; - - if (isInBoundaryX) { - if (Math.abs(panVelocityX.value) > 0 && zoomScale.value <= zoomRange.max) { - offsetX.value = withDecay({ - velocity: panVelocityX.value, - clamp: [minVector.x, maxVector.x], - deceleration, - rubberBandEffect: false, - }); - } - } else { - offsetX.value = withSpring(target.x, SPRING_CONFIG); - } - - if (isInBoundaryY) { - if ( - Math.abs(panVelocityY.value) > 0 && - zoomScale.value <= zoomRange.max && - // limit vertical pan only when content is smaller than screen - offsetY.value !== minVector.y && - offsetY.value !== maxVector.y - ) { - offsetY.value = withDecay({ - velocity: panVelocityY.value, - clamp: [minVector.y, maxVector.y], - deceleration, - }); - } - } else { - offsetY.value = withSpring(target.y, SPRING_CONFIG, () => { - isSwiping.value = false; - }); - } - }); - - const stopAnimation = useWorkletCallback(() => { - cancelAnimation(offsetX); - cancelAnimation(offsetY); - }); - - const zoomToCoordinates = useWorkletCallback( - (canvasFocalX, canvasFocalY) => { - 'worklet'; - - stopAnimation(); - - const canvasOffsetX = Math.max(0, (canvasSize.width - scaledWidth) / 2); - const canvasOffsetY = Math.max(0, (canvasSize.height - scaledHeight) / 2); - - const contentFocal = { - x: clamp(canvasFocalX - canvasOffsetX, 0, scaledWidth), - y: clamp(canvasFocalY - canvasOffsetY, 0, scaledHeight), - }; - - const canvasCenter = { - x: canvasSize.width / 2, - y: canvasSize.height / 2, - }; - - const originContentCenter = { - x: scaledWidth / 2, - y: scaledHeight / 2, - }; - - const targetContentSize = { - width: scaledWidth * doubleTapScale, - height: scaledHeight * doubleTapScale, - }; - - const targetContentCenter = { - x: targetContentSize.width / 2, - y: targetContentSize.height / 2, - }; - - const currentOrigin = { - x: (targetContentCenter.x - canvasCenter.x) * -1, - y: (targetContentCenter.y - canvasCenter.y) * -1, - }; - - const koef = { - x: (1 / originContentCenter.x) * contentFocal.x - 1, - y: (1 / originContentCenter.y) * contentFocal.y - 1, - }; - - const target = { - x: currentOrigin.x * koef.x, - y: currentOrigin.y * koef.y, - }; - - if (targetContentSize.height < canvasSize.height) { - target.y = 0; - } - - offsetX.value = withSpring(target.x, SPRING_CONFIG); - offsetY.value = withSpring(target.y, SPRING_CONFIG); - zoomScale.value = withSpring(doubleTapScale, SPRING_CONFIG); - pinchScaleOffset.value = doubleTapScale; - }, - [scaledWidth, scaledHeight, canvasSize, doubleTapScale], - ); - - const reset = useWorkletCallback((animated) => { - pinchScaleOffset.value = 1; - - stopAnimation(); - - if (animated) { - offsetX.value = withSpring(0, SPRING_CONFIG); - offsetY.value = withSpring(0, SPRING_CONFIG); - zoomScale.value = withSpring(1, SPRING_CONFIG); - } else { - zoomScale.value = 1; - translateX.value = 0; - translateY.value = 0; - offsetX.value = 0; - offsetY.value = 0; - pinchTranslateX.value = 0; - pinchTranslateY.value = 0; - } - }); - - const doubleTap = Gesture.Tap() - .numberOfTaps(2) - .maxDelay(150) - .maxDistance(20) - .onEnd((evt) => { - if (zoomScale.value > 1) { - reset(true); - } else { - zoomToCoordinates(evt.x, evt.y); - } - - if (onScaleChanged != null) { - runOnJS(onScaleChanged)(zoomScale.value); - } - }); - - const panGestureRef = useRef(Gesture.Pan()); - - const singleTap = Gesture.Tap() - .numberOfTaps(1) - .maxDuration(50) - .requireExternalGestureToFail(doubleTap, panGestureRef) - .onBegin(() => { - stopAnimation(); - }) - .onFinalize((evt, success) => { - if (!success || !onTap) { - return; - } - - runOnJS(onTap)(); - }); - - const previousTouch = useSharedValue(null); - - const panGesture = Gesture.Pan() - .manualActivation(true) - .averageTouches(true) - .onTouchesMove((evt, state) => { - if (zoomScale.value > 1) { - state.activate(); - } - - // TODO: Swipe down to close carousel gesture - // this needs fine tuning to work properly - // if (!isScrolling.value && scale.value === 1 && previousTouch.value != null) { - // const velocityX = Math.abs(evt.allTouches[0].x - previousTouch.value.x); - // const velocityY = evt.allTouches[0].y - previousTouch.value.y; - - // // TODO: this needs tuning - // if (Math.abs(velocityY) > velocityX && velocityY > 20) { - // state.activate(); - - // isSwiping.value = true; - // previousTouch.value = null; - - // runOnJS(onSwipeDown)(); - // return; - // } - // } - - if (previousTouch.value == null) { - previousTouch.value = { - x: evt.allTouches[0].x, - y: evt.allTouches[0].y, - }; - } - }) - .simultaneousWithExternalGesture(pagerRef, doubleTap, singleTap) - .onBegin(() => { - stopAnimation(); - }) - .onChange((evt) => { - // since we running both pinch and pan gesture handlers simultaneously - // we need to make sure that we don't pan when we pinch and move fingers - // since we track it as pinch focal gesture - if (evt.numberOfPointers > 1 || isScrolling.value) { - return; - } - - panVelocityX.value = evt.velocityX; - - panVelocityY.value = evt.velocityY; - - if (!isSwiping.value) { - translateX.value += evt.changeX; - } - - if (canPanVertically.value || isSwiping.value) { - translateY.value += evt.changeY; - } - }) - .onEnd((evt) => { - previousTouch.value = null; - - if (isScrolling.value) { - return; - } - - offsetX.value += translateX.value; - offsetY.value += translateY.value; - translateX.value = 0; - translateY.value = 0; - - if (isSwiping.value) { - const enoughVelocity = Math.abs(evt.velocityY) > 300 && Math.abs(evt.velocityX) < Math.abs(evt.velocityY); - const rightDirection = (evt.translationY > 0 && evt.velocityY > 0) || (evt.translationY < 0 && evt.velocityY < 0); - - if (enoughVelocity && rightDirection) { - const maybeInvert = (v) => { - const invert = evt.velocityY < 0; - return invert ? -v : v; - }; - - offsetY.value = withSpring( - maybeInvert(contentSize.height * 2), - { - stiffness: 50, - damping: 30, - mass: 1, - overshootClamping: true, - restDisplacementThreshold: 300, - restSpeedThreshold: 300, - velocity: Math.abs(evt.velocityY) < 1200 ? maybeInvert(1200) : evt.velocityY, - }, - () => { - runOnJS(onSwipeSuccess)(); - }, - ); - return; - } - } - - afterPanGesture(); - - panVelocityX.value = 0; - panVelocityY.value = 0; - }) - .withRef(panGestureRef); - - const getAdjustedFocal = useWorkletCallback( - (focalX, focalY) => ({ - x: focalX - (canvasSize.width / 2 + offsetX.value), - y: focalY - (canvasSize.height / 2 + offsetY.value), - }), - [canvasSize.width, canvasSize.height], - ); - - // used to store event scale value when we limit scale - const pinchGestureScale = useSharedValue(1); - const pinchGestureRunning = useSharedValue(false); - const pinchGesture = Gesture.Pinch() - .onTouchesDown((evt, state) => { - // we don't want to activate pinch gesture when we are scrolling pager - if (!isScrolling.value) { - return; - } - - state.fail(); - }) - .simultaneousWithExternalGesture(panGesture, doubleTap) - .onStart((evt) => { - pinchGestureRunning.value = true; - - stopAnimation(); - - const adjustFocal = getAdjustedFocal(evt.focalX, evt.focalY); - - origin.x.value = adjustFocal.x; - origin.y.value = adjustFocal.y; - }) - .onChange((evt) => { - const newZoomScale = pinchScaleOffset.value * evt.scale; - - if (zoomScale.value >= zoomRange.min * zoomScaleBounceFactors.min && zoomScale.value <= zoomRange.max * zoomScaleBounceFactors.max) { - zoomScale.value = newZoomScale; - pinchGestureScale.value = evt.scale; - } - - const adjustedFocal = getAdjustedFocal(evt.focalX, evt.focalY); - const newPinchTranslateX = adjustedFocal.x + pinchGestureScale.value * origin.x.value * -1; - const newPinchTranslateY = adjustedFocal.y + pinchGestureScale.value * origin.y.value * -1; - - if (zoomScale.value >= zoomRange.min && zoomScale.value <= zoomRange.max) { - pinchTranslateX.value = newPinchTranslateX; - pinchTranslateY.value = newPinchTranslateY; - } else { - pinchBounceTranslateX.value = newPinchTranslateX - pinchTranslateX.value; - pinchBounceTranslateY.value = newPinchTranslateY - pinchTranslateY.value; - } - }) - .onEnd(() => { - offsetX.value += pinchTranslateX.value; - offsetY.value += pinchTranslateY.value; - pinchTranslateX.value = 0; - pinchTranslateY.value = 0; - pinchScaleOffset.value = zoomScale.value; - pinchGestureScale.value = 1; - - if (pinchScaleOffset.value < zoomRange.min) { - pinchScaleOffset.value = zoomRange.min; - zoomScale.value = withSpring(zoomRange.min, SPRING_CONFIG); - } else if (pinchScaleOffset.value > zoomRange.max) { - pinchScaleOffset.value = zoomRange.max; - zoomScale.value = withSpring(zoomRange.max, SPRING_CONFIG); - } - - if (pinchBounceTranslateX.value !== 0 || pinchBounceTranslateY.value !== 0) { - pinchBounceTranslateX.value = withSpring(0, SPRING_CONFIG); - pinchBounceTranslateY.value = withSpring(0, SPRING_CONFIG); - } - - pinchGestureRunning.value = false; - - if (onScaleChanged != null) { - runOnJS(onScaleChanged)(zoomScale.value); - } - }); - - const [isPinchGestureInUse, setIsPinchGestureInUse] = useState(false); - useAnimatedReaction( - () => [zoomScale.value, pinchGestureRunning.value], - ([zoom, running]) => { - const newIsPinchGestureInUse = zoom !== 1 || running; - if (isPinchGestureInUse !== newIsPinchGestureInUse) { - runOnJS(setIsPinchGestureInUse)(newIsPinchGestureInUse); - } - }, - ); - // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => onPinchGestureChange(isPinchGestureInUse), [isPinchGestureInUse]); - - const animatedStyles = useAnimatedStyle(() => { - const x = pinchTranslateX.value + pinchBounceTranslateX.value + translateX.value + offsetX.value; - const y = pinchTranslateY.value + pinchBounceTranslateY.value + translateY.value + offsetY.value; - - if (isSwiping.value) { - onSwipe(y); - } - - return { - transform: [ - { - translateX: x, - }, - { - translateY: y, - }, - {scale: totalScale.value}, - ], - }; - }); - - // reacts to scale change and enables/disables pager scroll - useAnimatedReaction( - () => zoomScale.value, - () => { - shouldPagerScroll.value = zoomScale.value === 1; - }, - ); - - const mounted = useRef(false); - useEffect(() => { - if (!mounted.current) { - mounted.current = true; - return; - } - - if (!isActive) { - runOnUI(reset)(false); - } - }, [isActive, mounted, reset]); - - return ( - - - - - {children} - - - - - ); -} -MultiGestureCanvas.propTypes = multiGestureCanvasPropTypes; -MultiGestureCanvas.defaultProps = multiGestureCanvasDefaultProps; -MultiGestureCanvas.displayName = 'MultiGestureCanvas'; - -export default MultiGestureCanvas; -export {defaultZoomRange, zoomScaleBounceFactors}; From 83df84983451c7d95a5b9f7a0c93541cda08a98b Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 14 Dec 2023 17:16:42 +0100 Subject: [PATCH 002/107] continue --- .../Pager/AttachmentCarouselPagerContext.ts | 18 + .../AttachmentCarousel/Pager/index.tsx | 174 +++++ src/components/MultiGestureCanvas/index.tsx | 623 ++++++++++++++++++ src/components/MultiGestureCanvas/utils.ts | 42 ++ 4 files changed, 857 insertions(+) create mode 100644 src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts create mode 100644 src/components/Attachments/AttachmentCarousel/Pager/index.tsx create mode 100644 src/components/MultiGestureCanvas/index.tsx create mode 100644 src/components/MultiGestureCanvas/utils.ts diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts new file mode 100644 index 000000000000..6c19d1ccdafe --- /dev/null +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -0,0 +1,18 @@ +import {createContext} from 'react'; +import PagerView from 'react-native-pager-view'; +import {SharedValue} from 'react-native-reanimated'; + +type AttachmentCarouselPagerContextType = { + onTap: () => void; + onSwipe: (y: number) => void; + onSwipeSuccess: () => void; + onPinchGestureChange: (isPinchGestureInUse: boolean) => void; + pagerRef: React.Ref; + shouldPagerScroll: SharedValue; + isScrolling: SharedValue; +}; + +const AttachmentCarouselPagerContext = createContext(null); + +export default AttachmentCarouselPagerContext; +export type {AttachmentCarouselPagerContextType}; diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx new file mode 100644 index 000000000000..7043579edd3c --- /dev/null +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -0,0 +1,174 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, {useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; +import {View} from 'react-native'; +import {createNativeWrapper} from 'react-native-gesture-handler'; +import PagerView, {PagerViewProps} from 'react-native-pager-view'; +import Animated, {runOnJS, useAnimatedProps, useAnimatedReaction, useEvent, useHandler, useSharedValue} from 'react-native-reanimated'; +import refPropTypes from '@components/refPropTypes'; +import useThemeStyles from '@hooks/useThemeStyles'; +import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext'; + +const AnimatedPagerView = Animated.createAnimatedComponent(createNativeWrapper(PagerView)); + +type PageScrollHandler = NonNullable; +type PageScrollHandlerParams = Parameters; +const usePageScrollHandler = (handlers: PageScrollHandlerParams[0], dependencies: PageScrollHandlerParams[1]): PageScrollHandler => { + const {context, doDependenciesDiffer} = useHandler(handlers, dependencies); + const subscribeForEvents = ['onPageScroll']; + + return useEvent( + (event) => { + 'worklet'; + + const {onPageScroll} = handlers; + if (onPageScroll && event.eventName.endsWith('onPageScroll')) { + onPageScroll(event, context); + } + }, + subscribeForEvents, + doDependenciesDiffer, + ); +}; + +const noopWorklet = () => { + 'worklet'; + + // noop +}; + +const pagerPropTypes = { + items: PropTypes.arrayOf( + PropTypes.shape({ + key: PropTypes.string, + url: PropTypes.string, + }), + ).isRequired, + renderItem: PropTypes.func.isRequired, + initialIndex: PropTypes.number, + onPageSelected: PropTypes.func, + onTap: PropTypes.func, + onSwipe: PropTypes.func, + onSwipeSuccess: PropTypes.func, + onSwipeDown: PropTypes.func, + onPinchGestureChange: PropTypes.func, + forwardedRef: refPropTypes, +}; + +const pagerDefaultProps = { + initialIndex: 0, + onPageSelected: () => {}, + onTap: () => {}, + onSwipe: noopWorklet, + onSwipeSuccess: () => {}, + onSwipeDown: () => {}, + onPinchGestureChange: () => {}, + forwardedRef: null, +}; + +function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelected, onTap, onSwipe = noopWorklet, onSwipeSuccess, onSwipeDown, onPinchGestureChange, forwardedRef}) { + const styles = useThemeStyles(); + const shouldPagerScroll = useSharedValue(true); + const pagerRef = useRef(null); + + const isScrolling = useSharedValue(false); + const activeIndex = useSharedValue(initialIndex); + + const pageScrollHandler = usePageScrollHandler( + { + onPageScroll: (e) => { + 'worklet'; + + activeIndex.value = e.position; + isScrolling.value = e.offset !== 0; + }, + }, + [], + ); + + const [activePage, setActivePage] = useState(initialIndex); + + useEffect(() => { + setActivePage(initialIndex); + activeIndex.value = initialIndex; + }, [activeIndex, initialIndex]); + + // we use reanimated for this since onPageSelected is called + // in the middle of the pager animation + useAnimatedReaction( + () => isScrolling.value, + (stillScrolling) => { + if (stillScrolling) { + return; + } + + runOnJS(setActivePage)(activeIndex.value); + }, + ); + + useImperativeHandle( + forwardedRef, + () => ({ + setPage: (...props) => pagerRef.current.setPage(...props), + }), + [], + ); + + const animatedProps = useAnimatedProps(() => ({ + scrollEnabled: shouldPagerScroll.value, + })); + + const contextValue = useMemo( + () => ({ + isScrolling, + pagerRef, + shouldPagerScroll, + onPinchGestureChange, + onTap, + onSwipe, + onSwipeSuccess, + onSwipeDown, + }), + [isScrolling, pagerRef, shouldPagerScroll, onPinchGestureChange, onTap, onSwipe, onSwipeSuccess, onSwipeDown], + ); + + return ( + + + {_.map(items, (item, index) => ( + + {renderItem({item, index, isActive: index === activePage})} + + ))} + + + ); +} + +AttachmentCarouselPager.propTypes = pagerPropTypes; +AttachmentCarouselPager.defaultProps = pagerDefaultProps; +AttachmentCarouselPager.displayName = 'AttachmentCarouselPager'; + +const AttachmentCarouselPagerWithRef = React.forwardRef((props, ref) => ( + +)); + +AttachmentCarouselPagerWithRef.displayName = 'AttachmentCarouselPagerWithRef'; + +export default AttachmentCarouselPagerWithRef; diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx new file mode 100644 index 000000000000..224c0c41c23a --- /dev/null +++ b/src/components/MultiGestureCanvas/index.tsx @@ -0,0 +1,623 @@ +import React, {useContext, useEffect, useMemo, useRef, useState} from 'react'; +import {View} from 'react-native'; +import {Gesture, GestureDetector} from 'react-native-gesture-handler'; +import PagerView from 'react-native-pager-view'; +import Animated, { + cancelAnimation, + runOnJS, + runOnUI, + useAnimatedReaction, + useAnimatedStyle, + useDerivedValue, + useSharedValue, + useWorkletCallback, + withDecay, + withSpring, +} from 'react-native-reanimated'; +import AttachmentCarouselPagerContext, {AttachmentCarouselPagerContextType} from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {clamp, getCanvasFitScale, getDeepDefaultProps} from './utils'; + +const DOUBLE_TAP_SCALE = 3; + +const defaultZoomRange = { + min: 1, + max: 20, +}; + +const zoomScaleBounceFactors = { + min: 0.7, + max: 1.5, +}; + +const SPRING_CONFIG = { + mass: 1, + stiffness: 1000, + damping: 500, +}; + +type MultiGestureCanvasProps = React.PropsWithChildren<{ + /** + * Wheter the canvas is currently active (in the screen) or not. + * Disables certain gestures and functionality + */ + isActive: boolean; + + /** Handles scale changed event */ + onScaleChanged: (zoomScale: number) => void; + + /** The width and height of the canvas. + * This is needed in order to properly scale the content in the canvas + */ + canvasSize: { + width: number; + height: number; + }; + + /** The width and height of the content. + * This is needed in order to properly scale the content in the canvas + */ + contentSize: { + width: number; + height: number; + }; + + /** Range of zoom that can be applied to the content by pinching or double tapping. */ + zoomRange: { + min?: number; + max?: number; + }; +}>; + +function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, children, ...props}: MultiGestureCanvasProps) { + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {contentSize, zoomRange} = getDeepDefaultProps(props); + + const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); + + const pagerRefFallback = useRef(null); + + const defaultContext: AttachmentCarouselPagerContextType = { + onTap: () => undefined, + onSwipe: () => undefined, + onSwipeSuccess: () => undefined, + onPinchGestureChange: () => undefined, + pagerRef: pagerRefFallback, + shouldPagerScroll: useSharedValue(false), + isScrolling: useSharedValue(false), + ...props, + }; + const {onTap, onSwipe, onSwipeSuccess, pagerRef, shouldPagerScroll, isScrolling, onPinchGestureChange} = attachmentCarouselPagerContext ?? defaultContext; + + const {minScale: minContentScale, maxScale: maxContentScale} = useMemo(() => getCanvasFitScale({canvasSize, contentSize}), [canvasSize, contentSize]); + const scaledWidth = useMemo(() => contentSize.width * minContentScale, [contentSize.width, minContentScale]); + const scaledHeight = useMemo(() => contentSize.height * minContentScale, [contentSize.height, minContentScale]); + + // On double tap zoom to fill, but at least 3x zoom + const doubleTapScale = useMemo(() => Math.max(DOUBLE_TAP_SCALE, maxContentScale / minContentScale), [maxContentScale, minContentScale]); + + const zoomScale = useSharedValue(1); + // Adding together the pinch zoom scale and the initial scale to fit the content into the canvas + // Using the smaller content scale, so that the immage is not bigger than the canvas + // and not smaller than needed to fit + const totalScale = useDerivedValue(() => zoomScale.value * minContentScale, [minContentScale]); + + const zoomScaledContentWidth = useDerivedValue(() => contentSize.width * totalScale.value, [contentSize.width]); + const zoomScaledContentHeight = useDerivedValue(() => contentSize.height * totalScale.value, [contentSize.height]); + + // used for pan gesture + const translateY = useSharedValue(0); + const translateX = useSharedValue(0); + const offsetX = useSharedValue(0); + const offsetY = useSharedValue(0); + const isSwiping = useSharedValue(false); + + // used for moving fingers when pinching + const pinchTranslateX = useSharedValue(0); + const pinchTranslateY = useSharedValue(0); + const pinchBounceTranslateX = useSharedValue(0); + const pinchBounceTranslateY = useSharedValue(0); + + // storage for the the origin of the gesture + const origin = { + x: useSharedValue(0), + y: useSharedValue(0), + }; + + // storage for the pan velocity to calculate the decay + const panVelocityX = useSharedValue(0); + const panVelocityY = useSharedValue(0); + + // store scale in between gestures + const pinchScaleOffset = useSharedValue(1); + + // disable pan vertically when content is smaller than screen + const canPanVertically = useDerivedValue(() => canvasSize.height < zoomScaledContentHeight.value, [canvasSize.height]); + + // calculates bounds of the scaled content + // can we pan left/right/up/down + // can be used to limit gesture or implementing tension effect + const getBounds = useWorkletCallback(() => { + let rightBoundary = 0; + let topBoundary = 0; + + if (canvasSize.width < zoomScaledContentWidth.value) { + rightBoundary = Math.abs(canvasSize.width - zoomScaledContentWidth.value) / 2; + } + + if (canvasSize.height < zoomScaledContentHeight.value) { + topBoundary = Math.abs(zoomScaledContentHeight.value - canvasSize.height) / 2; + } + + const maxVector = {x: rightBoundary, y: topBoundary}; + const minVector = {x: -rightBoundary, y: -topBoundary}; + + const target = { + x: clamp(offsetX.value, minVector.x, maxVector.x), + y: clamp(offsetY.value, minVector.y, maxVector.y), + }; + + const isInBoundaryX = target.x === offsetX.value; + const isInBoundaryY = target.y === offsetY.value; + + return { + target, + isInBoundaryX, + isInBoundaryY, + minVector, + maxVector, + canPanLeft: target.x < maxVector.x, + canPanRight: target.x > minVector.x, + }; + }, [canvasSize.width, canvasSize.height]); + + const afterPanGesture = useWorkletCallback(() => { + const {target, isInBoundaryX, isInBoundaryY, minVector, maxVector} = getBounds(); + + if (!canPanVertically.value) { + offsetY.value = withSpring(target.y, SPRING_CONFIG); + } + + if (zoomScale.value === 1 && offsetX.value === 0 && offsetY.value === 0 && translateX.value === 0 && translateY.value === 0) { + // we don't need to run any animations + return; + } + + if (zoomScale.value <= 1) { + // just center it + offsetX.value = withSpring(0, SPRING_CONFIG); + offsetY.value = withSpring(0, SPRING_CONFIG); + return; + } + + const deceleration = 0.9915; + + if (isInBoundaryX) { + if (Math.abs(panVelocityX.value) > 0 && zoomScale.value <= zoomRange.max) { + offsetX.value = withDecay({ + velocity: panVelocityX.value, + clamp: [minVector.x, maxVector.x], + deceleration, + rubberBandEffect: false, + }); + } + } else { + offsetX.value = withSpring(target.x, SPRING_CONFIG); + } + + if (isInBoundaryY) { + if ( + Math.abs(panVelocityY.value) > 0 && + zoomScale.value <= zoomRange.max && + // limit vertical pan only when content is smaller than screen + offsetY.value !== minVector.y && + offsetY.value !== maxVector.y + ) { + offsetY.value = withDecay({ + velocity: panVelocityY.value, + clamp: [minVector.y, maxVector.y], + deceleration, + }); + } + } else { + offsetY.value = withSpring(target.y, SPRING_CONFIG, () => { + isSwiping.value = false; + }); + } + }); + + const stopAnimation = useWorkletCallback(() => { + cancelAnimation(offsetX); + cancelAnimation(offsetY); + }); + + const zoomToCoordinates = useWorkletCallback( + (canvasFocalX: number, canvasFocalY: number) => { + 'worklet'; + + stopAnimation(); + + const canvasOffsetX = Math.max(0, (canvasSize.width - scaledWidth) / 2); + const canvasOffsetY = Math.max(0, (canvasSize.height - scaledHeight) / 2); + + const contentFocal = { + x: clamp(canvasFocalX - canvasOffsetX, 0, scaledWidth), + y: clamp(canvasFocalY - canvasOffsetY, 0, scaledHeight), + }; + + const canvasCenter = { + x: canvasSize.width / 2, + y: canvasSize.height / 2, + }; + + const originContentCenter = { + x: scaledWidth / 2, + y: scaledHeight / 2, + }; + + const targetContentSize = { + width: scaledWidth * doubleTapScale, + height: scaledHeight * doubleTapScale, + }; + + const targetContentCenter = { + x: targetContentSize.width / 2, + y: targetContentSize.height / 2, + }; + + const currentOrigin = { + x: (targetContentCenter.x - canvasCenter.x) * -1, + y: (targetContentCenter.y - canvasCenter.y) * -1, + }; + + const koef = { + x: (1 / originContentCenter.x) * contentFocal.x - 1, + y: (1 / originContentCenter.y) * contentFocal.y - 1, + }; + + const target = { + x: currentOrigin.x * koef.x, + y: currentOrigin.y * koef.y, + }; + + if (targetContentSize.height < canvasSize.height) { + target.y = 0; + } + + offsetX.value = withSpring(target.x, SPRING_CONFIG); + offsetY.value = withSpring(target.y, SPRING_CONFIG); + zoomScale.value = withSpring(doubleTapScale, SPRING_CONFIG); + pinchScaleOffset.value = doubleTapScale; + }, + [scaledWidth, scaledHeight, canvasSize, doubleTapScale], + ); + + const reset = useWorkletCallback((animated) => { + pinchScaleOffset.value = 1; + + stopAnimation(); + + if (animated) { + offsetX.value = withSpring(0, SPRING_CONFIG); + offsetY.value = withSpring(0, SPRING_CONFIG); + zoomScale.value = withSpring(1, SPRING_CONFIG); + } else { + zoomScale.value = 1; + translateX.value = 0; + translateY.value = 0; + offsetX.value = 0; + offsetY.value = 0; + pinchTranslateX.value = 0; + pinchTranslateY.value = 0; + } + }); + + const doubleTap = Gesture.Tap() + .numberOfTaps(2) + .maxDelay(150) + .maxDistance(20) + .onEnd((evt) => { + if (zoomScale.value > 1) { + reset(true); + } else { + zoomToCoordinates(evt.x, evt.y); + } + + if (onScaleChanged != null) { + runOnJS(onScaleChanged)(zoomScale.value); + } + }); + + const panGestureRef = useRef(Gesture.Pan()); + + const singleTap = Gesture.Tap() + .numberOfTaps(1) + .maxDuration(50) + .requireExternalGestureToFail(doubleTap, panGestureRef) + .onBegin(() => { + stopAnimation(); + }) + .onFinalize((evt, success) => { + if (!success || !onTap) { + return; + } + + runOnJS(onTap)(); + }); + + const previousTouch = useSharedValue<{ + x: number; + y: number; + } | null>(null); + + const panGesture = Gesture.Pan() + .manualActivation(true) + .averageTouches(true) + .onTouchesMove((evt, state) => { + if (zoomScale.value > 1) { + state.activate(); + } + + // TODO: Swipe down to close carousel gesture + // this needs fine tuning to work properly + // if (!isScrolling.value && scale.value === 1 && previousTouch.value != null) { + // const velocityX = Math.abs(evt.allTouches[0].x - previousTouch.value.x); + // const velocityY = evt.allTouches[0].y - previousTouch.value.y; + + // // TODO: this needs tuning + // if (Math.abs(velocityY) > velocityX && velocityY > 20) { + // state.activate(); + + // isSwiping.value = true; + // previousTouch.value = null; + + // runOnJS(onSwipeDown)(); + // return; + // } + // } + + if (previousTouch.value == null) { + previousTouch.value = { + x: evt.allTouches[0].x, + y: evt.allTouches[0].y, + }; + } + }) + .simultaneousWithExternalGesture(pagerRef, doubleTap, singleTap) + .onBegin(() => { + stopAnimation(); + }) + .onChange((evt) => { + // since we running both pinch and pan gesture handlers simultaneously + // we need to make sure that we don't pan when we pinch and move fingers + // since we track it as pinch focal gesture + if (evt.numberOfPointers > 1 || isScrolling.value) { + return; + } + + panVelocityX.value = evt.velocityX; + + panVelocityY.value = evt.velocityY; + + if (!isSwiping.value) { + translateX.value += evt.changeX; + } + + if (canPanVertically.value || isSwiping.value) { + translateY.value += evt.changeY; + } + }) + .onEnd((evt) => { + previousTouch.value = null; + + if (isScrolling.value) { + return; + } + + offsetX.value += translateX.value; + offsetY.value += translateY.value; + translateX.value = 0; + translateY.value = 0; + + if (isSwiping.value) { + const enoughVelocity = Math.abs(evt.velocityY) > 300 && Math.abs(evt.velocityX) < Math.abs(evt.velocityY); + const rightDirection = (evt.translationY > 0 && evt.velocityY > 0) || (evt.translationY < 0 && evt.velocityY < 0); + + if (enoughVelocity && rightDirection) { + const maybeInvert = (v: number) => { + const invert = evt.velocityY < 0; + return invert ? -v : v; + }; + + offsetY.value = withSpring( + maybeInvert(contentSize.height * 2), + { + stiffness: 50, + damping: 30, + mass: 1, + overshootClamping: true, + restDisplacementThreshold: 300, + restSpeedThreshold: 300, + velocity: Math.abs(evt.velocityY) < 1200 ? maybeInvert(1200) : evt.velocityY, + }, + () => { + runOnJS(onSwipeSuccess)(); + }, + ); + return; + } + } + + afterPanGesture(); + + panVelocityX.value = 0; + panVelocityY.value = 0; + }) + .withRef(panGestureRef); + + const getAdjustedFocal = useWorkletCallback( + (focalX: number, focalY: number) => ({ + x: focalX - (canvasSize.width / 2 + offsetX.value), + y: focalY - (canvasSize.height / 2 + offsetY.value), + }), + [canvasSize.width, canvasSize.height], + ); + + // used to store event scale value when we limit scale + const pinchGestureScale = useSharedValue(1); + const pinchGestureRunning = useSharedValue(false); + const pinchGesture = Gesture.Pinch() + .onTouchesDown((evt, state) => { + // we don't want to activate pinch gesture when we are scrolling pager + if (!isScrolling.value) { + return; + } + + state.fail(); + }) + .simultaneousWithExternalGesture(panGesture, doubleTap) + .onStart((evt) => { + pinchGestureRunning.value = true; + + stopAnimation(); + + const adjustFocal = getAdjustedFocal(evt.focalX, evt.focalY); + + origin.x.value = adjustFocal.x; + origin.y.value = adjustFocal.y; + }) + .onChange((evt) => { + const newZoomScale = pinchScaleOffset.value * evt.scale; + + if (zoomScale.value >= zoomRange.min * zoomScaleBounceFactors.min && zoomScale.value <= zoomRange.max * zoomScaleBounceFactors.max) { + zoomScale.value = newZoomScale; + pinchGestureScale.value = evt.scale; + } + + const adjustedFocal = getAdjustedFocal(evt.focalX, evt.focalY); + const newPinchTranslateX = adjustedFocal.x + pinchGestureScale.value * origin.x.value * -1; + const newPinchTranslateY = adjustedFocal.y + pinchGestureScale.value * origin.y.value * -1; + + if (zoomScale.value >= zoomRange.min && zoomScale.value <= zoomRange.max) { + pinchTranslateX.value = newPinchTranslateX; + pinchTranslateY.value = newPinchTranslateY; + } else { + pinchBounceTranslateX.value = newPinchTranslateX - pinchTranslateX.value; + pinchBounceTranslateY.value = newPinchTranslateY - pinchTranslateY.value; + } + }) + .onEnd(() => { + offsetX.value += pinchTranslateX.value; + offsetY.value += pinchTranslateY.value; + pinchTranslateX.value = 0; + pinchTranslateY.value = 0; + pinchScaleOffset.value = zoomScale.value; + pinchGestureScale.value = 1; + + if (pinchScaleOffset.value < zoomRange.min) { + pinchScaleOffset.value = zoomRange.min; + zoomScale.value = withSpring(zoomRange.min, SPRING_CONFIG); + } else if (pinchScaleOffset.value > zoomRange.max) { + pinchScaleOffset.value = zoomRange.max; + zoomScale.value = withSpring(zoomRange.max, SPRING_CONFIG); + } + + if (pinchBounceTranslateX.value !== 0 || pinchBounceTranslateY.value !== 0) { + pinchBounceTranslateX.value = withSpring(0, SPRING_CONFIG); + pinchBounceTranslateY.value = withSpring(0, SPRING_CONFIG); + } + + pinchGestureRunning.value = false; + + if (onScaleChanged != null) { + runOnJS(onScaleChanged)(zoomScale.value); + } + }); + + const [isPinchGestureInUse, setIsPinchGestureInUse] = useState(false); + useAnimatedReaction( + () => ({scale: zoomScale.value, isRunning: pinchGestureRunning.value}), + ({scale, isRunning}) => { + const newIsPinchGestureInUse = scale !== 1 || isRunning; + if (isPinchGestureInUse !== newIsPinchGestureInUse) { + runOnJS(setIsPinchGestureInUse)(newIsPinchGestureInUse); + } + }, + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(() => onPinchGestureChange(isPinchGestureInUse), [isPinchGestureInUse]); + + const animatedStyles = useAnimatedStyle(() => { + const x = pinchTranslateX.value + pinchBounceTranslateX.value + translateX.value + offsetX.value; + const y = pinchTranslateY.value + pinchBounceTranslateY.value + translateY.value + offsetY.value; + + if (isSwiping.value) { + onSwipe(y); + } + + return { + transform: [ + { + translateX: x, + }, + { + translateY: y, + }, + {scale: totalScale.value}, + ], + }; + }); + + // reacts to scale change and enables/disables pager scroll + useAnimatedReaction( + () => zoomScale.value, + () => { + shouldPagerScroll.value = zoomScale.value === 1; + }, + ); + + const mounted = useRef(false); + useEffect(() => { + if (!mounted.current) { + mounted.current = true; + return; + } + + if (!isActive) { + runOnUI(reset)(false); + } + }, [isActive, mounted, reset]); + + return ( + + + + + {children} + + + + + ); +} +MultiGestureCanvas.displayName = 'MultiGestureCanvas'; + +export default MultiGestureCanvas; +export {defaultZoomRange, zoomScaleBounceFactors}; diff --git a/src/components/MultiGestureCanvas/utils.ts b/src/components/MultiGestureCanvas/utils.ts new file mode 100644 index 000000000000..697ca5e65ba5 --- /dev/null +++ b/src/components/MultiGestureCanvas/utils.ts @@ -0,0 +1,42 @@ +type GetCanvasFitScale = (props: { + canvasSize: { + width: number; + height: number; + }; + contentSize: { + width: number; + height: number; + }; +}) => {scaleX: number; scaleY: number; minScale: number; maxScale: number}; + +const getCanvasFitScale: GetCanvasFitScale = ({canvasSize, contentSize}) => { + const scaleX = canvasSize.width / contentSize.width; + const scaleY = canvasSize.height / contentSize.height; + + const minScale = Math.min(scaleX, scaleY); + const maxScale = Math.max(scaleX, scaleY); + + return {scaleX, scaleY, minScale, maxScale}; +}; + +function clamp(value, lowerBound, upperBound) { + 'worklet'; + + return Math.min(Math.max(lowerBound, value), upperBound); +} + +function getDeepDefaultProps({contentSize: contentSizeProp = {}, zoomRange: zoomRangeProp = {}}) { + const contentSize = { + width: contentSizeProp.width == null ? 1 : contentSizeProp.width, + height: contentSizeProp.height == null ? 1 : contentSizeProp.height, + }; + + const zoomRange = { + min: zoomRangeProp.min == null ? defaultZoomRange.min : zoomRangeProp.min, + max: zoomRangeProp.max == null ? defaultZoomRange.max : zoomRangeProp.max, + }; + + return {contentSize, zoomRange}; +} + +export {getCanvasFitScale, clamp, getDeepDefaultProps}; From 3a8a606720b787cff978e1d8a554fb348d654a0d Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 14 Dec 2023 17:39:57 +0100 Subject: [PATCH 003/107] further type stuff --- .../AttachmentCarousel/Pager/index.tsx | 58 ++++++++------- .../Pager/usePageScrollHandler.ts | 24 ++++++ .../MultiGestureCanvas/constants.ts | 11 +++ src/components/MultiGestureCanvas/index.tsx | 26 ++----- .../MultiGestureCanvas/propTypes.js | 73 ------------------- src/components/MultiGestureCanvas/types.ts | 11 +++ src/components/MultiGestureCanvas/utils.ts | 25 +++++-- 7 files changed, 102 insertions(+), 126 deletions(-) create mode 100644 src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts create mode 100644 src/components/MultiGestureCanvas/constants.ts delete mode 100644 src/components/MultiGestureCanvas/propTypes.js create mode 100644 src/components/MultiGestureCanvas/types.ts diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index 7043579edd3c..d483b59a6edb 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -3,34 +3,15 @@ import PropTypes from 'prop-types'; import React, {useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import {createNativeWrapper} from 'react-native-gesture-handler'; -import PagerView, {PagerViewProps} from 'react-native-pager-view'; -import Animated, {runOnJS, useAnimatedProps, useAnimatedReaction, useEvent, useHandler, useSharedValue} from 'react-native-reanimated'; +import PagerView from 'react-native-pager-view'; +import Animated, {runOnJS, useAnimatedProps, useAnimatedReaction, useSharedValue} from 'react-native-reanimated'; import refPropTypes from '@components/refPropTypes'; import useThemeStyles from '@hooks/useThemeStyles'; import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext'; +import usePageScrollHandler from './usePageScrollHandler'; const AnimatedPagerView = Animated.createAnimatedComponent(createNativeWrapper(PagerView)); -type PageScrollHandler = NonNullable; -type PageScrollHandlerParams = Parameters; -const usePageScrollHandler = (handlers: PageScrollHandlerParams[0], dependencies: PageScrollHandlerParams[1]): PageScrollHandler => { - const {context, doDependenciesDiffer} = useHandler(handlers, dependencies); - const subscribeForEvents = ['onPageScroll']; - - return useEvent( - (event) => { - 'worklet'; - - const {onPageScroll} = handlers; - if (onPageScroll && event.eventName.endsWith('onPageScroll')) { - onPageScroll(event, context); - } - }, - subscribeForEvents, - doDependenciesDiffer, - ); -}; - const noopWorklet = () => { 'worklet'; @@ -66,7 +47,34 @@ const pagerDefaultProps = { forwardedRef: null, }; -function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelected, onTap, onSwipe = noopWorklet, onSwipeSuccess, onSwipeDown, onPinchGestureChange, forwardedRef}) { +type AttachmentCarouselPagerProps = React.PropsWithChildren<{ + items: Array<{ + key: string; + url: string; + }>; + renderItem: () => React.ReactNode; + initialIndex: number; + onPageSelected: () => void; + onTap: () => void; + onSwipe: () => void; + onSwipeSuccess: () => void; + onSwipeDown: () => void; + onPinchGestureChange: () => void; + forwardedRef: React.Ref; +}>; + +function AttachmentCarouselPager({ + items, + renderItem, + initialIndex, + onPageSelected, + onTap, + onSwipe = noopWorklet, + onSwipeSuccess, + onSwipeDown, + onPinchGestureChange, + forwardedRef, +}: AttachmentCarouselPagerProps) { const styles = useThemeStyles(); const shouldPagerScroll = useSharedValue(true); const pagerRef = useRef(null); @@ -144,7 +152,7 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte style={styles.flex1} initialPage={initialIndex} > - {_.map(items, (item, index) => ( + {items.map((item, index) => ( ( diff --git a/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts b/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts new file mode 100644 index 000000000000..e65f1ff3cd00 --- /dev/null +++ b/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts @@ -0,0 +1,24 @@ +import {PagerViewProps} from 'react-native-pager-view'; +import {useEvent, useHandler} from 'react-native-reanimated'; + +type PageScrollHandler = NonNullable; +type PageScrollHandlerParams = Parameters; +const usePageScrollHandler = (handlers: PageScrollHandlerParams[0], dependencies: PageScrollHandlerParams[1]): PageScrollHandler => { + const {context, doDependenciesDiffer} = useHandler(handlers, dependencies); + const subscribeForEvents = ['onPageScroll']; + + return useEvent( + (event) => { + 'worklet'; + + const {onPageScroll} = handlers; + if (onPageScroll && event.eventName.endsWith('onPageScroll')) { + onPageScroll(event, context); + } + }, + subscribeForEvents, + doDependenciesDiffer, + ); +}; + +export default usePageScrollHandler; diff --git a/src/components/MultiGestureCanvas/constants.ts b/src/components/MultiGestureCanvas/constants.ts new file mode 100644 index 000000000000..0103d07c55c2 --- /dev/null +++ b/src/components/MultiGestureCanvas/constants.ts @@ -0,0 +1,11 @@ +const defaultZoomRange = { + min: 1, + max: 20, +}; + +const zoomScaleBounceFactors = { + min: 0.7, + max: 1.5, +}; + +export {defaultZoomRange, zoomScaleBounceFactors}; diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index 224c0c41c23a..17e2eeb417e0 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -17,20 +17,12 @@ import Animated, { import AttachmentCarouselPagerContext, {AttachmentCarouselPagerContextType} from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import {zoomScaleBounceFactors} from './constants'; +import {ContentSizeProp, ZoomRangeProp} from './types'; import {clamp, getCanvasFitScale, getDeepDefaultProps} from './utils'; const DOUBLE_TAP_SCALE = 3; -const defaultZoomRange = { - min: 1, - max: 20, -}; - -const zoomScaleBounceFactors = { - min: 0.7, - max: 1.5, -}; - const SPRING_CONFIG = { mass: 1, stiffness: 1000, @@ -58,22 +50,16 @@ type MultiGestureCanvasProps = React.PropsWithChildren<{ /** The width and height of the content. * This is needed in order to properly scale the content in the canvas */ - contentSize: { - width: number; - height: number; - }; + contentSize: ContentSizeProp; /** Range of zoom that can be applied to the content by pinching or double tapping. */ - zoomRange: { - min?: number; - max?: number; - }; + zoomRange?: ZoomRangeProp; }>; function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, children, ...props}: MultiGestureCanvasProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const {contentSize, zoomRange} = getDeepDefaultProps(props); + const {contentSize, zoomRange} = getDeepDefaultProps({contentSize: props.contentSize, zoomRange: props.zoomRange}); const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); @@ -620,4 +606,4 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr MultiGestureCanvas.displayName = 'MultiGestureCanvas'; export default MultiGestureCanvas; -export {defaultZoomRange, zoomScaleBounceFactors}; +export type {MultiGestureCanvasProps}; diff --git a/src/components/MultiGestureCanvas/propTypes.js b/src/components/MultiGestureCanvas/propTypes.js deleted file mode 100644 index f1961ec0e156..000000000000 --- a/src/components/MultiGestureCanvas/propTypes.js +++ /dev/null @@ -1,73 +0,0 @@ -import PropTypes from 'prop-types'; - -const defaultZoomRange = { - min: 1, - max: 20, -}; - -const zoomRangePropTypes = { - /** Range of zoom that can be applied to the content by pinching or double tapping. */ - zoomRange: PropTypes.shape({ - min: PropTypes.number, - max: PropTypes.number, - }), -}; - -const zoomRangeDefaultProps = { - zoomRange: { - min: defaultZoomRange.min, - max: defaultZoomRange.max, - }, -}; - -const multiGestureCanvasPropTypes = { - ...zoomRangePropTypes, - - /** - * Wheter the canvas is currently active (in the screen) or not. - * Disables certain gestures and functionality - */ - isActive: PropTypes.bool, - - /** Handles scale changed event */ - onScaleChanged: PropTypes.func, - - /** The width and height of the canvas. - * This is needed in order to properly scale the content in the canvas - */ - canvasSize: PropTypes.shape({ - width: PropTypes.number.isRequired, - height: PropTypes.number.isRequired, - }).isRequired, - - /** The width and height of the content. - * This is needed in order to properly scale the content in the canvas - */ - contentSize: PropTypes.shape({ - width: PropTypes.number, - height: PropTypes.number, - }), - - /** The scale factors (scaleX, scaleY) that are used to scale the content (width/height) to the canvas size. - * `scaledWidth` and `scaledHeight` reflect the actual size of the content after scaling. - */ - contentScaling: PropTypes.shape({ - scaleX: PropTypes.number, - scaleY: PropTypes.number, - scaledWidth: PropTypes.number, - scaledHeight: PropTypes.number, - }), - - /** Content that should be transformed inside the canvas (images, pdf, ...) */ - children: PropTypes.node.isRequired, -}; - -const multiGestureCanvasDefaultProps = { - isActive: true, - onScaleChanged: () => undefined, - contentSize: undefined, - contentScaling: undefined, - zoomRange: undefined, -}; - -export {defaultZoomRange, zoomRangePropTypes, zoomRangeDefaultProps, multiGestureCanvasPropTypes, multiGestureCanvasDefaultProps}; diff --git a/src/components/MultiGestureCanvas/types.ts b/src/components/MultiGestureCanvas/types.ts new file mode 100644 index 000000000000..11dfc767aacf --- /dev/null +++ b/src/components/MultiGestureCanvas/types.ts @@ -0,0 +1,11 @@ +type ContentSizeProp = { + width: number; + height: number; +}; + +type ZoomRangeProp = { + min?: number; + max?: number; +}; + +export type {ContentSizeProp, ZoomRangeProp}; diff --git a/src/components/MultiGestureCanvas/utils.ts b/src/components/MultiGestureCanvas/utils.ts index 697ca5e65ba5..85cc59887fd5 100644 --- a/src/components/MultiGestureCanvas/utils.ts +++ b/src/components/MultiGestureCanvas/utils.ts @@ -1,3 +1,6 @@ +import {defaultZoomRange} from './constants'; +import {ContentSizeProp, ZoomRangeProp} from './types'; + type GetCanvasFitScale = (props: { canvasSize: { width: number; @@ -19,24 +22,32 @@ const getCanvasFitScale: GetCanvasFitScale = ({canvasSize, contentSize}) => { return {scaleX, scaleY, minScale, maxScale}; }; -function clamp(value, lowerBound, upperBound) { +function clamp(value: number, lowerBound: number, upperBound: number) { 'worklet'; return Math.min(Math.max(lowerBound, value), upperBound); } -function getDeepDefaultProps({contentSize: contentSizeProp = {}, zoomRange: zoomRangeProp = {}}) { +type Props = { + contentSize?: ContentSizeProp; + zoomRange?: ZoomRangeProp; +}; +type PropsWithDefault = { + contentSize: ContentSizeProp; + zoomRange: Required; +}; +const getDeepDefaultProps = ({contentSize: contentSizeProp, zoomRange: zoomRangeProp}: Props): PropsWithDefault => { const contentSize = { - width: contentSizeProp.width == null ? 1 : contentSizeProp.width, - height: contentSizeProp.height == null ? 1 : contentSizeProp.height, + width: contentSizeProp?.width ?? 1, + height: contentSizeProp?.height ?? 1, }; const zoomRange = { - min: zoomRangeProp.min == null ? defaultZoomRange.min : zoomRangeProp.min, - max: zoomRangeProp.max == null ? defaultZoomRange.max : zoomRangeProp.max, + min: zoomRangeProp?.min ?? defaultZoomRange.min, + max: zoomRangeProp?.max ?? defaultZoomRange.max, }; return {contentSize, zoomRange}; -} +}; export {getCanvasFitScale, clamp, getDeepDefaultProps}; From 1da2e9a5e97000034c8926287b8c1a149d407182 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 28 Dec 2023 14:04:43 +0100 Subject: [PATCH 004/107] fix: better gestures --- src/components/MultiGestureCanvas/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index c5fd2632c22d..9c98f2780b05 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -578,7 +578,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr }, ]} > - + Date: Fri, 29 Dec 2023 13:10:20 +0100 Subject: [PATCH 005/107] further improve variable names --- src/components/MultiGestureCanvas/index.js | 66 +++++++++++----------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index 9c98f2780b05..6ff38a15981a 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -87,35 +87,33 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const zoomScaledContentWidth = useDerivedValue(() => contentSize.width * totalScale.value, [contentSize.width]); const zoomScaledContentHeight = useDerivedValue(() => contentSize.height * totalScale.value, [contentSize.height]); - // used for pan gesture - const translateY = useSharedValue(0); - const translateX = useSharedValue(0); + // pan and pinch gesture const offsetX = useSharedValue(0); const offsetY = useSharedValue(0); + + // pan gesture + const panTranslateX = useSharedValue(0); + const panTranslateY = useSharedValue(0); const isSwiping = useSharedValue(false); + // pan velocity to calculate the decay + const panVelocityX = useSharedValue(0); + const panVelocityY = useSharedValue(0); + // disable pan vertically when content is smaller than screen + const canPanVertically = useDerivedValue(() => canvasSize.height < zoomScaledContentHeight.value, [canvasSize.height]); - // used for moving fingers when pinching + // pinch gesture const pinchTranslateX = useSharedValue(0); const pinchTranslateY = useSharedValue(0); const pinchBounceTranslateX = useSharedValue(0); const pinchBounceTranslateY = useSharedValue(0); - - // storage for the the origin of the gesture - const origin = { + // scale in between gestures + const pinchScaleOffset = useSharedValue(1); + // origin of the pinch gesture + const pinchOrigin = { x: useSharedValue(0), y: useSharedValue(0), }; - // storage for the pan velocity to calculate the decay - const panVelocityX = useSharedValue(0); - const panVelocityY = useSharedValue(0); - - // store scale in between gestures - const pinchScaleOffset = useSharedValue(1); - - // disable pan vertically when content is smaller than screen - const canPanVertically = useDerivedValue(() => canvasSize.height < zoomScaledContentHeight.value, [canvasSize.height]); - // calculates bounds of the scaled content // can we pan left/right/up/down // can be used to limit gesture or implementing tension effect @@ -153,14 +151,14 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr }; }, [canvasSize.width, canvasSize.height]); - const afterPanGesture = useWorkletCallback(() => { + const returnToBoundaries = useWorkletCallback(() => { const {target, isInBoundaryX, isInBoundaryY, minVector, maxVector} = getBounds(); if (!canPanVertically.value) { offsetY.value = withSpring(target.y, SPRING_CONFIG); } - if (zoomScale.value === 1 && offsetX.value === 0 && offsetY.value === 0 && translateX.value === 0 && translateY.value === 0) { + if (zoomScale.value === 1 && offsetX.value === 0 && offsetY.value === 0 && panTranslateX.value === 0 && panTranslateY.value === 0) { // we don't need to run any animations return; } @@ -285,8 +283,8 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr zoomScale.value = withSpring(1, SPRING_CONFIG); } else { zoomScale.value = 1; - translateX.value = 0; - translateY.value = 0; + panTranslateX.value = 0; + panTranslateY.value = 0; offsetX.value = 0; offsetY.value = 0; pinchTranslateX.value = 0; @@ -379,11 +377,11 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr panVelocityY.value = evt.velocityY; if (!isSwiping.value) { - translateX.value += evt.changeX; + panTranslateX.value += evt.changeX; } if (canPanVertically.value || isSwiping.value) { - translateY.value += evt.changeY; + panTranslateY.value += evt.changeY; } }) .onEnd((evt) => { @@ -393,10 +391,10 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr return; } - offsetX.value += translateX.value; - offsetY.value += translateY.value; - translateX.value = 0; - translateY.value = 0; + offsetX.value += panTranslateX.value; + offsetY.value += panTranslateY.value; + panTranslateX.value = 0; + panTranslateY.value = 0; if (isSwiping.value) { const enoughVelocity = Math.abs(evt.velocityY) > 300 && Math.abs(evt.velocityX) < Math.abs(evt.velocityY); @@ -427,7 +425,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr } } - afterPanGesture(); + returnToBoundaries(); panVelocityX.value = 0; panVelocityY.value = 0; @@ -462,8 +460,8 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const adjustFocal = getAdjustedFocal(evt.focalX, evt.focalY); - origin.x.value = adjustFocal.x; - origin.y.value = adjustFocal.y; + pinchOrigin.x.value = adjustFocal.x; + pinchOrigin.y.value = adjustFocal.y; }) .onChange((evt) => { const newZoomScale = pinchScaleOffset.value * evt.scale; @@ -474,8 +472,8 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr } const adjustedFocal = getAdjustedFocal(evt.focalX, evt.focalY); - const newPinchTranslateX = adjustedFocal.x + pinchGestureScale.value * origin.x.value * -1; - const newPinchTranslateY = adjustedFocal.y + pinchGestureScale.value * origin.y.value * -1; + const newPinchTranslateX = adjustedFocal.x + pinchGestureScale.value * pinchOrigin.x.value * -1; + const newPinchTranslateY = adjustedFocal.y + pinchGestureScale.value * pinchOrigin.y.value * -1; if (zoomScale.value >= zoomRange.min && zoomScale.value <= zoomRange.max) { pinchTranslateX.value = newPinchTranslateX; @@ -527,8 +525,8 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr useEffect(() => onPinchGestureChange(isPinchGestureInUse), [isPinchGestureInUse]); const animatedStyles = useAnimatedStyle(() => { - const x = pinchTranslateX.value + pinchBounceTranslateX.value + translateX.value + offsetX.value; - const y = pinchTranslateY.value + pinchBounceTranslateY.value + translateY.value + offsetY.value; + const x = pinchTranslateX.value + pinchBounceTranslateX.value + panTranslateX.value + offsetX.value; + const y = pinchTranslateY.value + pinchBounceTranslateY.value + panTranslateY.value + offsetY.value; if (isSwiping.value) { onSwipe(y); From 4c0d2c571df616d2ddcc31e36411cb7ef5b327e2 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 29 Dec 2023 13:59:43 +0100 Subject: [PATCH 006/107] extract gesture code to hooks --- src/components/MultiGestureCanvas/index.js | 519 +++--------------- .../MultiGestureCanvas/usePanGesture.js | 237 ++++++++ .../MultiGestureCanvas/usePinchGesture.js | 115 ++++ .../MultiGestureCanvas/useTapGestures.js | 127 +++++ src/components/MultiGestureCanvas/utils.ts | 19 + 5 files changed, 581 insertions(+), 436 deletions(-) create mode 100644 src/components/MultiGestureCanvas/usePanGesture.js create mode 100644 src/components/MultiGestureCanvas/usePinchGesture.js create mode 100644 src/components/MultiGestureCanvas/useTapGestures.js create mode 100644 src/components/MultiGestureCanvas/utils.ts diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index 6ff38a15981a..1e8f70c9665f 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -1,42 +1,19 @@ import React, {useContext, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; -import Animated, { - cancelAnimation, - runOnJS, - runOnUI, - useAnimatedReaction, - useAnimatedStyle, - useDerivedValue, - useSharedValue, - useWorkletCallback, - withDecay, - withSpring, -} from 'react-native-reanimated'; +import Animated, {cancelAnimation, runOnJS, runOnUI, useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, useWorkletCallback, withSpring} from 'react-native-reanimated'; import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import getCanvasFitScale from './getCanvasFitScale'; import {defaultZoomRange, multiGestureCanvasDefaultProps, multiGestureCanvasPropTypes} from './propTypes'; +import usePanGesture from './usePanGesture'; +import usePinchGesture from './usePinchGesture'; +import useTapGestures from './useTapGestures'; +import * as MultiGestureCanvasUtils from './utils'; -const DOUBLE_TAP_SCALE = 3; - -const zoomScaleBounceFactors = { - min: 0.7, - max: 1.5, -}; - -const SPRING_CONFIG = { - mass: 1, - stiffness: 1000, - damping: 500, -}; - -function clamp(value, lowerBound, upperBound) { - 'worklet'; - - return Math.min(Math.max(lowerBound, value), upperBound); -} +const SPRING_CONFIG = MultiGestureCanvasUtils.SPRING_CONFIG; +const zoomScaleBounceFactors = MultiGestureCanvasUtils.zoomScaleBounceFactors; function getDeepDefaultProps({contentSize: contentSizeProp = {}, zoomRange: zoomRangeProp = {}}) { const contentSize = { @@ -72,11 +49,6 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr }; const {minScale: minContentScale, maxScale: maxContentScale} = useMemo(() => getCanvasFitScale({canvasSize, contentSize}), [canvasSize, contentSize]); - const scaledWidth = useMemo(() => contentSize.width * minContentScale, [contentSize.width, minContentScale]); - const scaledHeight = useMemo(() => contentSize.height * minContentScale, [contentSize.height, minContentScale]); - - // On double tap zoom to fill, but at least 3x zoom - const doubleTapScale = useMemo(() => Math.max(DOUBLE_TAP_SCALE, maxContentScale / minContentScale), [maxContentScale, minContentScale]); const zoomScale = useSharedValue(1); // Adding together the pinch zoom scale and the initial scale to fit the content into the canvas @@ -84,9 +56,6 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr // and not smaller than needed to fit const totalScale = useDerivedValue(() => zoomScale.value * minContentScale, [minContentScale]); - const zoomScaledContentWidth = useDerivedValue(() => contentSize.width * totalScale.value, [contentSize.width]); - const zoomScaledContentHeight = useDerivedValue(() => contentSize.height * totalScale.value, [contentSize.height]); - // pan and pinch gesture const offsetX = useSharedValue(0); const offsetY = useSharedValue(0); @@ -95,11 +64,6 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const panTranslateX = useSharedValue(0); const panTranslateY = useSharedValue(0); const isSwiping = useSharedValue(false); - // pan velocity to calculate the decay - const panVelocityX = useSharedValue(0); - const panVelocityY = useSharedValue(0); - // disable pan vertically when content is smaller than screen - const canPanVertically = useDerivedValue(() => canvasSize.height < zoomScaledContentHeight.value, [canvasSize.height]); // pinch gesture const pinchTranslateX = useSharedValue(0); @@ -108,170 +72,12 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const pinchBounceTranslateY = useSharedValue(0); // scale in between gestures const pinchScaleOffset = useSharedValue(1); - // origin of the pinch gesture - const pinchOrigin = { - x: useSharedValue(0), - y: useSharedValue(0), - }; - - // calculates bounds of the scaled content - // can we pan left/right/up/down - // can be used to limit gesture or implementing tension effect - const getBounds = useWorkletCallback(() => { - let rightBoundary = 0; - let topBoundary = 0; - - if (canvasSize.width < zoomScaledContentWidth.value) { - rightBoundary = Math.abs(canvasSize.width - zoomScaledContentWidth.value) / 2; - } - - if (canvasSize.height < zoomScaledContentHeight.value) { - topBoundary = Math.abs(zoomScaledContentHeight.value - canvasSize.height) / 2; - } - - const maxVector = {x: rightBoundary, y: topBoundary}; - const minVector = {x: -rightBoundary, y: -topBoundary}; - - const target = { - x: clamp(offsetX.value, minVector.x, maxVector.x), - y: clamp(offsetY.value, minVector.y, maxVector.y), - }; - - const isInBoundaryX = target.x === offsetX.value; - const isInBoundaryY = target.y === offsetY.value; - - return { - target, - isInBoundaryX, - isInBoundaryY, - minVector, - maxVector, - canPanLeft: target.x < maxVector.x, - canPanRight: target.x > minVector.x, - }; - }, [canvasSize.width, canvasSize.height]); - - const returnToBoundaries = useWorkletCallback(() => { - const {target, isInBoundaryX, isInBoundaryY, minVector, maxVector} = getBounds(); - - if (!canPanVertically.value) { - offsetY.value = withSpring(target.y, SPRING_CONFIG); - } - - if (zoomScale.value === 1 && offsetX.value === 0 && offsetY.value === 0 && panTranslateX.value === 0 && panTranslateY.value === 0) { - // we don't need to run any animations - return; - } - - if (zoomScale.value <= 1) { - // just center it - offsetX.value = withSpring(0, SPRING_CONFIG); - offsetY.value = withSpring(0, SPRING_CONFIG); - return; - } - - const deceleration = 0.9915; - - if (isInBoundaryX) { - if (Math.abs(panVelocityX.value) > 0 && zoomScale.value <= zoomRange.max) { - offsetX.value = withDecay({ - velocity: panVelocityX.value, - clamp: [minVector.x, maxVector.x], - deceleration, - rubberBandEffect: false, - }); - } - } else { - offsetX.value = withSpring(target.x, SPRING_CONFIG); - } - - if (isInBoundaryY) { - if ( - Math.abs(panVelocityY.value) > 0 && - zoomScale.value <= zoomRange.max && - // limit vertical pan only when content is smaller than screen - offsetY.value !== minVector.y && - offsetY.value !== maxVector.y - ) { - offsetY.value = withDecay({ - velocity: panVelocityY.value, - clamp: [minVector.y, maxVector.y], - deceleration, - }); - } - } else { - offsetY.value = withSpring(target.y, SPRING_CONFIG, () => { - isSwiping.value = false; - }); - } - }); const stopAnimation = useWorkletCallback(() => { cancelAnimation(offsetX); cancelAnimation(offsetY); }); - const zoomToCoordinates = useWorkletCallback( - (canvasFocalX, canvasFocalY) => { - 'worklet'; - - stopAnimation(); - - const canvasOffsetX = Math.max(0, (canvasSize.width - scaledWidth) / 2); - const canvasOffsetY = Math.max(0, (canvasSize.height - scaledHeight) / 2); - - const contentFocal = { - x: clamp(canvasFocalX - canvasOffsetX, 0, scaledWidth), - y: clamp(canvasFocalY - canvasOffsetY, 0, scaledHeight), - }; - - const canvasCenter = { - x: canvasSize.width / 2, - y: canvasSize.height / 2, - }; - - const originContentCenter = { - x: scaledWidth / 2, - y: scaledHeight / 2, - }; - - const targetContentSize = { - width: scaledWidth * doubleTapScale, - height: scaledHeight * doubleTapScale, - }; - - const targetContentCenter = { - x: targetContentSize.width / 2, - y: targetContentSize.height / 2, - }; - - const currentOrigin = { - x: (targetContentCenter.x - canvasCenter.x) * -1, - y: (targetContentCenter.y - canvasCenter.y) * -1, - }; - - const koef = { - x: (1 / originContentCenter.x) * contentFocal.x - 1, - y: (1 / originContentCenter.y) * contentFocal.y - 1, - }; - - const target = { - x: currentOrigin.x * koef.x, - y: currentOrigin.y * koef.y, - }; - - if (targetContentSize.height < canvasSize.height) { - target.y = 0; - } - - offsetX.value = withSpring(target.x, SPRING_CONFIG); - offsetY.value = withSpring(target.y, SPRING_CONFIG); - zoomScale.value = withSpring(doubleTapScale, SPRING_CONFIG); - pinchScaleOffset.value = doubleTapScale; - }, - [scaledWidth, scaledHeight, canvasSize, doubleTapScale], - ); - const reset = useWorkletCallback((animated) => { pinchScaleOffset.value = 1; @@ -282,235 +88,76 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr offsetY.value = withSpring(0, SPRING_CONFIG); zoomScale.value = withSpring(1, SPRING_CONFIG); } else { + offsetX.value = 0; + offsetY.value = 0; zoomScale.value = 1; panTranslateX.value = 0; panTranslateY.value = 0; - offsetX.value = 0; - offsetY.value = 0; pinchTranslateX.value = 0; pinchTranslateY.value = 0; } }); - const doubleTap = Gesture.Tap() - .numberOfTaps(2) - .maxDelay(150) - .maxDistance(20) - .onEnd((evt) => { - if (zoomScale.value > 1) { - reset(true); - } else { - zoomToCoordinates(evt.x, evt.y); - } - - if (onScaleChanged != null) { - runOnJS(onScaleChanged)(zoomScale.value); - } - }); - const panGestureRef = useRef(Gesture.Pan()); - const singleTap = Gesture.Tap() - .numberOfTaps(1) - .maxDuration(50) - .requireExternalGestureToFail(doubleTap, panGestureRef) - .onBegin(() => { - stopAnimation(); - }) - .onFinalize((evt, success) => { - if (!success || !onTap) { - return; - } - - runOnJS(onTap)(); - }); - - const previousTouch = useSharedValue(null); - - const panGesture = Gesture.Pan() - .manualActivation(true) - .averageTouches(true) - .onTouchesMove((evt, state) => { - if (zoomScale.value > 1) { - state.activate(); - } - - // TODO: Swipe down to close carousel gesture - // this needs fine tuning to work properly - // if (!isScrolling.value && scale.value === 1 && previousTouch.value != null) { - // const velocityX = Math.abs(evt.allTouches[0].x - previousTouch.value.x); - // const velocityY = evt.allTouches[0].y - previousTouch.value.y; - - // // TODO: this needs tuning - // if (Math.abs(velocityY) > velocityX && velocityY > 20) { - // state.activate(); - - // isSwiping.value = true; - // previousTouch.value = null; - - // runOnJS(onSwipeDown)(); - // return; - // } - // } - - if (previousTouch.value == null) { - previousTouch.value = { - x: evt.allTouches[0].x, - y: evt.allTouches[0].y, - }; - } - }) - .simultaneousWithExternalGesture(pagerRef, doubleTap, singleTap) - .onBegin(() => { - stopAnimation(); - }) - .onChange((evt) => { - // since we running both pinch and pan gesture handlers simultaneously - // we need to make sure that we don't pan when we pinch and move fingers - // since we track it as pinch focal gesture - if (evt.numberOfPointers > 1 || isScrolling.value) { - return; - } - - panVelocityX.value = evt.velocityX; - - panVelocityY.value = evt.velocityY; - - if (!isSwiping.value) { - panTranslateX.value += evt.changeX; - } - - if (canPanVertically.value || isSwiping.value) { - panTranslateY.value += evt.changeY; - } - }) - .onEnd((evt) => { - previousTouch.value = null; - - if (isScrolling.value) { - return; - } - - offsetX.value += panTranslateX.value; - offsetY.value += panTranslateY.value; - panTranslateX.value = 0; - panTranslateY.value = 0; - - if (isSwiping.value) { - const enoughVelocity = Math.abs(evt.velocityY) > 300 && Math.abs(evt.velocityX) < Math.abs(evt.velocityY); - const rightDirection = (evt.translationY > 0 && evt.velocityY > 0) || (evt.translationY < 0 && evt.velocityY < 0); - - if (enoughVelocity && rightDirection) { - const maybeInvert = (v) => { - const invert = evt.velocityY < 0; - return invert ? -v : v; - }; - - offsetY.value = withSpring( - maybeInvert(contentSize.height * 2), - { - stiffness: 50, - damping: 30, - mass: 1, - overshootClamping: true, - restDisplacementThreshold: 300, - restSpeedThreshold: 300, - velocity: Math.abs(evt.velocityY) < 1200 ? maybeInvert(1200) : evt.velocityY, - }, - () => { - runOnJS(onSwipeSuccess)(); - }, - ); - return; - } - } - - returnToBoundaries(); + const {singleTap, doubleTap} = useTapGestures({ + canvasSize, + contentSize, + minContentScale, + maxContentScale, + panGestureRef, + offsetX, + offsetY, + pinchScaleOffset, + zoomScale, + reset, + stopAnimation, + onScaleChanged, + onTap, + }); - panVelocityX.value = 0; - panVelocityY.value = 0; - }) - .withRef(panGestureRef); + const panGesture = usePanGesture({ + canvasSize, + contentSize, + panGestureRef, + pagerRef, + singleTap, + doubleTap, + zoomScale, + zoomRange, + totalScale, + offsetX, + offsetY, + panTranslateX, + panTranslateY, + isSwiping, + isScrolling, + onSwipeSuccess, + stopAnimation, + }); - const getAdjustedFocal = useWorkletCallback( - (focalX, focalY) => ({ - x: focalX - (canvasSize.width / 2 + offsetX.value), - y: focalY - (canvasSize.height / 2 + offsetY.value), - }), - [canvasSize.width, canvasSize.height], - ); + const pinchGesture = usePinchGesture({ + canvasSize, + contentSize, + panGestureRef, + pagerRef, + singleTap, + doubleTap, + zoomScale, + zoomRange, + totalScale, + offsetX, + offsetY, + panTranslateX, + panTranslateY, + isSwiping, + isScrolling, + onSwipeSuccess, + stopAnimation, + }); - // used to store event scale value when we limit scale - const pinchGestureScale = useSharedValue(1); + // Triggers "onPinchGestureChange" callback when pinch scale changes const pinchGestureRunning = useSharedValue(false); - const pinchGesture = Gesture.Pinch() - .onTouchesDown((evt, state) => { - // we don't want to activate pinch gesture when we are scrolling pager - if (!isScrolling.value) { - return; - } - - state.fail(); - }) - .simultaneousWithExternalGesture(panGesture, doubleTap) - .onStart((evt) => { - pinchGestureRunning.value = true; - - stopAnimation(); - - const adjustFocal = getAdjustedFocal(evt.focalX, evt.focalY); - - pinchOrigin.x.value = adjustFocal.x; - pinchOrigin.y.value = adjustFocal.y; - }) - .onChange((evt) => { - const newZoomScale = pinchScaleOffset.value * evt.scale; - - if (zoomScale.value >= zoomRange.min * zoomScaleBounceFactors.min && zoomScale.value <= zoomRange.max * zoomScaleBounceFactors.max) { - zoomScale.value = newZoomScale; - pinchGestureScale.value = evt.scale; - } - - const adjustedFocal = getAdjustedFocal(evt.focalX, evt.focalY); - const newPinchTranslateX = adjustedFocal.x + pinchGestureScale.value * pinchOrigin.x.value * -1; - const newPinchTranslateY = adjustedFocal.y + pinchGestureScale.value * pinchOrigin.y.value * -1; - - if (zoomScale.value >= zoomRange.min && zoomScale.value <= zoomRange.max) { - pinchTranslateX.value = newPinchTranslateX; - pinchTranslateY.value = newPinchTranslateY; - } else { - pinchBounceTranslateX.value = newPinchTranslateX - pinchTranslateX.value; - pinchBounceTranslateY.value = newPinchTranslateY - pinchTranslateY.value; - } - }) - .onEnd(() => { - offsetX.value += pinchTranslateX.value; - offsetY.value += pinchTranslateY.value; - pinchTranslateX.value = 0; - pinchTranslateY.value = 0; - pinchScaleOffset.value = zoomScale.value; - pinchGestureScale.value = 1; - - if (pinchScaleOffset.value < zoomRange.min) { - pinchScaleOffset.value = zoomRange.min; - zoomScale.value = withSpring(zoomRange.min, SPRING_CONFIG); - } else if (pinchScaleOffset.value > zoomRange.max) { - pinchScaleOffset.value = zoomRange.max; - zoomScale.value = withSpring(zoomRange.max, SPRING_CONFIG); - } - - if (pinchBounceTranslateX.value !== 0 || pinchBounceTranslateY.value !== 0) { - pinchBounceTranslateX.value = withSpring(0, SPRING_CONFIG); - pinchBounceTranslateY.value = withSpring(0, SPRING_CONFIG); - } - - pinchGestureRunning.value = false; - - if (onScaleChanged != null) { - runOnJS(onScaleChanged)(zoomScale.value); - } - }); - const [isPinchGestureInUse, setIsPinchGestureInUse] = useState(false); useAnimatedReaction( () => [zoomScale.value, pinchGestureRunning.value], @@ -524,6 +171,26 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => onPinchGestureChange(isPinchGestureInUse), [isPinchGestureInUse]); + // reacts to scale change and enables/disables pager scroll + useAnimatedReaction( + () => zoomScale.value, + () => { + shouldPagerScroll.value = zoomScale.value === 1; + }, + ); + + const mounted = useRef(false); + useEffect(() => { + if (!mounted.current) { + mounted.current = true; + return; + } + + if (!isActive) { + runOnUI(reset)(false); + } + }, [isActive, mounted, reset]); + const animatedStyles = useAnimatedStyle(() => { const x = pinchTranslateX.value + pinchBounceTranslateX.value + panTranslateX.value + offsetX.value; const y = pinchTranslateY.value + pinchBounceTranslateY.value + panTranslateY.value + offsetY.value; @@ -545,26 +212,6 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr }; }); - // reacts to scale change and enables/disables pager scroll - useAnimatedReaction( - () => zoomScale.value, - () => { - shouldPagerScroll.value = zoomScale.value === 1; - }, - ); - - const mounted = useRef(false); - useEffect(() => { - if (!mounted.current) { - mounted.current = true; - return; - } - - if (!isActive) { - runOnUI(reset)(false); - } - }, [isActive, mounted, reset]); - return ( { + const zoomScaledContentWidth = useDerivedValue(() => contentSize.width * totalScale.value, [contentSize.width]); + const zoomScaledContentHeight = useDerivedValue(() => contentSize.height * totalScale.value, [contentSize.height]); + + // pan velocity to calculate the decay + const panVelocityX = useSharedValue(0); + const panVelocityY = useSharedValue(0); + // disable pan vertically when content is smaller than screen + const canPanVertically = useDerivedValue(() => canvasSize.height < zoomScaledContentHeight.value, [canvasSize.height]); + + const previousTouch = useSharedValue(null); + + // calculates bounds of the scaled content + // can we pan left/right/up/down + // can be used to limit gesture or implementing tension effect + const getBounds = useWorkletCallback(() => { + let rightBoundary = 0; + let topBoundary = 0; + + if (canvasSize.width < zoomScaledContentWidth.value) { + rightBoundary = Math.abs(canvasSize.width - zoomScaledContentWidth.value) / 2; + } + + if (canvasSize.height < zoomScaledContentHeight.value) { + topBoundary = Math.abs(zoomScaledContentHeight.value - canvasSize.height) / 2; + } + + const maxVector = {x: rightBoundary, y: topBoundary}; + const minVector = {x: -rightBoundary, y: -topBoundary}; + + const target = { + x: clamp(offsetX.value, minVector.x, maxVector.x), + y: clamp(offsetY.value, minVector.y, maxVector.y), + }; + + const isInBoundaryX = target.x === offsetX.value; + const isInBoundaryY = target.y === offsetY.value; + + return { + target, + isInBoundaryX, + isInBoundaryY, + minVector, + maxVector, + canPanLeft: target.x < maxVector.x, + canPanRight: target.x > minVector.x, + }; + }, [canvasSize.width, canvasSize.height]); + + const returnToBoundaries = useWorkletCallback(() => { + const {target, isInBoundaryX, isInBoundaryY, minVector, maxVector} = getBounds(); + + if (zoomScale.value === zoomRange.min && offsetX.value === 0 && offsetY.value === 0 && panTranslateX.value === 0 && panTranslateY.value === 0) { + // we don't need to run any animations + return; + } + + if (zoomScale.value <= zoomRange.min) { + // just center it + offsetX.value = withSpring(0, SPRING_CONFIG); + offsetY.value = withSpring(0, SPRING_CONFIG); + return; + } + + if (isInBoundaryX) { + if (Math.abs(panVelocityX.value) > 0 && zoomScale.value <= zoomRange.max) { + offsetX.value = withDecay({ + velocity: panVelocityX.value, + clamp: [minVector.x, maxVector.x], + deceleration: PAN_DECAY_DECELARATION, + rubberBandEffect: false, + }); + } + } else { + offsetX.value = withSpring(target.x, SPRING_CONFIG); + } + + if (!canPanVertically.value) { + offsetY.value = withSpring(target.y, SPRING_CONFIG); + } else if (isInBoundaryY) { + if ( + Math.abs(panVelocityY.value) > 0 && + zoomScale.value <= zoomRange.max && + // limit vertical pan only when content is smaller than screen + offsetY.value !== minVector.y && + offsetY.value !== maxVector.y + ) { + offsetY.value = withDecay({ + velocity: panVelocityY.value, + clamp: [minVector.y, maxVector.y], + deceleration: PAN_DECAY_DECELARATION, + }); + } + } else { + offsetY.value = withSpring(target.y, SPRING_CONFIG, () => { + isSwiping.value = false; + }); + } + }); + + const panGesture = Gesture.Pan() + .manualActivation(true) + .averageTouches(true) + .onTouchesMove((evt, state) => { + if (zoomScale.value > 1) { + state.activate(); + } + + // TODO: Swipe down to close carousel gesture + // this needs fine tuning to work properly + // if (!isScrolling.value && scale.value === 1 && previousTouch.value != null) { + // const velocityX = Math.abs(evt.allTouches[0].x - previousTouch.value.x); + // const velocityY = evt.allTouches[0].y - previousTouch.value.y; + + // // TODO: this needs tuning + // if (Math.abs(velocityY) > velocityX && velocityY > 20) { + // state.activate(); + + // isSwiping.value = true; + // previousTouch.value = null; + + // runOnJS(onSwipeDown)(); + // return; + // } + // } + + if (previousTouch.value == null) { + previousTouch.value = { + x: evt.allTouches[0].x, + y: evt.allTouches[0].y, + }; + } + }) + .simultaneousWithExternalGesture(pagerRef, singleTap, doubleTap) + .onBegin(() => { + stopAnimation(); + }) + .onChange((evt) => { + // since we're running both pinch and pan gesture handlers simultaneously + // we need to make sure that we don't pan when we pinch and move fingers + // since we track it as pinch focal gesture + if (evt.numberOfPointers > 1 || isScrolling.value) { + return; + } + + panVelocityX.value = evt.velocityX; + + panVelocityY.value = evt.velocityY; + + if (!isSwiping.value) { + panTranslateX.value += evt.changeX; + } + + if (canPanVertically.value || isSwiping.value) { + panTranslateY.value += evt.changeY; + } + }) + .onEnd((evt) => { + previousTouch.value = null; + + if (isScrolling.value) { + return; + } + + offsetX.value += panTranslateX.value; + offsetY.value += panTranslateY.value; + panTranslateX.value = 0; + panTranslateY.value = 0; + + if (isSwiping.value) { + const enoughVelocity = Math.abs(evt.velocityY) > 300 && Math.abs(evt.velocityX) < Math.abs(evt.velocityY); + const rightDirection = (evt.translationY > 0 && evt.velocityY > 0) || (evt.translationY < 0 && evt.velocityY < 0); + + if (enoughVelocity && rightDirection) { + const maybeInvert = (v) => { + const invert = evt.velocityY < 0; + return invert ? -v : v; + }; + + offsetY.value = withSpring( + maybeInvert(contentSize.height * 2), + { + stiffness: 50, + damping: 30, + mass: 1, + overshootClamping: true, + restDisplacementThreshold: 300, + restSpeedThreshold: 300, + velocity: Math.abs(evt.velocityY) < 1200 ? maybeInvert(1200) : evt.velocityY, + }, + () => { + runOnJS(onSwipeSuccess)(); + }, + ); + return; + } + } + + returnToBoundaries(); + + panVelocityX.value = 0; + panVelocityY.value = 0; + }) + .withRef(panGestureRef); + + return panGesture; +}; + +export default usePanGesture; diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.js new file mode 100644 index 000000000000..9a7c8b9cf7b7 --- /dev/null +++ b/src/components/MultiGestureCanvas/usePinchGesture.js @@ -0,0 +1,115 @@ +/* eslint-disable no-param-reassign */ +import {Gesture} from 'react-native-gesture-handler'; +import {runOnJS, useSharedValue, useWorkletCallback, withSpring} from 'react-native-reanimated'; +import * as MultiGestureCanvasUtils from './utils'; + +const SPRING_CONFIG = MultiGestureCanvasUtils.SPRING_CONFIG; +const zoomScaleBounceFactors = MultiGestureCanvasUtils.zoomScaleBounceFactors; + +const usePinchGesture = ({ + canvasSize, + singleTap, + doubleTap, + panGesture, + zoomScale, + zoomRange, + offsetX, + offsetY, + pinchTranslateX, + pinchTranslateY, + pinchBounceTranslateX, + pinchBounceTranslateY, + pinchScaleOffset, + pinchGestureRunning, + isScrolling, + stopAnimation, + onScaleChanged, +}) => { + // used to store event scale value when we limit scale + const pinchGestureScale = useSharedValue(1); + + // origin of the pinch gesture + const pinchOrigin = { + x: useSharedValue(0), + y: useSharedValue(0), + }; + + const getAdjustedFocal = useWorkletCallback( + (focalX, focalY) => ({ + x: focalX - (canvasSize.width / 2 + offsetX.value), + y: focalY - (canvasSize.height / 2 + offsetY.value), + }), + [canvasSize.width, canvasSize.height], + ); + const pinchGesture = Gesture.Pinch() + .onTouchesDown((evt, state) => { + // we don't want to activate pinch gesture when we are scrolling pager + if (!isScrolling.value) { + return; + } + + state.fail(); + }) + .simultaneousWithExternalGesture(panGesture, singleTap, doubleTap) + .onStart((evt) => { + pinchGestureRunning.value = true; + + stopAnimation(); + + const adjustFocal = getAdjustedFocal(evt.focalX, evt.focalY); + + pinchOrigin.x.value = adjustFocal.x; + pinchOrigin.y.value = adjustFocal.y; + }) + .onChange((evt) => { + const newZoomScale = pinchScaleOffset.value * evt.scale; + + if (zoomScale.value >= zoomRange.min * zoomScaleBounceFactors.min && zoomScale.value <= zoomRange.max * zoomScaleBounceFactors.max) { + zoomScale.value = newZoomScale; + pinchGestureScale.value = evt.scale; + } + + const adjustedFocal = getAdjustedFocal(evt.focalX, evt.focalY); + const newPinchTranslateX = adjustedFocal.x + pinchGestureScale.value * pinchOrigin.x.value * -1; + const newPinchTranslateY = adjustedFocal.y + pinchGestureScale.value * pinchOrigin.y.value * -1; + + if (zoomScale.value >= zoomRange.min && zoomScale.value <= zoomRange.max) { + pinchTranslateX.value = newPinchTranslateX; + pinchTranslateY.value = newPinchTranslateY; + } else { + pinchBounceTranslateX.value = newPinchTranslateX - pinchTranslateX.value; + pinchBounceTranslateY.value = newPinchTranslateY - pinchTranslateY.value; + } + }) + .onEnd(() => { + offsetX.value += pinchTranslateX.value; + offsetY.value += pinchTranslateY.value; + pinchTranslateX.value = 0; + pinchTranslateY.value = 0; + pinchScaleOffset.value = zoomScale.value; + pinchGestureScale.value = 1; + + if (pinchScaleOffset.value < zoomRange.min) { + pinchScaleOffset.value = zoomRange.min; + zoomScale.value = withSpring(zoomRange.min, SPRING_CONFIG); + } else if (pinchScaleOffset.value > zoomRange.max) { + pinchScaleOffset.value = zoomRange.max; + zoomScale.value = withSpring(zoomRange.max, SPRING_CONFIG); + } + + if (pinchBounceTranslateX.value !== 0 || pinchBounceTranslateY.value !== 0) { + pinchBounceTranslateX.value = withSpring(0, SPRING_CONFIG); + pinchBounceTranslateY.value = withSpring(0, SPRING_CONFIG); + } + + pinchGestureRunning.value = false; + + if (onScaleChanged != null) { + runOnJS(onScaleChanged)(zoomScale.value); + } + }); + + return pinchGesture; +}; + +export default usePinchGesture; diff --git a/src/components/MultiGestureCanvas/useTapGestures.js b/src/components/MultiGestureCanvas/useTapGestures.js new file mode 100644 index 000000000000..0af5076618a4 --- /dev/null +++ b/src/components/MultiGestureCanvas/useTapGestures.js @@ -0,0 +1,127 @@ +/* eslint-disable no-param-reassign */ +import {useMemo} from 'react'; +import {Gesture} from 'react-native-gesture-handler'; +import {runOnJS, useWorkletCallback, withSpring} from 'react-native-reanimated'; +import * as MultiGestureCanvasUtils from './utils'; + +const clamp = MultiGestureCanvasUtils.clamp; +const DOUBLE_TAP_SCALE = MultiGestureCanvasUtils.DOUBLE_TAP_SCALE; +const SPRING_CONFIG = MultiGestureCanvasUtils.SPRING_CONFIG; + +const useTapGestures = ({ + canvasSize, + contentSize, + minContentScale, + maxContentScale, + panGestureRef, + offsetX, + offsetY, + pinchScaleOffset, + zoomScale, + reset, + stopAnimation, + onScaleChanged, + onTap, +}) => { + const scaledWidth = useMemo(() => contentSize.width * minContentScale, [contentSize.width, minContentScale]); + const scaledHeight = useMemo(() => contentSize.height * minContentScale, [contentSize.height, minContentScale]); + + // On double tap zoom to fill, but at least 3x zoom + const doubleTapScale = useMemo(() => Math.max(DOUBLE_TAP_SCALE, maxContentScale / minContentScale), [maxContentScale, minContentScale]); + + const zoomToCoordinates = useWorkletCallback( + (canvasFocalX, canvasFocalY) => { + 'worklet'; + + stopAnimation(); + + const canvasOffsetX = Math.max(0, (canvasSize.width - scaledWidth) / 2); + const canvasOffsetY = Math.max(0, (canvasSize.height - scaledHeight) / 2); + + const contentFocal = { + x: clamp(canvasFocalX - canvasOffsetX, 0, scaledWidth), + y: clamp(canvasFocalY - canvasOffsetY, 0, scaledHeight), + }; + + const canvasCenter = { + x: canvasSize.width / 2, + y: canvasSize.height / 2, + }; + + const originContentCenter = { + x: scaledWidth / 2, + y: scaledHeight / 2, + }; + + const targetContentSize = { + width: scaledWidth * doubleTapScale, + height: scaledHeight * doubleTapScale, + }; + + const targetContentCenter = { + x: targetContentSize.width / 2, + y: targetContentSize.height / 2, + }; + + const currentOrigin = { + x: (targetContentCenter.x - canvasCenter.x) * -1, + y: (targetContentCenter.y - canvasCenter.y) * -1, + }; + + const koef = { + x: (1 / originContentCenter.x) * contentFocal.x - 1, + y: (1 / originContentCenter.y) * contentFocal.y - 1, + }; + + const target = { + x: currentOrigin.x * koef.x, + y: currentOrigin.y * koef.y, + }; + + if (targetContentSize.height < canvasSize.height) { + target.y = 0; + } + + offsetX.value = withSpring(target.x, SPRING_CONFIG); + offsetY.value = withSpring(target.y, SPRING_CONFIG); + zoomScale.value = withSpring(doubleTapScale, SPRING_CONFIG); + pinchScaleOffset.value = doubleTapScale; + }, + [scaledWidth, scaledHeight, canvasSize, doubleTapScale], + ); + + const doubleTap = Gesture.Tap() + .numberOfTaps(2) + .maxDelay(150) + .maxDistance(20) + .onEnd((evt) => { + if (zoomScale.value > 1) { + reset(true); + } else { + zoomToCoordinates(evt.x, evt.y); + } + + if (onScaleChanged != null) { + runOnJS(onScaleChanged)(zoomScale.value); + } + }); + + const singleTap = Gesture.Tap() + .numberOfTaps(1) + .maxDuration(50) + .requireExternalGestureToFail(doubleTap, panGestureRef) + .onBegin(() => { + stopAnimation(); + }) + .onFinalize((_evt, success) => { + if (!success || !onTap) { + return; + } + + runOnJS(onTap)(); + }); + + return {singleTap, doubleTap}; +}; + +export default useTapGestures; diff --git a/src/components/MultiGestureCanvas/utils.ts b/src/components/MultiGestureCanvas/utils.ts new file mode 100644 index 000000000000..c28f02ed0f39 --- /dev/null +++ b/src/components/MultiGestureCanvas/utils.ts @@ -0,0 +1,19 @@ +const DOUBLE_TAP_SCALE = 3; + +const SPRING_CONFIG = { + mass: 1, + stiffness: 1000, + damping: 500, +}; + +const zoomScaleBounceFactors = { + min: 0.7, + max: 1.5, +}; +function clamp(value: number, lowerBound: number, upperBound: number) { + 'worklet'; + + return Math.min(Math.max(lowerBound, value), upperBound); +} + +export {clamp, DOUBLE_TAP_SCALE, SPRING_CONFIG, zoomScaleBounceFactors}; From 2632f75d6bcc0f336649054efa4052b453e099a1 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 29 Dec 2023 14:07:43 +0100 Subject: [PATCH 007/107] further simplify --- src/components/MultiGestureCanvas/index.js | 40 ++++++------------- .../MultiGestureCanvas/usePinchGesture.js | 21 ++++++++-- 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index 1e8f70c9665f..70713bf3d80c 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -1,7 +1,7 @@ -import React, {useContext, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useContext, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; -import Animated, {cancelAnimation, runOnJS, runOnUI, useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, useWorkletCallback, withSpring} from 'react-native-reanimated'; +import Animated, {cancelAnimation, runOnUI, useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, useWorkletCallback, withSpring} from 'react-native-reanimated'; import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -56,7 +56,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr // and not smaller than needed to fit const totalScale = useDerivedValue(() => zoomScale.value * minContentScale, [minContentScale]); - // pan and pinch gesture + // stored offset of the canvas (used for panning and pinching) const offsetX = useSharedValue(0); const offsetY = useSharedValue(0); @@ -64,6 +64,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const panTranslateX = useSharedValue(0); const panTranslateY = useSharedValue(0); const isSwiping = useSharedValue(false); + const panGestureRef = useRef(Gesture.Pan()); // pinch gesture const pinchTranslateX = useSharedValue(0); @@ -98,8 +99,6 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr } }); - const panGestureRef = useRef(Gesture.Pan()); - const {singleTap, doubleTap} = useTapGestures({ canvasSize, contentSize, @@ -138,39 +137,24 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const pinchGesture = usePinchGesture({ canvasSize, - contentSize, - panGestureRef, - pagerRef, singleTap, doubleTap, + panGesture, zoomScale, zoomRange, - totalScale, offsetX, offsetY, - panTranslateX, - panTranslateY, - isSwiping, + pinchTranslateX, + pinchTranslateY, + pinchBounceTranslateX, + pinchBounceTranslateY, + pinchScaleOffset, isScrolling, - onSwipeSuccess, stopAnimation, + onScaleChanged, + onPinchGestureChange, }); - // Triggers "onPinchGestureChange" callback when pinch scale changes - const pinchGestureRunning = useSharedValue(false); - const [isPinchGestureInUse, setIsPinchGestureInUse] = useState(false); - useAnimatedReaction( - () => [zoomScale.value, pinchGestureRunning.value], - ([zoom, running]) => { - const newIsPinchGestureInUse = zoom !== 1 || running; - if (isPinchGestureInUse !== newIsPinchGestureInUse) { - runOnJS(setIsPinchGestureInUse)(newIsPinchGestureInUse); - } - }, - ); - // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => onPinchGestureChange(isPinchGestureInUse), [isPinchGestureInUse]); - // reacts to scale change and enables/disables pager scroll useAnimatedReaction( () => zoomScale.value, diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.js index 9a7c8b9cf7b7..544baa1d045e 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.js +++ b/src/components/MultiGestureCanvas/usePinchGesture.js @@ -1,6 +1,7 @@ /* eslint-disable no-param-reassign */ +import {useEffect, useState} from 'react'; import {Gesture} from 'react-native-gesture-handler'; -import {runOnJS, useSharedValue, useWorkletCallback, withSpring} from 'react-native-reanimated'; +import {runOnJS, useAnimatedReaction, useSharedValue, useWorkletCallback, withSpring} from 'react-native-reanimated'; import * as MultiGestureCanvasUtils from './utils'; const SPRING_CONFIG = MultiGestureCanvasUtils.SPRING_CONFIG; @@ -20,14 +21,14 @@ const usePinchGesture = ({ pinchBounceTranslateX, pinchBounceTranslateY, pinchScaleOffset, - pinchGestureRunning, isScrolling, stopAnimation, onScaleChanged, + onPinchGestureChange, }) => { // used to store event scale value when we limit scale const pinchGestureScale = useSharedValue(1); - + const pinchGestureRunning = useSharedValue(false); // origin of the pinch gesture const pinchOrigin = { x: useSharedValue(0), @@ -109,6 +110,20 @@ const usePinchGesture = ({ } }); + // Triggers "onPinchGestureChange" callback when pinch scale changes + const [isPinchGestureInUse, setIsPinchGestureInUse] = useState(false); + useAnimatedReaction( + () => [zoomScale.value, pinchGestureRunning.value], + ([zoom, running]) => { + const newIsPinchGestureInUse = zoom !== 1 || running; + if (isPinchGestureInUse !== newIsPinchGestureInUse) { + runOnJS(setIsPinchGestureInUse)(newIsPinchGestureInUse); + } + }, + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(() => onPinchGestureChange(isPinchGestureInUse), [isPinchGestureInUse]); + return pinchGesture; }; From 141387187c0c429577f3787d07705763d693ce41 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 29 Dec 2023 14:26:00 +0100 Subject: [PATCH 008/107] improve styles in Lightbox --- src/components/Lightbox.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/components/Lightbox.js b/src/components/Lightbox.js index 06f8ee4cfeb6..0b09ed1e745a 100644 --- a/src/components/Lightbox.js +++ b/src/components/Lightbox.js @@ -168,7 +168,7 @@ function Lightbox({isAuthTokenRequired, source, onScaleChanged, onPress, onError {isContainerLoaded && ( <> {isLightboxVisible && ( - + setImageLoaded(true)} onLoad={(e) => { - const width = (e.nativeEvent?.width || 0) / PixelRatio.get(); - const height = (e.nativeEvent?.height || 0) / PixelRatio.get(); + const width = (e.nativeEvent?.width || 0) * PixelRatio.get(); + const height = (e.nativeEvent?.height || 0) * PixelRatio.get(); setImageDimensions({...imageDimensions, lightboxSize: {width, height}}); }} /> @@ -194,10 +194,7 @@ function Lightbox({isAuthTokenRequired, source, onScaleChanged, onPress, onError {/* Keep rendering the image without gestures as fallback if the carousel item is not active and while the lightbox is loading the image */} {isFallbackVisible && ( - + setFallbackLoaded(true)} onLoad={(e) => { - const width = e.nativeEvent?.width || 0; - const height = e.nativeEvent?.height || 0; + const width = (e.nativeEvent?.width || 0) * PixelRatio.get(); + const height = (e.nativeEvent?.height || 0) * PixelRatio.get(); if (imageDimensions?.lightboxSize != null) { return; From dae44fca62a988ebafa34489349c19dc6bdba624 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 29 Dec 2023 18:27:25 +0100 Subject: [PATCH 009/107] rename "isScrolling" prop --- .../AttachmentCarousel/Pager/index.js | 10 ++++---- src/components/MultiGestureCanvas/index.js | 14 +++++------ .../MultiGestureCanvas/usePanGesture.js | 21 +++++++++-------- .../MultiGestureCanvas/usePinchGesture.js | 23 ++++++++++++------- 4 files changed, 39 insertions(+), 29 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.js b/src/components/Attachments/AttachmentCarousel/Pager/index.js index 553e963a3461..699e2fc812cc 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.js +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.js @@ -69,7 +69,7 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte const shouldPagerScroll = useSharedValue(true); const pagerRef = useRef(null); - const isScrolling = useSharedValue(false); + const isSwipingHorizontally = useSharedValue(false); const activeIndex = useSharedValue(initialIndex); const pageScrollHandler = usePageScrollHandler( @@ -78,7 +78,7 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte 'worklet'; activeIndex.value = e.position; - isScrolling.value = e.offset !== 0; + isSwipingHorizontally.value = e.offset !== 0; }, }, [], @@ -94,7 +94,7 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte // we use reanimated for this since onPageSelected is called // in the middle of the pager animation useAnimatedReaction( - () => isScrolling.value, + () => isSwipingHorizontally.value, (stillScrolling) => { if (stillScrolling) { return; @@ -118,7 +118,7 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte const contextValue = useMemo( () => ({ - isScrolling, + isSwipingHorizontally, pagerRef, shouldPagerScroll, onPinchGestureChange, @@ -127,7 +127,7 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte onSwipeSuccess, onSwipeDown, }), - [isScrolling, pagerRef, shouldPagerScroll, onPinchGestureChange, onTap, onSwipe, onSwipeSuccess, onSwipeDown], + [isSwipingHorizontally, pagerRef, shouldPagerScroll, onPinchGestureChange, onTap, onSwipe, onSwipeSuccess, onSwipeDown], ); return ( diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index 70713bf3d80c..671426ba66e7 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -37,14 +37,14 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); const pagerRefFallback = useRef(null); - const {onTap, onSwipe, onSwipeSuccess, pagerRef, shouldPagerScroll, isScrolling, onPinchGestureChange} = attachmentCarouselPagerContext || { + const {onTap, onSwipe, onSwipeSuccess, pagerRef, shouldPagerScroll, isSwipingHorizontally, onPinchGestureChange} = attachmentCarouselPagerContext || { onTap: () => undefined, onSwipe: () => undefined, onSwipeSuccess: () => undefined, onPinchGestureChange: () => undefined, pagerRef: pagerRefFallback, shouldPagerScroll: false, - isScrolling: false, + isSwipingHorizontally: false, ...props, }; @@ -63,7 +63,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr // pan gesture const panTranslateX = useSharedValue(0); const panTranslateY = useSharedValue(0); - const isSwiping = useSharedValue(false); + const isSwipingVertically = useSharedValue(false); const panGestureRef = useRef(Gesture.Pan()); // pinch gesture @@ -129,8 +129,8 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr offsetY, panTranslateX, panTranslateY, - isSwiping, - isScrolling, + isSwipingHorizontally, + isSwipingVertically, onSwipeSuccess, stopAnimation, }); @@ -149,7 +149,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr pinchBounceTranslateX, pinchBounceTranslateY, pinchScaleOffset, - isScrolling, + isSwipingHorizontally, stopAnimation, onScaleChanged, onPinchGestureChange, @@ -179,7 +179,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const x = pinchTranslateX.value + pinchBounceTranslateX.value + panTranslateX.value + offsetX.value; const y = pinchTranslateY.value + pinchBounceTranslateY.value + panTranslateY.value + offsetY.value; - if (isSwiping.value) { + if (isSwipingVertically.value) { onSwipe(y); } diff --git a/src/components/MultiGestureCanvas/usePanGesture.js b/src/components/MultiGestureCanvas/usePanGesture.js index ddc887bcb3f4..c81af12ebb62 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.js +++ b/src/components/MultiGestureCanvas/usePanGesture.js @@ -22,8 +22,8 @@ const usePanGesture = ({ offsetY, panTranslateX, panTranslateY, - isSwiping, - isScrolling, + isSwipingVertically, + isSwipingHorizontally, onSwipeSuccess, stopAnimation, }) => { @@ -121,7 +121,7 @@ const usePanGesture = ({ } } else { offsetY.value = withSpring(target.y, SPRING_CONFIG, () => { - isSwiping.value = false; + isSwipingVertically.value = false; }); } }); @@ -136,7 +136,7 @@ const usePanGesture = ({ // TODO: Swipe down to close carousel gesture // this needs fine tuning to work properly - // if (!isScrolling.value && scale.value === 1 && previousTouch.value != null) { + // if (!isSwipingHorizontally.value && scale.value === 1 && previousTouch.value != null) { // const velocityX = Math.abs(evt.allTouches[0].x - previousTouch.value.x); // const velocityY = evt.allTouches[0].y - previousTouch.value.y; @@ -167,7 +167,7 @@ const usePanGesture = ({ // since we're running both pinch and pan gesture handlers simultaneously // we need to make sure that we don't pan when we pinch and move fingers // since we track it as pinch focal gesture - if (evt.numberOfPointers > 1 || isScrolling.value) { + if (evt.numberOfPointers > 1 || isSwipingHorizontally.value) { return; } @@ -175,27 +175,30 @@ const usePanGesture = ({ panVelocityY.value = evt.velocityY; - if (!isSwiping.value) { + if (!isSwipingVertically.value) { panTranslateX.value += evt.changeX; } - if (canPanVertically.value || isSwiping.value) { + if (canPanVertically.value || isSwipingVertically.value) { panTranslateY.value += evt.changeY; } }) .onEnd((evt) => { previousTouch.value = null; - if (isScrolling.value) { + // If we are swiping, we don't want to return to boundaries + if (isSwipingHorizontally.value) { return; } + // add pan translation to total offset offsetX.value += panTranslateX.value; offsetY.value += panTranslateY.value; + // reset pan gesture variables panTranslateX.value = 0; panTranslateY.value = 0; - if (isSwiping.value) { + if (isSwipingVertically.value) { const enoughVelocity = Math.abs(evt.velocityY) > 300 && Math.abs(evt.velocityX) < Math.abs(evt.velocityY); const rightDirection = (evt.translationY > 0 && evt.velocityY > 0) || (evt.translationY < 0 && evt.velocityY < 0); diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.js index 544baa1d045e..4dbbdf53f3f0 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.js +++ b/src/components/MultiGestureCanvas/usePinchGesture.js @@ -21,7 +21,7 @@ const usePinchGesture = ({ pinchBounceTranslateX, pinchBounceTranslateY, pinchScaleOffset, - isScrolling, + isSwipingHorizontally, stopAnimation, onScaleChanged, onPinchGestureChange, @@ -45,7 +45,7 @@ const usePinchGesture = ({ const pinchGesture = Gesture.Pinch() .onTouchesDown((evt, state) => { // we don't want to activate pinch gesture when we are scrolling pager - if (!isScrolling.value) { + if (!isSwipingHorizontally.value) { return; } @@ -57,19 +57,21 @@ const usePinchGesture = ({ stopAnimation(); - const adjustFocal = getAdjustedFocal(evt.focalX, evt.focalY); + const adjustedFocal = getAdjustedFocal(evt.focalX, evt.focalY); - pinchOrigin.x.value = adjustFocal.x; - pinchOrigin.y.value = adjustFocal.y; + pinchOrigin.x.value = adjustedFocal.x; + pinchOrigin.y.value = adjustedFocal.y; }) .onChange((evt) => { const newZoomScale = pinchScaleOffset.value * evt.scale; + // limit zoom scale to zoom range and bounce if we go out of range if (zoomScale.value >= zoomRange.min * zoomScaleBounceFactors.min && zoomScale.value <= zoomRange.max * zoomScaleBounceFactors.max) { zoomScale.value = newZoomScale; pinchGestureScale.value = evt.scale; } + // calculate new pinch translation const adjustedFocal = getAdjustedFocal(evt.focalX, evt.focalY); const newPinchTranslateX = adjustedFocal.x + pinchGestureScale.value * pinchOrigin.x.value * -1; const newPinchTranslateY = adjustedFocal.y + pinchGestureScale.value * pinchOrigin.y.value * -1; @@ -78,24 +80,29 @@ const usePinchGesture = ({ pinchTranslateX.value = newPinchTranslateX; pinchTranslateY.value = newPinchTranslateY; } else { + // Store x and y translation that is produced while bouncing to separate variables + // so that we can revert the bounce once pinch gesture is released pinchBounceTranslateX.value = newPinchTranslateX - pinchTranslateX.value; pinchBounceTranslateY.value = newPinchTranslateY - pinchTranslateY.value; } }) .onEnd(() => { + // Add pinch translation to total offset offsetX.value += pinchTranslateX.value; offsetY.value += pinchTranslateY.value; + // Reset pinch gesture variables pinchTranslateX.value = 0; pinchTranslateY.value = 0; - pinchScaleOffset.value = zoomScale.value; pinchGestureScale.value = 1; - if (pinchScaleOffset.value < zoomRange.min) { + if (zoomScale.value < zoomRange.min) { pinchScaleOffset.value = zoomRange.min; zoomScale.value = withSpring(zoomRange.min, SPRING_CONFIG); - } else if (pinchScaleOffset.value > zoomRange.max) { + } else if (zoomScale.value > zoomRange.max) { pinchScaleOffset.value = zoomRange.max; zoomScale.value = withSpring(zoomRange.max, SPRING_CONFIG); + } else { + pinchScaleOffset.value = zoomScale.value; } if (pinchBounceTranslateX.value !== 0 || pinchBounceTranslateY.value !== 0) { From 7bcf3f5aa31a22d75eca5d659e8aaff21f938f6e Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 29 Dec 2023 18:43:26 +0100 Subject: [PATCH 010/107] add more docs --- .../MultiGestureCanvas/usePanGesture.js | 20 +++++++++---------- .../MultiGestureCanvas/usePinchGesture.js | 12 +++++------ 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/components/MultiGestureCanvas/usePanGesture.js b/src/components/MultiGestureCanvas/usePanGesture.js index c81af12ebb62..e8203f3bb75e 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.js +++ b/src/components/MultiGestureCanvas/usePanGesture.js @@ -30,14 +30,13 @@ const usePanGesture = ({ const zoomScaledContentWidth = useDerivedValue(() => contentSize.width * totalScale.value, [contentSize.width]); const zoomScaledContentHeight = useDerivedValue(() => contentSize.height * totalScale.value, [contentSize.height]); + const previousTouch = useSharedValue(null); // pan velocity to calculate the decay const panVelocityX = useSharedValue(0); const panVelocityY = useSharedValue(0); // disable pan vertically when content is smaller than screen const canPanVertically = useDerivedValue(() => canvasSize.height < zoomScaledContentHeight.value, [canvasSize.height]); - const previousTouch = useSharedValue(null); - // calculates bounds of the scaled content // can we pan left/right/up/down // can be used to limit gesture or implementing tension effect @@ -167,6 +166,7 @@ const usePanGesture = ({ // since we're running both pinch and pan gesture handlers simultaneously // we need to make sure that we don't pan when we pinch and move fingers // since we track it as pinch focal gesture + // we also need to prevent panning when we are swiping horizontally (in the pager) if (evt.numberOfPointers > 1 || isSwipingHorizontally.value) { return; } @@ -184,20 +184,16 @@ const usePanGesture = ({ } }) .onEnd((evt) => { - previousTouch.value = null; + // add pan translation to total offset + offsetX.value += panTranslateX.value; + offsetY.value += panTranslateY.value; // If we are swiping, we don't want to return to boundaries if (isSwipingHorizontally.value) { return; } - // add pan translation to total offset - offsetX.value += panTranslateX.value; - offsetY.value += panTranslateY.value; - // reset pan gesture variables - panTranslateX.value = 0; - panTranslateY.value = 0; - + // swipe to close animation when swiping down if (isSwipingVertically.value) { const enoughVelocity = Math.abs(evt.velocityY) > 300 && Math.abs(evt.velocityX) < Math.abs(evt.velocityY); const rightDirection = (evt.translationY > 0 && evt.velocityY > 0) || (evt.translationY < 0 && evt.velocityY < 0); @@ -229,8 +225,12 @@ const usePanGesture = ({ returnToBoundaries(); + // reset pan gesture variables panVelocityX.value = 0; panVelocityY.value = 0; + panTranslateX.value = 0; + panTranslateY.value = 0; + previousTouch.value = null; }) .withRef(panGestureRef); diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.js index 4dbbdf53f3f0..cbfa525daae9 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.js +++ b/src/components/MultiGestureCanvas/usePinchGesture.js @@ -90,10 +90,6 @@ const usePinchGesture = ({ // Add pinch translation to total offset offsetX.value += pinchTranslateX.value; offsetY.value += pinchTranslateY.value; - // Reset pinch gesture variables - pinchTranslateX.value = 0; - pinchTranslateY.value = 0; - pinchGestureScale.value = 1; if (zoomScale.value < zoomRange.min) { pinchScaleOffset.value = zoomRange.min; @@ -110,11 +106,15 @@ const usePinchGesture = ({ pinchBounceTranslateY.value = withSpring(0, SPRING_CONFIG); } - pinchGestureRunning.value = false; - if (onScaleChanged != null) { runOnJS(onScaleChanged)(zoomScale.value); } + + // Reset pinch gesture variables + pinchGestureRunning.value = false; + pinchTranslateX.value = 0; + pinchTranslateY.value = 0; + pinchGestureScale.value = 1; }); // Triggers "onPinchGestureChange" callback when pinch scale changes From a08e2bb866b2f5dce29cd16f78d5160045cc099d Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 29 Dec 2023 20:14:12 +0100 Subject: [PATCH 011/107] further improve component --- src/components/MultiGestureCanvas/index.js | 36 +++++++------- .../MultiGestureCanvas/usePanGesture.js | 47 ++++++++++--------- .../MultiGestureCanvas/usePinchGesture.js | 31 ++++++------ .../MultiGestureCanvas/useTapGestures.js | 11 +++-- src/components/MultiGestureCanvas/utils.ts | 4 +- 5 files changed, 65 insertions(+), 64 deletions(-) diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index 671426ba66e7..adbc46112621 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -56,9 +56,9 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr // and not smaller than needed to fit const totalScale = useDerivedValue(() => zoomScale.value * minContentScale, [minContentScale]); - // stored offset of the canvas (used for panning and pinching) - const offsetX = useSharedValue(0); - const offsetY = useSharedValue(0); + // total offset of the canvas (panning + pinching offset) + const totalOffsetX = useSharedValue(0); + const totalOffsetY = useSharedValue(0); // pan gesture const panTranslateX = useSharedValue(0); @@ -75,8 +75,8 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const pinchScaleOffset = useSharedValue(1); const stopAnimation = useWorkletCallback(() => { - cancelAnimation(offsetX); - cancelAnimation(offsetY); + cancelAnimation(totalOffsetX); + cancelAnimation(totalOffsetY); }); const reset = useWorkletCallback((animated) => { @@ -85,12 +85,12 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr stopAnimation(); if (animated) { - offsetX.value = withSpring(0, SPRING_CONFIG); - offsetY.value = withSpring(0, SPRING_CONFIG); + totalOffsetX.value = withSpring(0, SPRING_CONFIG); + totalOffsetY.value = withSpring(0, SPRING_CONFIG); zoomScale.value = withSpring(1, SPRING_CONFIG); } else { - offsetX.value = 0; - offsetY.value = 0; + totalOffsetX.value = 0; + totalOffsetY.value = 0; zoomScale.value = 1; panTranslateX.value = 0; panTranslateY.value = 0; @@ -105,8 +105,8 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr minContentScale, maxContentScale, panGestureRef, - offsetX, - offsetY, + totalOffsetX, + totalOffsetY, pinchScaleOffset, zoomScale, reset, @@ -125,8 +125,8 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr zoomScale, zoomRange, totalScale, - offsetX, - offsetY, + totalOffsetX, + totalOffsetY, panTranslateX, panTranslateY, isSwipingHorizontally, @@ -142,8 +142,8 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr panGesture, zoomScale, zoomRange, - offsetX, - offsetY, + totalOffsetX, + totalOffsetY, pinchTranslateX, pinchTranslateY, pinchBounceTranslateX, @@ -176,8 +176,10 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr }, [isActive, mounted, reset]); const animatedStyles = useAnimatedStyle(() => { - const x = pinchTranslateX.value + pinchBounceTranslateX.value + panTranslateX.value + offsetX.value; - const y = pinchTranslateY.value + pinchBounceTranslateY.value + panTranslateY.value + offsetY.value; + const x = pinchTranslateX.value + pinchBounceTranslateX.value + panTranslateX.value + totalOffsetX.value; + const y = pinchTranslateY.value + pinchBounceTranslateY.value + panTranslateY.value + totalOffsetY.value; + + // console.log({pinchTranslateY: pinchTranslateY.value, pinchBounceTranslateY: pinchBounceTranslateY.value, panTranslateY: panTranslateY.value, totalOffsetY: totalOffsetY.value}); if (isSwipingVertically.value) { onSwipe(y); diff --git a/src/components/MultiGestureCanvas/usePanGesture.js b/src/components/MultiGestureCanvas/usePanGesture.js index e8203f3bb75e..5d6279a8be56 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.js +++ b/src/components/MultiGestureCanvas/usePanGesture.js @@ -18,8 +18,8 @@ const usePanGesture = ({ zoomScale, zoomRange, totalScale, - offsetX, - offsetY, + totalOffsetX, + totalOffsetY, panTranslateX, panTranslateY, isSwipingVertically, @@ -56,12 +56,12 @@ const usePanGesture = ({ const minVector = {x: -rightBoundary, y: -topBoundary}; const target = { - x: clamp(offsetX.value, minVector.x, maxVector.x), - y: clamp(offsetY.value, minVector.y, maxVector.y), + x: clamp(totalOffsetX.value, minVector.x, maxVector.x), + y: clamp(totalOffsetY.value, minVector.y, maxVector.y), }; - const isInBoundaryX = target.x === offsetX.value; - const isInBoundaryY = target.y === offsetY.value; + const isInBoundaryX = target.x === totalOffsetX.value; + const isInBoundaryY = target.y === totalOffsetY.value; return { target, @@ -77,21 +77,21 @@ const usePanGesture = ({ const returnToBoundaries = useWorkletCallback(() => { const {target, isInBoundaryX, isInBoundaryY, minVector, maxVector} = getBounds(); - if (zoomScale.value === zoomRange.min && offsetX.value === 0 && offsetY.value === 0 && panTranslateX.value === 0 && panTranslateY.value === 0) { + if (zoomScale.value === zoomRange.min && totalOffsetX.value === 0 && totalOffsetY.value === 0 && panTranslateX.value === 0 && panTranslateY.value === 0) { // we don't need to run any animations return; } if (zoomScale.value <= zoomRange.min) { // just center it - offsetX.value = withSpring(0, SPRING_CONFIG); - offsetY.value = withSpring(0, SPRING_CONFIG); + totalOffsetX.value = withSpring(0, SPRING_CONFIG); + totalOffsetY.value = withSpring(0, SPRING_CONFIG); return; } if (isInBoundaryX) { if (Math.abs(panVelocityX.value) > 0 && zoomScale.value <= zoomRange.max) { - offsetX.value = withDecay({ + totalOffsetX.value = withDecay({ velocity: panVelocityX.value, clamp: [minVector.x, maxVector.x], deceleration: PAN_DECAY_DECELARATION, @@ -99,27 +99,27 @@ const usePanGesture = ({ }); } } else { - offsetX.value = withSpring(target.x, SPRING_CONFIG); + totalOffsetX.value = withSpring(target.x, SPRING_CONFIG); } if (!canPanVertically.value) { - offsetY.value = withSpring(target.y, SPRING_CONFIG); + totalOffsetY.value = withSpring(target.y, SPRING_CONFIG); } else if (isInBoundaryY) { if ( Math.abs(panVelocityY.value) > 0 && zoomScale.value <= zoomRange.max && // limit vertical pan only when content is smaller than screen - offsetY.value !== minVector.y && - offsetY.value !== maxVector.y + totalOffsetY.value !== minVector.y && + totalOffsetY.value !== maxVector.y ) { - offsetY.value = withDecay({ + totalOffsetY.value = withDecay({ velocity: panVelocityY.value, clamp: [minVector.y, maxVector.y], deceleration: PAN_DECAY_DECELARATION, }); } } else { - offsetY.value = withSpring(target.y, SPRING_CONFIG, () => { + totalOffsetY.value = withSpring(target.y, SPRING_CONFIG, () => { isSwipingVertically.value = false; }); } @@ -159,7 +159,7 @@ const usePanGesture = ({ } }) .simultaneousWithExternalGesture(pagerRef, singleTap, doubleTap) - .onBegin(() => { + .onStart(() => { stopAnimation(); }) .onChange((evt) => { @@ -185,8 +185,12 @@ const usePanGesture = ({ }) .onEnd((evt) => { // add pan translation to total offset - offsetX.value += panTranslateX.value; - offsetY.value += panTranslateY.value; + totalOffsetX.value += panTranslateX.value; + totalOffsetY.value += panTranslateY.value; + // reset pan gesture variables + panTranslateX.value = 0; + panTranslateY.value = 0; + previousTouch.value = null; // If we are swiping, we don't want to return to boundaries if (isSwipingHorizontally.value) { @@ -204,7 +208,7 @@ const usePanGesture = ({ return invert ? -v : v; }; - offsetY.value = withSpring( + totalOffsetY.value = withSpring( maybeInvert(contentSize.height * 2), { stiffness: 50, @@ -228,9 +232,6 @@ const usePanGesture = ({ // reset pan gesture variables panVelocityX.value = 0; panVelocityY.value = 0; - panTranslateX.value = 0; - panTranslateY.value = 0; - previousTouch.value = null; }) .withRef(panGestureRef); diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.js index cbfa525daae9..78aed77814cd 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.js +++ b/src/components/MultiGestureCanvas/usePinchGesture.js @@ -14,8 +14,8 @@ const usePinchGesture = ({ panGesture, zoomScale, zoomRange, - offsetX, - offsetY, + totalOffsetX, + totalOffsetY, pinchTranslateX, pinchTranslateY, pinchBounceTranslateX, @@ -26,9 +26,9 @@ const usePinchGesture = ({ onScaleChanged, onPinchGestureChange, }) => { + const isPinchGestureRunning = useSharedValue(false); // used to store event scale value when we limit scale const pinchGestureScale = useSharedValue(1); - const pinchGestureRunning = useSharedValue(false); // origin of the pinch gesture const pinchOrigin = { x: useSharedValue(0), @@ -37,8 +37,8 @@ const usePinchGesture = ({ const getAdjustedFocal = useWorkletCallback( (focalX, focalY) => ({ - x: focalX - (canvasSize.width / 2 + offsetX.value), - y: focalY - (canvasSize.height / 2 + offsetY.value), + x: focalX - (canvasSize.width / 2 + totalOffsetX.value), + y: focalY - (canvasSize.height / 2 + totalOffsetY.value), }), [canvasSize.width, canvasSize.height], ); @@ -53,7 +53,7 @@ const usePinchGesture = ({ }) .simultaneousWithExternalGesture(panGesture, singleTap, doubleTap) .onStart((evt) => { - pinchGestureRunning.value = true; + isPinchGestureRunning.value = true; stopAnimation(); @@ -88,8 +88,13 @@ const usePinchGesture = ({ }) .onEnd(() => { // Add pinch translation to total offset - offsetX.value += pinchTranslateX.value; - offsetY.value += pinchTranslateY.value; + totalOffsetX.value += pinchTranslateX.value; + totalOffsetY.value += pinchTranslateY.value; + // Reset pinch gesture variables + pinchTranslateX.value = 0; + pinchTranslateY.value = 0; + pinchGestureScale.value = 1; + isPinchGestureRunning.value = false; if (zoomScale.value < zoomRange.min) { pinchScaleOffset.value = zoomRange.min; @@ -107,20 +112,14 @@ const usePinchGesture = ({ } if (onScaleChanged != null) { - runOnJS(onScaleChanged)(zoomScale.value); + runOnJS(onScaleChanged)(pinchScaleOffset.value); } - - // Reset pinch gesture variables - pinchGestureRunning.value = false; - pinchTranslateX.value = 0; - pinchTranslateY.value = 0; - pinchGestureScale.value = 1; }); // Triggers "onPinchGestureChange" callback when pinch scale changes const [isPinchGestureInUse, setIsPinchGestureInUse] = useState(false); useAnimatedReaction( - () => [zoomScale.value, pinchGestureRunning.value], + () => [zoomScale.value, isPinchGestureRunning.value], ([zoom, running]) => { const newIsPinchGestureInUse = zoom !== 1 || running; if (isPinchGestureInUse !== newIsPinchGestureInUse) { diff --git a/src/components/MultiGestureCanvas/useTapGestures.js b/src/components/MultiGestureCanvas/useTapGestures.js index 0af5076618a4..a08fbc8fed4c 100644 --- a/src/components/MultiGestureCanvas/useTapGestures.js +++ b/src/components/MultiGestureCanvas/useTapGestures.js @@ -4,8 +4,9 @@ import {Gesture} from 'react-native-gesture-handler'; import {runOnJS, useWorkletCallback, withSpring} from 'react-native-reanimated'; import * as MultiGestureCanvasUtils from './utils'; +const DOUBLE_TAP_SCALE = 3; + const clamp = MultiGestureCanvasUtils.clamp; -const DOUBLE_TAP_SCALE = MultiGestureCanvasUtils.DOUBLE_TAP_SCALE; const SPRING_CONFIG = MultiGestureCanvasUtils.SPRING_CONFIG; const useTapGestures = ({ @@ -14,8 +15,8 @@ const useTapGestures = ({ minContentScale, maxContentScale, panGestureRef, - offsetX, - offsetY, + totalOffsetX, + totalOffsetY, pinchScaleOffset, zoomScale, reset, @@ -82,8 +83,8 @@ const useTapGestures = ({ target.y = 0; } - offsetX.value = withSpring(target.x, SPRING_CONFIG); - offsetY.value = withSpring(target.y, SPRING_CONFIG); + totalOffsetX.value = withSpring(target.x, SPRING_CONFIG); + totalOffsetY.value = withSpring(target.y, SPRING_CONFIG); zoomScale.value = withSpring(doubleTapScale, SPRING_CONFIG); pinchScaleOffset.value = doubleTapScale; }, diff --git a/src/components/MultiGestureCanvas/utils.ts b/src/components/MultiGestureCanvas/utils.ts index c28f02ed0f39..da4c1133d237 100644 --- a/src/components/MultiGestureCanvas/utils.ts +++ b/src/components/MultiGestureCanvas/utils.ts @@ -1,5 +1,3 @@ -const DOUBLE_TAP_SCALE = 3; - const SPRING_CONFIG = { mass: 1, stiffness: 1000, @@ -16,4 +14,4 @@ function clamp(value: number, lowerBound: number, upperBound: number) { return Math.min(Math.max(lowerBound, value), upperBound); } -export {clamp, DOUBLE_TAP_SCALE, SPRING_CONFIG, zoomScaleBounceFactors}; +export {clamp, SPRING_CONFIG, zoomScaleBounceFactors}; From 87559169ec7a23ec4748aa2ad06f8743d16a26c2 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 3 Jan 2024 14:04:34 +0100 Subject: [PATCH 012/107] add and improve comments --- .../AttachmentCarousel/Pager/index.js | 7 +-- src/components/MultiGestureCanvas/index.js | 21 ++++---- .../MultiGestureCanvas/usePanGesture.js | 52 +++++++++++-------- .../MultiGestureCanvas/usePinchGesture.js | 3 +- .../MultiGestureCanvas/useTapGestures.js | 3 +- src/components/MultiGestureCanvas/utils.ts | 12 ++++- 6 files changed, 56 insertions(+), 42 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.js b/src/components/Attachments/AttachmentCarousel/Pager/index.js index 699e2fc812cc..ad844c1df854 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.js +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.js @@ -47,7 +47,6 @@ const pagerPropTypes = { onPageSelected: PropTypes.func, onTap: PropTypes.func, onSwipe: PropTypes.func, - onSwipeSuccess: PropTypes.func, onSwipeDown: PropTypes.func, onPinchGestureChange: PropTypes.func, forwardedRef: refPropTypes, @@ -58,13 +57,12 @@ const pagerDefaultProps = { onPageSelected: () => {}, onTap: () => {}, onSwipe: noopWorklet, - onSwipeSuccess: () => {}, onSwipeDown: () => {}, onPinchGestureChange: () => {}, forwardedRef: null, }; -function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelected, onTap, onSwipe = noopWorklet, onSwipeSuccess, onSwipeDown, onPinchGestureChange, forwardedRef}) { +function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelected, onTap, onSwipe = noopWorklet, onSwipeDown, onPinchGestureChange, forwardedRef}) { const styles = useThemeStyles(); const shouldPagerScroll = useSharedValue(true); const pagerRef = useRef(null); @@ -124,10 +122,9 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte onPinchGestureChange, onTap, onSwipe, - onSwipeSuccess, onSwipeDown, }), - [isSwipingHorizontally, pagerRef, shouldPagerScroll, onPinchGestureChange, onTap, onSwipe, onSwipeSuccess, onSwipeDown], + [isSwipingHorizontally, pagerRef, shouldPagerScroll, onPinchGestureChange, onTap, onSwipe, onSwipeDown], ); return ( diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index adbc46112621..9da8053aef07 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -1,7 +1,7 @@ import React, {useContext, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; -import Animated, {cancelAnimation, runOnUI, useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, useWorkletCallback, withSpring} from 'react-native-reanimated'; +import Animated, {cancelAnimation, runOnUI, useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, withSpring} from 'react-native-reanimated'; import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -14,6 +14,7 @@ import * as MultiGestureCanvasUtils from './utils'; const SPRING_CONFIG = MultiGestureCanvasUtils.SPRING_CONFIG; const zoomScaleBounceFactors = MultiGestureCanvasUtils.zoomScaleBounceFactors; +const useWorkletCallback = MultiGestureCanvasUtils.useWorkletCallback; function getDeepDefaultProps({contentSize: contentSizeProp = {}, zoomRange: zoomRangeProp = {}}) { const contentSize = { @@ -37,10 +38,9 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); const pagerRefFallback = useRef(null); - const {onTap, onSwipe, onSwipeSuccess, pagerRef, shouldPagerScroll, isSwipingHorizontally, onPinchGestureChange} = attachmentCarouselPagerContext || { + const {onTap, onSwipeDown, pagerRef, shouldPagerScroll, isSwipingHorizontally, onPinchGestureChange} = attachmentCarouselPagerContext || { onTap: () => undefined, - onSwipe: () => undefined, - onSwipeSuccess: () => undefined, + onSwipeDown: () => undefined, onPinchGestureChange: () => undefined, pagerRef: pagerRefFallback, shouldPagerScroll: false, @@ -131,7 +131,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr panTranslateY, isSwipingHorizontally, isSwipingVertically, - onSwipeSuccess, + onSwipeDown, stopAnimation, }); @@ -155,7 +155,8 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr onPinchGestureChange, }); - // reacts to scale change and enables/disables pager scroll + // Enables/disables the pager scroll based on the zoom scale + // When the content is zoomed in/out, the pager should be disabled useAnimatedReaction( () => zoomScale.value, () => { @@ -179,11 +180,9 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const x = pinchTranslateX.value + pinchBounceTranslateX.value + panTranslateX.value + totalOffsetX.value; const y = pinchTranslateY.value + pinchBounceTranslateY.value + panTranslateY.value + totalOffsetY.value; - // console.log({pinchTranslateY: pinchTranslateY.value, pinchBounceTranslateY: pinchBounceTranslateY.value, panTranslateY: panTranslateY.value, totalOffsetY: totalOffsetY.value}); - - if (isSwipingVertically.value) { - onSwipe(y); - } + // if (isSwipingVertically.value) { + // onSwipe(y); + // } return { transform: [ diff --git a/src/components/MultiGestureCanvas/usePanGesture.js b/src/components/MultiGestureCanvas/usePanGesture.js index 5d6279a8be56..a2b92d3bcf3c 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.js +++ b/src/components/MultiGestureCanvas/usePanGesture.js @@ -1,12 +1,13 @@ /* eslint-disable no-param-reassign */ import {Gesture} from 'react-native-gesture-handler'; -import {runOnJS, useDerivedValue, useSharedValue, useWorkletCallback, withDecay, withSpring} from 'react-native-reanimated'; +import {runOnJS, useDerivedValue, useSharedValue, withDecay, withSpring} from 'react-native-reanimated'; import * as MultiGestureCanvasUtils from './utils'; const PAN_DECAY_DECELARATION = 0.9915; -const clamp = MultiGestureCanvasUtils.clamp; const SPRING_CONFIG = MultiGestureCanvasUtils.SPRING_CONFIG; +const clamp = MultiGestureCanvasUtils.clamp; +const useWorkletCallback = MultiGestureCanvasUtils.useWorkletCallback; const usePanGesture = ({ canvasSize, @@ -24,22 +25,26 @@ const usePanGesture = ({ panTranslateY, isSwipingVertically, isSwipingHorizontally, - onSwipeSuccess, + onSwipeDown, stopAnimation, }) => { + // The content size after scaling it with the current (total) zoom value const zoomScaledContentWidth = useDerivedValue(() => contentSize.width * totalScale.value, [contentSize.width]); const zoomScaledContentHeight = useDerivedValue(() => contentSize.height * totalScale.value, [contentSize.height]); + // Used to track previous touch position for the "swipe down to close" gesture const previousTouch = useSharedValue(null); - // pan velocity to calculate the decay + + // Pan velocity to calculate the decay const panVelocityX = useSharedValue(0); const panVelocityY = useSharedValue(0); - // disable pan vertically when content is smaller than screen + + // Disable "swipe down to close" gesture when content is bigger than the canvas const canPanVertically = useDerivedValue(() => canvasSize.height < zoomScaledContentHeight.value, [canvasSize.height]); - // calculates bounds of the scaled content - // can we pan left/right/up/down - // can be used to limit gesture or implementing tension effect + // Calculates bounds of the scaled content + // Can we pan left/right/up/down + // Can be used to limit gesture or implementing tension effect const getBounds = useWorkletCallback(() => { let rightBoundary = 0; let topBoundary = 0; @@ -78,12 +83,12 @@ const usePanGesture = ({ const {target, isInBoundaryX, isInBoundaryY, minVector, maxVector} = getBounds(); if (zoomScale.value === zoomRange.min && totalOffsetX.value === 0 && totalOffsetY.value === 0 && panTranslateX.value === 0 && panTranslateY.value === 0) { - // we don't need to run any animations + // We don't need to run any animations return; } + // If we are zoomed out, we want to center the content if (zoomScale.value <= zoomRange.min) { - // just center it totalOffsetX.value = withSpring(0, SPRING_CONFIG); totalOffsetY.value = withSpring(0, SPRING_CONFIG); return; @@ -108,7 +113,7 @@ const usePanGesture = ({ if ( Math.abs(panVelocityY.value) > 0 && zoomScale.value <= zoomRange.max && - // limit vertical pan only when content is smaller than screen + // Limit vertical panning when content is smaller than screen totalOffsetY.value !== minVector.y && totalOffsetY.value !== maxVector.y ) { @@ -143,10 +148,9 @@ const usePanGesture = ({ // if (Math.abs(velocityY) > velocityX && velocityY > 20) { // state.activate(); - // isSwiping.value = true; + // isSwipingVertically.value = true; // previousTouch.value = null; - // runOnJS(onSwipeDown)(); // return; // } // } @@ -163,10 +167,10 @@ const usePanGesture = ({ stopAnimation(); }) .onChange((evt) => { - // since we're running both pinch and pan gesture handlers simultaneously - // we need to make sure that we don't pan when we pinch and move fingers - // since we track it as pinch focal gesture - // we also need to prevent panning when we are swiping horizontally (in the pager) + // Since we're running both pinch and pan gesture handlers simultaneously, + // we need to make sure that we don't pan when we pinch AND move fingers + // since we track it as pinch focal gesture. + // We also need to prevent panning when we are swiping horizontally (from page to page) if (evt.numberOfPointers > 1 || isSwipingHorizontally.value) { return; } @@ -184,20 +188,22 @@ const usePanGesture = ({ } }) .onEnd((evt) => { - // add pan translation to total offset + // Add pan translation to total offset totalOffsetX.value += panTranslateX.value; totalOffsetY.value += panTranslateY.value; - // reset pan gesture variables + + // Reset pan gesture variables panTranslateX.value = 0; panTranslateY.value = 0; previousTouch.value = null; - // If we are swiping, we don't want to return to boundaries + // If we are swiping (in the pager), we don't want to return to boundaries if (isSwipingHorizontally.value) { return; } - // swipe to close animation when swiping down + // Triggers the "swipe down to close" animation and the "onSwipeDown" callback, + // which can be used to close the lightbox/carousel if (isSwipingVertically.value) { const enoughVelocity = Math.abs(evt.velocityY) > 300 && Math.abs(evt.velocityX) < Math.abs(evt.velocityY); const rightDirection = (evt.translationY > 0 && evt.velocityY > 0) || (evt.translationY < 0 && evt.velocityY < 0); @@ -220,7 +226,7 @@ const usePanGesture = ({ velocity: Math.abs(evt.velocityY) < 1200 ? maybeInvert(1200) : evt.velocityY, }, () => { - runOnJS(onSwipeSuccess)(); + runOnJS(onSwipeDown)(); }, ); return; @@ -229,7 +235,7 @@ const usePanGesture = ({ returnToBoundaries(); - // reset pan gesture variables + // Reset pan gesture variables panVelocityX.value = 0; panVelocityY.value = 0; }) diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.js index 78aed77814cd..fad23d1d409a 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.js +++ b/src/components/MultiGestureCanvas/usePinchGesture.js @@ -1,11 +1,12 @@ /* eslint-disable no-param-reassign */ import {useEffect, useState} from 'react'; import {Gesture} from 'react-native-gesture-handler'; -import {runOnJS, useAnimatedReaction, useSharedValue, useWorkletCallback, withSpring} from 'react-native-reanimated'; +import {runOnJS, useAnimatedReaction, useSharedValue, withSpring} from 'react-native-reanimated'; import * as MultiGestureCanvasUtils from './utils'; const SPRING_CONFIG = MultiGestureCanvasUtils.SPRING_CONFIG; const zoomScaleBounceFactors = MultiGestureCanvasUtils.zoomScaleBounceFactors; +const useWorkletCallback = MultiGestureCanvasUtils.useWorkletCallback; const usePinchGesture = ({ canvasSize, diff --git a/src/components/MultiGestureCanvas/useTapGestures.js b/src/components/MultiGestureCanvas/useTapGestures.js index a08fbc8fed4c..0b354ed6c54a 100644 --- a/src/components/MultiGestureCanvas/useTapGestures.js +++ b/src/components/MultiGestureCanvas/useTapGestures.js @@ -1,13 +1,14 @@ /* eslint-disable no-param-reassign */ import {useMemo} from 'react'; import {Gesture} from 'react-native-gesture-handler'; -import {runOnJS, useWorkletCallback, withSpring} from 'react-native-reanimated'; +import {runOnJS, withSpring} from 'react-native-reanimated'; import * as MultiGestureCanvasUtils from './utils'; const DOUBLE_TAP_SCALE = 3; const clamp = MultiGestureCanvasUtils.clamp; const SPRING_CONFIG = MultiGestureCanvasUtils.SPRING_CONFIG; +const useWorkletCallback = MultiGestureCanvasUtils.useWorkletCallback; const useTapGestures = ({ canvasSize, diff --git a/src/components/MultiGestureCanvas/utils.ts b/src/components/MultiGestureCanvas/utils.ts index da4c1133d237..7a4ba21358c4 100644 --- a/src/components/MultiGestureCanvas/utils.ts +++ b/src/components/MultiGestureCanvas/utils.ts @@ -1,3 +1,5 @@ +import {useCallback} from 'react'; + const SPRING_CONFIG = { mass: 1, stiffness: 1000, @@ -8,10 +10,18 @@ const zoomScaleBounceFactors = { min: 0.7, max: 1.5, }; + function clamp(value: number, lowerBound: number, upperBound: number) { 'worklet'; return Math.min(Math.max(lowerBound, value), upperBound); } -export {clamp, SPRING_CONFIG, zoomScaleBounceFactors}; +const useWorkletCallback = (callback: Parameters[0], deps: Parameters[1] = []) => { + 'worklet'; + + // eslint-disable-next-line react-hooks/exhaustive-deps + return useCallback(callback, deps); +}; + +export {SPRING_CONFIG, zoomScaleBounceFactors, clamp, useWorkletCallback}; From 0e7fe03e1a61782d40e8ce08b3144c0bf06cbb84 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 3 Jan 2024 14:17:40 +0100 Subject: [PATCH 013/107] remove "swipe down to close gesture" --- .../AttachmentCarousel/Pager/index.js | 16 +--- src/components/MultiGestureCanvas/index.js | 10 +-- .../MultiGestureCanvas/usePanGesture.js | 75 ++----------------- 3 files changed, 9 insertions(+), 92 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.js b/src/components/Attachments/AttachmentCarousel/Pager/index.js index ad844c1df854..a85ae10e2328 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.js +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.js @@ -29,12 +29,6 @@ function usePageScrollHandler(handlers, dependencies) { ); } -const noopWorklet = () => { - 'worklet'; - - // noop -}; - const pagerPropTypes = { items: PropTypes.arrayOf( PropTypes.shape({ @@ -46,8 +40,6 @@ const pagerPropTypes = { initialIndex: PropTypes.number, onPageSelected: PropTypes.func, onTap: PropTypes.func, - onSwipe: PropTypes.func, - onSwipeDown: PropTypes.func, onPinchGestureChange: PropTypes.func, forwardedRef: refPropTypes, }; @@ -56,13 +48,11 @@ const pagerDefaultProps = { initialIndex: 0, onPageSelected: () => {}, onTap: () => {}, - onSwipe: noopWorklet, - onSwipeDown: () => {}, onPinchGestureChange: () => {}, forwardedRef: null, }; -function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelected, onTap, onSwipe = noopWorklet, onSwipeDown, onPinchGestureChange, forwardedRef}) { +function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelected, onTap, onPinchGestureChange, forwardedRef}) { const styles = useThemeStyles(); const shouldPagerScroll = useSharedValue(true); const pagerRef = useRef(null); @@ -121,10 +111,8 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte shouldPagerScroll, onPinchGestureChange, onTap, - onSwipe, - onSwipeDown, }), - [isSwipingHorizontally, pagerRef, shouldPagerScroll, onPinchGestureChange, onTap, onSwipe, onSwipeDown], + [isSwipingHorizontally, pagerRef, shouldPagerScroll, onPinchGestureChange, onTap], ); return ( diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index 9da8053aef07..1d91e4cad20e 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -38,9 +38,8 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); const pagerRefFallback = useRef(null); - const {onTap, onSwipeDown, pagerRef, shouldPagerScroll, isSwipingHorizontally, onPinchGestureChange} = attachmentCarouselPagerContext || { + const {onTap, pagerRef, shouldPagerScroll, isSwipingHorizontally, onPinchGestureChange} = attachmentCarouselPagerContext || { onTap: () => undefined, - onSwipeDown: () => undefined, onPinchGestureChange: () => undefined, pagerRef: pagerRefFallback, shouldPagerScroll: false, @@ -63,7 +62,6 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr // pan gesture const panTranslateX = useSharedValue(0); const panTranslateY = useSharedValue(0); - const isSwipingVertically = useSharedValue(false); const panGestureRef = useRef(Gesture.Pan()); // pinch gesture @@ -130,8 +128,6 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr panTranslateX, panTranslateY, isSwipingHorizontally, - isSwipingVertically, - onSwipeDown, stopAnimation, }); @@ -180,10 +176,6 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const x = pinchTranslateX.value + pinchBounceTranslateX.value + panTranslateX.value + totalOffsetX.value; const y = pinchTranslateY.value + pinchBounceTranslateY.value + panTranslateY.value + totalOffsetY.value; - // if (isSwipingVertically.value) { - // onSwipe(y); - // } - return { transform: [ { diff --git a/src/components/MultiGestureCanvas/usePanGesture.js b/src/components/MultiGestureCanvas/usePanGesture.js index a2b92d3bcf3c..807252670b43 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.js +++ b/src/components/MultiGestureCanvas/usePanGesture.js @@ -1,6 +1,6 @@ /* eslint-disable no-param-reassign */ import {Gesture} from 'react-native-gesture-handler'; -import {runOnJS, useDerivedValue, useSharedValue, withDecay, withSpring} from 'react-native-reanimated'; +import {useDerivedValue, useSharedValue, withDecay, withSpring} from 'react-native-reanimated'; import * as MultiGestureCanvasUtils from './utils'; const PAN_DECAY_DECELARATION = 0.9915; @@ -23,9 +23,7 @@ const usePanGesture = ({ totalOffsetY, panTranslateX, panTranslateY, - isSwipingVertically, isSwipingHorizontally, - onSwipeDown, stopAnimation, }) => { // The content size after scaling it with the current (total) zoom value @@ -39,9 +37,6 @@ const usePanGesture = ({ const panVelocityX = useSharedValue(0); const panVelocityY = useSharedValue(0); - // Disable "swipe down to close" gesture when content is bigger than the canvas - const canPanVertically = useDerivedValue(() => canvasSize.height < zoomScaledContentHeight.value, [canvasSize.height]); - // Calculates bounds of the scaled content // Can we pan left/right/up/down // Can be used to limit gesture or implementing tension effect @@ -107,9 +102,7 @@ const usePanGesture = ({ totalOffsetX.value = withSpring(target.x, SPRING_CONFIG); } - if (!canPanVertically.value) { - totalOffsetY.value = withSpring(target.y, SPRING_CONFIG); - } else if (isInBoundaryY) { + if (isInBoundaryY) { if ( Math.abs(panVelocityY.value) > 0 && zoomScale.value <= zoomRange.max && @@ -124,9 +117,7 @@ const usePanGesture = ({ }); } } else { - totalOffsetY.value = withSpring(target.y, SPRING_CONFIG, () => { - isSwipingVertically.value = false; - }); + totalOffsetY.value = withSpring(target.y, SPRING_CONFIG); } }); @@ -138,23 +129,6 @@ const usePanGesture = ({ state.activate(); } - // TODO: Swipe down to close carousel gesture - // this needs fine tuning to work properly - // if (!isSwipingHorizontally.value && scale.value === 1 && previousTouch.value != null) { - // const velocityX = Math.abs(evt.allTouches[0].x - previousTouch.value.x); - // const velocityY = evt.allTouches[0].y - previousTouch.value.y; - - // // TODO: this needs tuning - // if (Math.abs(velocityY) > velocityX && velocityY > 20) { - // state.activate(); - - // isSwipingVertically.value = true; - // previousTouch.value = null; - - // return; - // } - // } - if (previousTouch.value == null) { previousTouch.value = { x: evt.allTouches[0].x, @@ -176,18 +150,12 @@ const usePanGesture = ({ } panVelocityX.value = evt.velocityX; - panVelocityY.value = evt.velocityY; - if (!isSwipingVertically.value) { - panTranslateX.value += evt.changeX; - } - - if (canPanVertically.value || isSwipingVertically.value) { - panTranslateY.value += evt.changeY; - } + panTranslateX.value += evt.changeX; + panTranslateY.value += evt.changeY; }) - .onEnd((evt) => { + .onEnd(() => { // Add pan translation to total offset totalOffsetX.value += panTranslateX.value; totalOffsetY.value += panTranslateY.value; @@ -202,37 +170,6 @@ const usePanGesture = ({ return; } - // Triggers the "swipe down to close" animation and the "onSwipeDown" callback, - // which can be used to close the lightbox/carousel - if (isSwipingVertically.value) { - const enoughVelocity = Math.abs(evt.velocityY) > 300 && Math.abs(evt.velocityX) < Math.abs(evt.velocityY); - const rightDirection = (evt.translationY > 0 && evt.velocityY > 0) || (evt.translationY < 0 && evt.velocityY < 0); - - if (enoughVelocity && rightDirection) { - const maybeInvert = (v) => { - const invert = evt.velocityY < 0; - return invert ? -v : v; - }; - - totalOffsetY.value = withSpring( - maybeInvert(contentSize.height * 2), - { - stiffness: 50, - damping: 30, - mass: 1, - overshootClamping: true, - restDisplacementThreshold: 300, - restSpeedThreshold: 300, - velocity: Math.abs(evt.velocityY) < 1200 ? maybeInvert(1200) : evt.velocityY, - }, - () => { - runOnJS(onSwipeDown)(); - }, - ); - return; - } - } - returnToBoundaries(); // Reset pan gesture variables From 70bd5b786ccf0f81205b1a95176f64dc55d7050a Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 3 Jan 2024 14:31:39 +0100 Subject: [PATCH 014/107] remove unused props --- src/components/AttachmentModal.js | 1 - src/components/Attachments/AttachmentCarousel/index.native.js | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index 863e59aa4474..7c062366f8a7 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -449,7 +449,6 @@ function AttachmentModal(props) { report={props.report} onNavigate={onNavigate} source={props.source} - onClose={closeModal} onToggleKeyboard={updateConfirmButtonVisibility} setDownloadButtonVisibility={setDownloadButtonVisibility} /> diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js index f5479b73abdb..003c27844fbc 100644 --- a/src/components/Attachments/AttachmentCarousel/index.native.js +++ b/src/components/Attachments/AttachmentCarousel/index.native.js @@ -18,7 +18,7 @@ import extractAttachmentsFromReport from './extractAttachmentsFromReport'; import AttachmentCarouselPager from './Pager'; import useCarouselArrows from './useCarouselArrows'; -function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, translate, onClose}) { +function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, translate}) { const styles = useThemeStyles(); const pagerRef = useRef(null); const [page, setPage] = useState(); @@ -147,7 +147,6 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, setShouldShowArrows(true); } }} - onSwipeDown={onClose} ref={pagerRef} /> From f95f9c664917d60af6e9b234373a70e95eb179c5 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 3 Jan 2024 14:34:33 +0100 Subject: [PATCH 015/107] rename variable --- .../Attachments/AttachmentCarousel/Pager/index.js | 10 +++++----- src/components/MultiGestureCanvas/index.js | 8 ++++---- src/components/MultiGestureCanvas/usePanGesture.js | 6 +++--- src/components/MultiGestureCanvas/usePinchGesture.js | 4 ++-- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.js b/src/components/Attachments/AttachmentCarousel/Pager/index.js index a85ae10e2328..d7d6bda1be29 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.js +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.js @@ -57,7 +57,7 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte const shouldPagerScroll = useSharedValue(true); const pagerRef = useRef(null); - const isSwipingHorizontally = useSharedValue(false); + const isSwipingInPager = useSharedValue(false); const activeIndex = useSharedValue(initialIndex); const pageScrollHandler = usePageScrollHandler( @@ -66,7 +66,7 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte 'worklet'; activeIndex.value = e.position; - isSwipingHorizontally.value = e.offset !== 0; + isSwipingInPager.value = e.offset !== 0; }, }, [], @@ -82,7 +82,7 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte // we use reanimated for this since onPageSelected is called // in the middle of the pager animation useAnimatedReaction( - () => isSwipingHorizontally.value, + () => isSwipingInPager.value, (stillScrolling) => { if (stillScrolling) { return; @@ -106,13 +106,13 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte const contextValue = useMemo( () => ({ - isSwipingHorizontally, + isSwipingInPager, pagerRef, shouldPagerScroll, onPinchGestureChange, onTap, }), - [isSwipingHorizontally, pagerRef, shouldPagerScroll, onPinchGestureChange, onTap], + [isSwipingInPager, pagerRef, shouldPagerScroll, onPinchGestureChange, onTap], ); return ( diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index 1d91e4cad20e..24f01cf5f3f1 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -38,12 +38,12 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); const pagerRefFallback = useRef(null); - const {onTap, pagerRef, shouldPagerScroll, isSwipingHorizontally, onPinchGestureChange} = attachmentCarouselPagerContext || { + const {onTap, pagerRef, shouldPagerScroll, isSwipingInPager, onPinchGestureChange} = attachmentCarouselPagerContext || { onTap: () => undefined, onPinchGestureChange: () => undefined, pagerRef: pagerRefFallback, shouldPagerScroll: false, - isSwipingHorizontally: false, + isSwipingInPager: false, ...props, }; @@ -127,7 +127,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr totalOffsetY, panTranslateX, panTranslateY, - isSwipingHorizontally, + isSwipingInPager, stopAnimation, }); @@ -145,7 +145,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr pinchBounceTranslateX, pinchBounceTranslateY, pinchScaleOffset, - isSwipingHorizontally, + isSwipingInPager, stopAnimation, onScaleChanged, onPinchGestureChange, diff --git a/src/components/MultiGestureCanvas/usePanGesture.js b/src/components/MultiGestureCanvas/usePanGesture.js index 807252670b43..b6639dcac1a6 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.js +++ b/src/components/MultiGestureCanvas/usePanGesture.js @@ -23,7 +23,7 @@ const usePanGesture = ({ totalOffsetY, panTranslateX, panTranslateY, - isSwipingHorizontally, + isSwipingInPager, stopAnimation, }) => { // The content size after scaling it with the current (total) zoom value @@ -145,7 +145,7 @@ const usePanGesture = ({ // we need to make sure that we don't pan when we pinch AND move fingers // since we track it as pinch focal gesture. // We also need to prevent panning when we are swiping horizontally (from page to page) - if (evt.numberOfPointers > 1 || isSwipingHorizontally.value) { + if (evt.numberOfPointers > 1 || isSwipingInPager.value) { return; } @@ -166,7 +166,7 @@ const usePanGesture = ({ previousTouch.value = null; // If we are swiping (in the pager), we don't want to return to boundaries - if (isSwipingHorizontally.value) { + if (isSwipingInPager.value) { return; } diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.js index fad23d1d409a..630ace4e3042 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.js +++ b/src/components/MultiGestureCanvas/usePinchGesture.js @@ -22,7 +22,7 @@ const usePinchGesture = ({ pinchBounceTranslateX, pinchBounceTranslateY, pinchScaleOffset, - isSwipingHorizontally, + isSwipingInPager, stopAnimation, onScaleChanged, onPinchGestureChange, @@ -46,7 +46,7 @@ const usePinchGesture = ({ const pinchGesture = Gesture.Pinch() .onTouchesDown((evt, state) => { // we don't want to activate pinch gesture when we are scrolling pager - if (!isSwipingHorizontally.value) { + if (!isSwipingInPager.value) { return; } From 610f59fa1713148cb971ddcb913b9b782bd23cd0 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 3 Jan 2024 14:56:43 +0100 Subject: [PATCH 016/107] simplify pinch gesture --- src/components/MultiGestureCanvas/index.js | 56 ++++++------- .../MultiGestureCanvas/usePinchGesture.js | 78 ++++++++++++------- .../MultiGestureCanvas/useTapGestures.js | 6 +- 3 files changed, 80 insertions(+), 60 deletions(-) diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index 24f01cf5f3f1..ade0a7b54c38 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -50,27 +50,24 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const {minScale: minContentScale, maxScale: maxContentScale} = useMemo(() => getCanvasFitScale({canvasSize, contentSize}), [canvasSize, contentSize]); const zoomScale = useSharedValue(1); - // Adding together the pinch zoom scale and the initial scale to fit the content into the canvas - // Using the smaller content scale, so that the immage is not bigger than the canvas + + // Adding together zoom scale and the initial scale to fit the content into the canvas + // Using the minimum content scale, so that the image is not bigger than the canvas // and not smaller than needed to fit const totalScale = useDerivedValue(() => zoomScale.value * minContentScale, [minContentScale]); - // total offset of the canvas (panning + pinching offset) - const totalOffsetX = useSharedValue(0); - const totalOffsetY = useSharedValue(0); - - // pan gesture const panTranslateX = useSharedValue(0); const panTranslateY = useSharedValue(0); const panGestureRef = useRef(Gesture.Pan()); - // pinch gesture + const pinchScale = useSharedValue(1); const pinchTranslateX = useSharedValue(0); const pinchTranslateY = useSharedValue(0); - const pinchBounceTranslateX = useSharedValue(0); - const pinchBounceTranslateY = useSharedValue(0); - // scale in between gestures - const pinchScaleOffset = useSharedValue(1); + + // Total offset of the canvas + // Contains both offsets from panning and pinching gestures + const totalOffsetX = useSharedValue(0); + const totalOffsetY = useSharedValue(0); const stopAnimation = useWorkletCallback(() => { cancelAnimation(totalOffsetX); @@ -78,23 +75,30 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr }); const reset = useWorkletCallback((animated) => { - pinchScaleOffset.value = 1; + pinchScale.value = 1; stopAnimation(); + pinchScale.value = 1; + if (animated) { totalOffsetX.value = withSpring(0, SPRING_CONFIG); totalOffsetY.value = withSpring(0, SPRING_CONFIG); + panTranslateX.value = withSpring(0, SPRING_CONFIG); + panTranslateY.value = withSpring(0, SPRING_CONFIG); + pinchTranslateX.value = withSpring(0, SPRING_CONFIG); + pinchTranslateY.value = withSpring(0, SPRING_CONFIG); zoomScale.value = withSpring(1, SPRING_CONFIG); - } else { - totalOffsetX.value = 0; - totalOffsetY.value = 0; - zoomScale.value = 1; - panTranslateX.value = 0; - panTranslateY.value = 0; - pinchTranslateX.value = 0; - pinchTranslateY.value = 0; + return; } + + totalOffsetX.value = 0; + totalOffsetY.value = 0; + panTranslateX.value = 0; + panTranslateY.value = 0; + pinchTranslateX.value = 0; + pinchTranslateY.value = 0; + zoomScale.value = 1; }); const {singleTap, doubleTap} = useTapGestures({ @@ -105,7 +109,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr panGestureRef, totalOffsetX, totalOffsetY, - pinchScaleOffset, + pinchScale, zoomScale, reset, stopAnimation, @@ -142,9 +146,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr totalOffsetY, pinchTranslateX, pinchTranslateY, - pinchBounceTranslateX, - pinchBounceTranslateY, - pinchScaleOffset, + pinchScale, isSwipingInPager, stopAnimation, onScaleChanged, @@ -173,8 +175,8 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr }, [isActive, mounted, reset]); const animatedStyles = useAnimatedStyle(() => { - const x = pinchTranslateX.value + pinchBounceTranslateX.value + panTranslateX.value + totalOffsetX.value; - const y = pinchTranslateY.value + pinchBounceTranslateY.value + panTranslateY.value + totalOffsetY.value; + const x = pinchTranslateX.value + panTranslateX.value + totalOffsetX.value; + const y = pinchTranslateY.value + panTranslateY.value + totalOffsetY.value; return { transform: [ diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.js index 630ace4e3042..47964aa7bb2e 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.js +++ b/src/components/MultiGestureCanvas/usePinchGesture.js @@ -17,25 +17,32 @@ const usePinchGesture = ({ zoomRange, totalOffsetX, totalOffsetY, - pinchTranslateX, - pinchTranslateY, - pinchBounceTranslateX, - pinchBounceTranslateY, - pinchScaleOffset, + totalPinchTranslateX, + totalPinchTranslateY, + pinchScale, isSwipingInPager, stopAnimation, onScaleChanged, onPinchGestureChange, }) => { const isPinchGestureRunning = useSharedValue(false); - // used to store event scale value when we limit scale - const pinchGestureScale = useSharedValue(1); - // origin of the pinch gesture + + // Used to store event scale value when we limit scale + const currentPinchScale = useSharedValue(1); + + // Origin of the pinch gesture const pinchOrigin = { x: useSharedValue(0), y: useSharedValue(0), }; + const pinchTranslateX = useSharedValue(0); + const pinchTranslateY = useSharedValue(0); + // In order to keep track of the "bounce" effect when pinching over/under the min/max zoom scale + // we need to have extra "bounce" translation variables + const pinchBounceTranslateX = useSharedValue(0); + const pinchBounceTranslateY = useSharedValue(0); + const getAdjustedFocal = useWorkletCallback( (focalX, focalY) => ({ x: focalX - (canvasSize.width / 2 + totalOffsetX.value), @@ -45,7 +52,7 @@ const usePinchGesture = ({ ); const pinchGesture = Gesture.Pinch() .onTouchesDown((evt, state) => { - // we don't want to activate pinch gesture when we are scrolling pager + // We don't want to activate pinch gesture when we are scrolling pager if (!isSwipingInPager.value) { return; } @@ -64,60 +71,70 @@ const usePinchGesture = ({ pinchOrigin.y.value = adjustedFocal.y; }) .onChange((evt) => { - const newZoomScale = pinchScaleOffset.value * evt.scale; + const newZoomScale = pinchScale.value * evt.scale; - // limit zoom scale to zoom range and bounce if we go out of range + // Limit zoom scale to zoom range including bounce range if (zoomScale.value >= zoomRange.min * zoomScaleBounceFactors.min && zoomScale.value <= zoomRange.max * zoomScaleBounceFactors.max) { zoomScale.value = newZoomScale; - pinchGestureScale.value = evt.scale; + currentPinchScale.value = evt.scale; } - // calculate new pinch translation + // Calculate new pinch translation const adjustedFocal = getAdjustedFocal(evt.focalX, evt.focalY); - const newPinchTranslateX = adjustedFocal.x + pinchGestureScale.value * pinchOrigin.x.value * -1; - const newPinchTranslateY = adjustedFocal.y + pinchGestureScale.value * pinchOrigin.y.value * -1; + const newPinchTranslateX = adjustedFocal.x + currentPinchScale.value * pinchOrigin.x.value * -1; + const newPinchTranslateY = adjustedFocal.y + currentPinchScale.value * pinchOrigin.y.value * -1; if (zoomScale.value >= zoomRange.min && zoomScale.value <= zoomRange.max) { pinchTranslateX.value = newPinchTranslateX; pinchTranslateY.value = newPinchTranslateY; } else { - // Store x and y translation that is produced while bouncing to separate variables - // so that we can revert the bounce once pinch gesture is released + // Store x and y translation that is produced while bouncing + // so we can revert the bounce once pinch gesture is released pinchBounceTranslateX.value = newPinchTranslateX - pinchTranslateX.value; pinchBounceTranslateY.value = newPinchTranslateY - pinchTranslateY.value; } + + totalPinchTranslateX.value = pinchTranslateX.value + pinchBounceTranslateX.value; + totalPinchTranslateY.value = pinchTranslateY.value + pinchBounceTranslateY.value; }) .onEnd(() => { // Add pinch translation to total offset - totalOffsetX.value += pinchTranslateX.value; - totalOffsetY.value += pinchTranslateY.value; + totalOffsetX.value += totalPinchTranslateX.value; + totalOffsetY.value += totalPinchTranslateX.value; + // Reset pinch gesture variables pinchTranslateX.value = 0; pinchTranslateY.value = 0; - pinchGestureScale.value = 1; + totalPinchTranslateX.value = 0; + totalPinchTranslateY.value = 0; + currentPinchScale.value = 1; isPinchGestureRunning.value = false; + // If the content was "overzoomed" or "underzoomed", we need to bounce back with an animation + if (pinchBounceTranslateX.value !== 0 || pinchBounceTranslateY.value !== 0) { + pinchBounceTranslateX.value = withSpring(0, SPRING_CONFIG); + pinchBounceTranslateY.value = withSpring(0, SPRING_CONFIG); + } + if (zoomScale.value < zoomRange.min) { - pinchScaleOffset.value = zoomRange.min; + // If the zoom scale is less than the minimum zoom scale, we need to set the zoom scale to the minimum + pinchScale.value = zoomRange.min; zoomScale.value = withSpring(zoomRange.min, SPRING_CONFIG); } else if (zoomScale.value > zoomRange.max) { - pinchScaleOffset.value = zoomRange.max; + // If the zoom scale is higher than the maximum zoom scale, we need to set the zoom scale to the maximum + pinchScale.value = zoomRange.max; zoomScale.value = withSpring(zoomRange.max, SPRING_CONFIG); } else { - pinchScaleOffset.value = zoomScale.value; - } - - if (pinchBounceTranslateX.value !== 0 || pinchBounceTranslateY.value !== 0) { - pinchBounceTranslateX.value = withSpring(0, SPRING_CONFIG); - pinchBounceTranslateY.value = withSpring(0, SPRING_CONFIG); + // Otherwise, we just update the pinch scale offset + pinchScale.value = zoomScale.value; } if (onScaleChanged != null) { - runOnJS(onScaleChanged)(pinchScaleOffset.value); + runOnJS(onScaleChanged)(pinchScale.value); } }); - // Triggers "onPinchGestureChange" callback when pinch scale changes + // The "useAnimatedReaction" triggers a state update to run the "onPinchGestureChange" only once per re-render const [isPinchGestureInUse, setIsPinchGestureInUse] = useState(false); useAnimatedReaction( () => [zoomScale.value, isPinchGestureRunning.value], @@ -128,6 +145,7 @@ const usePinchGesture = ({ } }, ); + // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => onPinchGestureChange(isPinchGestureInUse), [isPinchGestureInUse]); diff --git a/src/components/MultiGestureCanvas/useTapGestures.js b/src/components/MultiGestureCanvas/useTapGestures.js index 0b354ed6c54a..dbba208801e7 100644 --- a/src/components/MultiGestureCanvas/useTapGestures.js +++ b/src/components/MultiGestureCanvas/useTapGestures.js @@ -18,7 +18,7 @@ const useTapGestures = ({ panGestureRef, totalOffsetX, totalOffsetY, - pinchScaleOffset, + pinchScale, zoomScale, reset, stopAnimation, @@ -28,7 +28,7 @@ const useTapGestures = ({ const scaledWidth = useMemo(() => contentSize.width * minContentScale, [contentSize.width, minContentScale]); const scaledHeight = useMemo(() => contentSize.height * minContentScale, [contentSize.height, minContentScale]); - // On double tap zoom to fill, but at least 3x zoom + // On double tap zoom to fill, but at least zoom by 3x const doubleTapScale = useMemo(() => Math.max(DOUBLE_TAP_SCALE, maxContentScale / minContentScale), [maxContentScale, minContentScale]); const zoomToCoordinates = useWorkletCallback( @@ -87,7 +87,7 @@ const useTapGestures = ({ totalOffsetX.value = withSpring(target.x, SPRING_CONFIG); totalOffsetY.value = withSpring(target.y, SPRING_CONFIG); zoomScale.value = withSpring(doubleTapScale, SPRING_CONFIG); - pinchScaleOffset.value = doubleTapScale; + pinchScale.value = doubleTapScale; }, [scaledWidth, scaledHeight, canvasSize, doubleTapScale], ); From 730a9981d6494598e6c3bc50973f38926cdf8e14 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 3 Jan 2024 15:19:36 +0100 Subject: [PATCH 017/107] fix: variable names --- src/components/MultiGestureCanvas/usePinchGesture.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.js index 47964aa7bb2e..01737ec0efb0 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.js +++ b/src/components/MultiGestureCanvas/usePinchGesture.js @@ -17,8 +17,8 @@ const usePinchGesture = ({ zoomRange, totalOffsetX, totalOffsetY, - totalPinchTranslateX, - totalPinchTranslateY, + pinchTranslateX: totalPinchTranslateX, + pinchTranslateY: totalPinchTranslateY, pinchScale, isSwipingInPager, stopAnimation, From 0b205545bf84910dc8538e0c759643d684f61996 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 3 Jan 2024 15:34:14 +0100 Subject: [PATCH 018/107] fix: calculation of total pinch translation --- .../MultiGestureCanvas/usePinchGesture.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.js index 01737ec0efb0..3d37f16e789e 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.js +++ b/src/components/MultiGestureCanvas/usePinchGesture.js @@ -43,6 +43,14 @@ const usePinchGesture = ({ const pinchBounceTranslateX = useSharedValue(0); const pinchBounceTranslateY = useSharedValue(0); + useAnimatedReaction( + () => [pinchTranslateX.value, pinchTranslateY.value, pinchBounceTranslateX.value, pinchBounceTranslateY.value], + ([translateX, translateY, bounceX, bounceY]) => { + totalPinchTranslateX.value = translateX + bounceX; + totalPinchTranslateY.value = translateY + bounceY; + }, + ); + const getAdjustedFocal = useWorkletCallback( (focalX, focalY) => ({ x: focalX - (canvasSize.width / 2 + totalOffsetX.value), @@ -93,9 +101,6 @@ const usePinchGesture = ({ pinchBounceTranslateX.value = newPinchTranslateX - pinchTranslateX.value; pinchBounceTranslateY.value = newPinchTranslateY - pinchTranslateY.value; } - - totalPinchTranslateX.value = pinchTranslateX.value + pinchBounceTranslateX.value; - totalPinchTranslateY.value = pinchTranslateY.value + pinchBounceTranslateY.value; }) .onEnd(() => { // Add pinch translation to total offset @@ -105,8 +110,6 @@ const usePinchGesture = ({ // Reset pinch gesture variables pinchTranslateX.value = 0; pinchTranslateY.value = 0; - totalPinchTranslateX.value = 0; - totalPinchTranslateY.value = 0; currentPinchScale.value = 1; isPinchGestureRunning.value = false; From 9483b8f79878524bca452ec168ec92a4622f86a6 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 3 Jan 2024 16:21:46 +0100 Subject: [PATCH 019/107] rename variables --- src/components/MultiGestureCanvas/index.js | 47 +++++++++---------- .../MultiGestureCanvas/usePanGesture.js | 6 +-- .../MultiGestureCanvas/usePinchGesture.js | 18 +++---- .../MultiGestureCanvas/useTapGestures.js | 20 ++------ 4 files changed, 38 insertions(+), 53 deletions(-) diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index ade0a7b54c38..a1969cb5a904 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -64,14 +64,13 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const pinchTranslateX = useSharedValue(0); const pinchTranslateY = useSharedValue(0); - // Total offset of the canvas - // Contains both offsets from panning and pinching gestures - const totalOffsetX = useSharedValue(0); - const totalOffsetY = useSharedValue(0); + // Total offset of the content including previous translations from panning and pinching gestures + const offsetX = useSharedValue(0); + const offsetY = useSharedValue(0); const stopAnimation = useWorkletCallback(() => { - cancelAnimation(totalOffsetX); - cancelAnimation(totalOffsetY); + cancelAnimation(offsetX); + cancelAnimation(offsetY); }); const reset = useWorkletCallback((animated) => { @@ -82,8 +81,8 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr pinchScale.value = 1; if (animated) { - totalOffsetX.value = withSpring(0, SPRING_CONFIG); - totalOffsetY.value = withSpring(0, SPRING_CONFIG); + offsetX.value = withSpring(0, SPRING_CONFIG); + offsetY.value = withSpring(0, SPRING_CONFIG); panTranslateX.value = withSpring(0, SPRING_CONFIG); panTranslateY.value = withSpring(0, SPRING_CONFIG); pinchTranslateX.value = withSpring(0, SPRING_CONFIG); @@ -92,8 +91,8 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr return; } - totalOffsetX.value = 0; - totalOffsetY.value = 0; + offsetX.value = 0; + offsetY.value = 0; panTranslateX.value = 0; panTranslateY.value = 0; pinchTranslateX.value = 0; @@ -101,14 +100,14 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr zoomScale.value = 1; }); - const {singleTap, doubleTap} = useTapGestures({ + const {singleTapGesture, doubleTapGesture} = useTapGestures({ canvasSize, contentSize, minContentScale, maxContentScale, panGestureRef, - totalOffsetX, - totalOffsetY, + offsetX, + offsetY, pinchScale, zoomScale, reset, @@ -120,15 +119,15 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const panGesture = usePanGesture({ canvasSize, contentSize, + singleTapGesture, + doubleTapGesture, panGestureRef, pagerRef, - singleTap, - doubleTap, zoomScale, zoomRange, totalScale, - totalOffsetX, - totalOffsetY, + offsetX, + offsetY, panTranslateX, panTranslateY, isSwipingInPager, @@ -137,13 +136,13 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const pinchGesture = usePinchGesture({ canvasSize, - singleTap, - doubleTap, + singleTapGesture, + doubleTapGesture, panGesture, zoomScale, zoomRange, - totalOffsetX, - totalOffsetY, + offsetX, + offsetY, pinchTranslateX, pinchTranslateY, pinchScale, @@ -175,8 +174,8 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr }, [isActive, mounted, reset]); const animatedStyles = useAnimatedStyle(() => { - const x = pinchTranslateX.value + panTranslateX.value + totalOffsetX.value; - const y = pinchTranslateY.value + panTranslateY.value + totalOffsetY.value; + const x = pinchTranslateX.value + panTranslateX.value + offsetX.value; + const y = pinchTranslateY.value + panTranslateY.value + offsetY.value; return { transform: [ @@ -202,7 +201,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr }, ]} > - + { stopAnimation(); }) diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.js index 3d37f16e789e..1756365cec88 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.js +++ b/src/components/MultiGestureCanvas/usePinchGesture.js @@ -10,13 +10,13 @@ const useWorkletCallback = MultiGestureCanvasUtils.useWorkletCallback; const usePinchGesture = ({ canvasSize, - singleTap, - doubleTap, + singleTapGesture, + doubleTapGesture, panGesture, zoomScale, zoomRange, - totalOffsetX, - totalOffsetY, + offsetX, + offsetY, pinchTranslateX: totalPinchTranslateX, pinchTranslateY: totalPinchTranslateY, pinchScale, @@ -53,8 +53,8 @@ const usePinchGesture = ({ const getAdjustedFocal = useWorkletCallback( (focalX, focalY) => ({ - x: focalX - (canvasSize.width / 2 + totalOffsetX.value), - y: focalY - (canvasSize.height / 2 + totalOffsetY.value), + x: focalX - (canvasSize.width / 2 + offsetX.value), + y: focalY - (canvasSize.height / 2 + offsetY.value), }), [canvasSize.width, canvasSize.height], ); @@ -67,7 +67,7 @@ const usePinchGesture = ({ state.fail(); }) - .simultaneousWithExternalGesture(panGesture, singleTap, doubleTap) + .simultaneousWithExternalGesture(panGesture, singleTapGesture, doubleTapGesture) .onStart((evt) => { isPinchGestureRunning.value = true; @@ -104,8 +104,8 @@ const usePinchGesture = ({ }) .onEnd(() => { // Add pinch translation to total offset - totalOffsetX.value += totalPinchTranslateX.value; - totalOffsetY.value += totalPinchTranslateX.value; + offsetX.value += totalPinchTranslateX.value; + offsetY.value += totalPinchTranslateX.value; // Reset pinch gesture variables pinchTranslateX.value = 0; diff --git a/src/components/MultiGestureCanvas/useTapGestures.js b/src/components/MultiGestureCanvas/useTapGestures.js index dbba208801e7..c2df4392f96c 100644 --- a/src/components/MultiGestureCanvas/useTapGestures.js +++ b/src/components/MultiGestureCanvas/useTapGestures.js @@ -10,21 +10,7 @@ const clamp = MultiGestureCanvasUtils.clamp; const SPRING_CONFIG = MultiGestureCanvasUtils.SPRING_CONFIG; const useWorkletCallback = MultiGestureCanvasUtils.useWorkletCallback; -const useTapGestures = ({ - canvasSize, - contentSize, - minContentScale, - maxContentScale, - panGestureRef, - totalOffsetX, - totalOffsetY, - pinchScale, - zoomScale, - reset, - stopAnimation, - onScaleChanged, - onTap, -}) => { +const useTapGestures = ({canvasSize, contentSize, minContentScale, maxContentScale, panGestureRef, offsetX, offsetY, pinchScale, zoomScale, reset, stopAnimation, onScaleChanged, onTap}) => { const scaledWidth = useMemo(() => contentSize.width * minContentScale, [contentSize.width, minContentScale]); const scaledHeight = useMemo(() => contentSize.height * minContentScale, [contentSize.height, minContentScale]); @@ -84,8 +70,8 @@ const useTapGestures = ({ target.y = 0; } - totalOffsetX.value = withSpring(target.x, SPRING_CONFIG); - totalOffsetY.value = withSpring(target.y, SPRING_CONFIG); + offsetX.value = withSpring(target.x, SPRING_CONFIG); + offsetY.value = withSpring(target.y, SPRING_CONFIG); zoomScale.value = withSpring(doubleTapScale, SPRING_CONFIG); pinchScale.value = doubleTapScale; }, From 16f2497fb9eaa6cc98be58f5fafd88fb267cb1ec Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 3 Jan 2024 17:22:02 +0100 Subject: [PATCH 020/107] improve comments and remove aliases --- src/components/MultiGestureCanvas/index.js | 25 +++-- .../MultiGestureCanvas/usePanGesture.js | 42 ++++---- .../MultiGestureCanvas/usePinchGesture.js | 19 ++-- .../MultiGestureCanvas/useTapGestures.js | 95 +++++++++++-------- 4 files changed, 92 insertions(+), 89 deletions(-) diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index a1969cb5a904..0964f63913fd 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -12,10 +12,6 @@ import usePinchGesture from './usePinchGesture'; import useTapGestures from './useTapGestures'; import * as MultiGestureCanvasUtils from './utils'; -const SPRING_CONFIG = MultiGestureCanvasUtils.SPRING_CONFIG; -const zoomScaleBounceFactors = MultiGestureCanvasUtils.zoomScaleBounceFactors; -const useWorkletCallback = MultiGestureCanvasUtils.useWorkletCallback; - function getDeepDefaultProps({contentSize: contentSizeProp = {}, zoomRange: zoomRangeProp = {}}) { const contentSize = { width: contentSizeProp.width == null ? 1 : contentSizeProp.width, @@ -68,12 +64,12 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const offsetX = useSharedValue(0); const offsetY = useSharedValue(0); - const stopAnimation = useWorkletCallback(() => { + const stopAnimation = MultiGestureCanvasUtils.useWorkletCallback(() => { cancelAnimation(offsetX); cancelAnimation(offsetY); }); - const reset = useWorkletCallback((animated) => { + const reset = MultiGestureCanvasUtils.useWorkletCallback((animated) => { pinchScale.value = 1; stopAnimation(); @@ -81,13 +77,13 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr pinchScale.value = 1; if (animated) { - offsetX.value = withSpring(0, SPRING_CONFIG); - offsetY.value = withSpring(0, SPRING_CONFIG); - panTranslateX.value = withSpring(0, SPRING_CONFIG); - panTranslateY.value = withSpring(0, SPRING_CONFIG); - pinchTranslateX.value = withSpring(0, SPRING_CONFIG); - pinchTranslateY.value = withSpring(0, SPRING_CONFIG); - zoomScale.value = withSpring(1, SPRING_CONFIG); + offsetX.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); + offsetY.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); + panTranslateX.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); + panTranslateY.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); + pinchTranslateX.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); + pinchTranslateY.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); + zoomScale.value = withSpring(1, MultiGestureCanvasUtils.SPRING_CONFIG); return; } @@ -222,4 +218,5 @@ MultiGestureCanvas.defaultProps = multiGestureCanvasDefaultProps; MultiGestureCanvas.displayName = 'MultiGestureCanvas'; export default MultiGestureCanvas; -export {defaultZoomRange, zoomScaleBounceFactors}; +export {defaultZoomRange}; +export {zoomScaleBounceFactors} from './utils'; diff --git a/src/components/MultiGestureCanvas/usePanGesture.js b/src/components/MultiGestureCanvas/usePanGesture.js index f43b67a0f1f4..f842d1fe4329 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.js +++ b/src/components/MultiGestureCanvas/usePanGesture.js @@ -5,10 +5,6 @@ import * as MultiGestureCanvasUtils from './utils'; const PAN_DECAY_DECELARATION = 0.9915; -const SPRING_CONFIG = MultiGestureCanvasUtils.SPRING_CONFIG; -const clamp = MultiGestureCanvasUtils.clamp; -const useWorkletCallback = MultiGestureCanvasUtils.useWorkletCallback; - const usePanGesture = ({ canvasSize, contentSize, @@ -19,8 +15,8 @@ const usePanGesture = ({ zoomScale, zoomRange, totalScale, - totalOffsetX, - totalOffsetY, + offsetX, + offsetY, panTranslateX, panTranslateY, isSwipingInPager, @@ -40,7 +36,7 @@ const usePanGesture = ({ // Calculates bounds of the scaled content // Can we pan left/right/up/down // Can be used to limit gesture or implementing tension effect - const getBounds = useWorkletCallback(() => { + const getBounds = MultiGestureCanvasUtils.useWorkletCallback(() => { let rightBoundary = 0; let topBoundary = 0; @@ -56,12 +52,12 @@ const usePanGesture = ({ const minVector = {x: -rightBoundary, y: -topBoundary}; const target = { - x: clamp(totalOffsetX.value, minVector.x, maxVector.x), - y: clamp(totalOffsetY.value, minVector.y, maxVector.y), + x: MultiGestureCanvasUtils.clamp(offsetX.value, minVector.x, maxVector.x), + y: MultiGestureCanvasUtils.clamp(offsetY.value, minVector.y, maxVector.y), }; - const isInBoundaryX = target.x === totalOffsetX.value; - const isInBoundaryY = target.y === totalOffsetY.value; + const isInBoundaryX = target.x === offsetX.value; + const isInBoundaryY = target.y === offsetY.value; return { target, @@ -74,24 +70,24 @@ const usePanGesture = ({ }; }, [canvasSize.width, canvasSize.height]); - const returnToBoundaries = useWorkletCallback(() => { + const returnToBoundaries = MultiGestureCanvasUtils.useWorkletCallback(() => { const {target, isInBoundaryX, isInBoundaryY, minVector, maxVector} = getBounds(); - if (zoomScale.value === zoomRange.min && totalOffsetX.value === 0 && totalOffsetY.value === 0 && panTranslateX.value === 0 && panTranslateY.value === 0) { + if (zoomScale.value === zoomRange.min && offsetX.value === 0 && offsetY.value === 0 && panTranslateX.value === 0 && panTranslateY.value === 0) { // We don't need to run any animations return; } // If we are zoomed out, we want to center the content if (zoomScale.value <= zoomRange.min) { - totalOffsetX.value = withSpring(0, SPRING_CONFIG); - totalOffsetY.value = withSpring(0, SPRING_CONFIG); + offsetX.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); + offsetY.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); return; } if (isInBoundaryX) { if (Math.abs(panVelocityX.value) > 0 && zoomScale.value <= zoomRange.max) { - totalOffsetX.value = withDecay({ + offsetX.value = withDecay({ velocity: panVelocityX.value, clamp: [minVector.x, maxVector.x], deceleration: PAN_DECAY_DECELARATION, @@ -99,7 +95,7 @@ const usePanGesture = ({ }); } } else { - totalOffsetX.value = withSpring(target.x, SPRING_CONFIG); + offsetX.value = withSpring(target.x, MultiGestureCanvasUtils.SPRING_CONFIG); } if (isInBoundaryY) { @@ -107,17 +103,17 @@ const usePanGesture = ({ Math.abs(panVelocityY.value) > 0 && zoomScale.value <= zoomRange.max && // Limit vertical panning when content is smaller than screen - totalOffsetY.value !== minVector.y && - totalOffsetY.value !== maxVector.y + offsetY.value !== minVector.y && + offsetY.value !== maxVector.y ) { - totalOffsetY.value = withDecay({ + offsetY.value = withDecay({ velocity: panVelocityY.value, clamp: [minVector.y, maxVector.y], deceleration: PAN_DECAY_DECELARATION, }); } } else { - totalOffsetY.value = withSpring(target.y, SPRING_CONFIG); + offsetY.value = withSpring(target.y, MultiGestureCanvasUtils.SPRING_CONFIG); } }); @@ -157,8 +153,8 @@ const usePanGesture = ({ }) .onEnd(() => { // Add pan translation to total offset - totalOffsetX.value += panTranslateX.value; - totalOffsetY.value += panTranslateY.value; + offsetX.value += panTranslateX.value; + offsetY.value += panTranslateY.value; // Reset pan gesture variables panTranslateX.value = 0; diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.js index 1756365cec88..3f79f8aedbf3 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.js +++ b/src/components/MultiGestureCanvas/usePinchGesture.js @@ -4,10 +4,6 @@ import {Gesture} from 'react-native-gesture-handler'; import {runOnJS, useAnimatedReaction, useSharedValue, withSpring} from 'react-native-reanimated'; import * as MultiGestureCanvasUtils from './utils'; -const SPRING_CONFIG = MultiGestureCanvasUtils.SPRING_CONFIG; -const zoomScaleBounceFactors = MultiGestureCanvasUtils.zoomScaleBounceFactors; -const useWorkletCallback = MultiGestureCanvasUtils.useWorkletCallback; - const usePinchGesture = ({ canvasSize, singleTapGesture, @@ -51,7 +47,7 @@ const usePinchGesture = ({ }, ); - const getAdjustedFocal = useWorkletCallback( + const getAdjustedFocal = MultiGestureCanvasUtils.useWorkletCallback( (focalX, focalY) => ({ x: focalX - (canvasSize.width / 2 + offsetX.value), y: focalY - (canvasSize.height / 2 + offsetY.value), @@ -82,7 +78,10 @@ const usePinchGesture = ({ const newZoomScale = pinchScale.value * evt.scale; // Limit zoom scale to zoom range including bounce range - if (zoomScale.value >= zoomRange.min * zoomScaleBounceFactors.min && zoomScale.value <= zoomRange.max * zoomScaleBounceFactors.max) { + if ( + zoomScale.value >= zoomRange.min * MultiGestureCanvasUtils.zoomScaleBounceFactors.min && + zoomScale.value <= zoomRange.max * MultiGestureCanvasUtils.zoomScaleBounceFactors.max + ) { zoomScale.value = newZoomScale; currentPinchScale.value = evt.scale; } @@ -115,18 +114,18 @@ const usePinchGesture = ({ // If the content was "overzoomed" or "underzoomed", we need to bounce back with an animation if (pinchBounceTranslateX.value !== 0 || pinchBounceTranslateY.value !== 0) { - pinchBounceTranslateX.value = withSpring(0, SPRING_CONFIG); - pinchBounceTranslateY.value = withSpring(0, SPRING_CONFIG); + pinchBounceTranslateX.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); + pinchBounceTranslateY.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); } if (zoomScale.value < zoomRange.min) { // If the zoom scale is less than the minimum zoom scale, we need to set the zoom scale to the minimum pinchScale.value = zoomRange.min; - zoomScale.value = withSpring(zoomRange.min, SPRING_CONFIG); + zoomScale.value = withSpring(zoomRange.min, MultiGestureCanvasUtils.SPRING_CONFIG); } else if (zoomScale.value > zoomRange.max) { // If the zoom scale is higher than the maximum zoom scale, we need to set the zoom scale to the maximum pinchScale.value = zoomRange.max; - zoomScale.value = withSpring(zoomRange.max, SPRING_CONFIG); + zoomScale.value = withSpring(zoomRange.max, MultiGestureCanvasUtils.SPRING_CONFIG); } else { // Otherwise, we just update the pinch scale offset pinchScale.value = zoomScale.value; diff --git a/src/components/MultiGestureCanvas/useTapGestures.js b/src/components/MultiGestureCanvas/useTapGestures.js index c2df4392f96c..3b64b02e56b5 100644 --- a/src/components/MultiGestureCanvas/useTapGestures.js +++ b/src/components/MultiGestureCanvas/useTapGestures.js @@ -6,83 +6,94 @@ import * as MultiGestureCanvasUtils from './utils'; const DOUBLE_TAP_SCALE = 3; -const clamp = MultiGestureCanvasUtils.clamp; -const SPRING_CONFIG = MultiGestureCanvasUtils.SPRING_CONFIG; -const useWorkletCallback = MultiGestureCanvasUtils.useWorkletCallback; - const useTapGestures = ({canvasSize, contentSize, minContentScale, maxContentScale, panGestureRef, offsetX, offsetY, pinchScale, zoomScale, reset, stopAnimation, onScaleChanged, onTap}) => { - const scaledWidth = useMemo(() => contentSize.width * minContentScale, [contentSize.width, minContentScale]); - const scaledHeight = useMemo(() => contentSize.height * minContentScale, [contentSize.height, minContentScale]); + // The content size after scaling it with minimum scale to fit the content into the canvas + const scaledContentWidth = useMemo(() => contentSize.width * minContentScale, [contentSize.width, minContentScale]); + const scaledContentHeight = useMemo(() => contentSize.height * minContentScale, [contentSize.height, minContentScale]); - // On double tap zoom to fill, but at least zoom by 3x + // On double tap the content should be zoomed to fill, but at least zoomed by DOUBLE_TAP_SCALE const doubleTapScale = useMemo(() => Math.max(DOUBLE_TAP_SCALE, maxContentScale / minContentScale), [maxContentScale, minContentScale]); - const zoomToCoordinates = useWorkletCallback( - (canvasFocalX, canvasFocalY) => { + const zoomToCoordinates = MultiGestureCanvasUtils.useWorkletCallback( + (focalX, focalY) => { 'worklet'; stopAnimation(); - const canvasOffsetX = Math.max(0, (canvasSize.width - scaledWidth) / 2); - const canvasOffsetY = Math.max(0, (canvasSize.height - scaledHeight) / 2); + // By how much the canvas is bigger than the content horizontally and vertically per side + const horizontalCanvasOffset = Math.max(0, (canvasSize.width - scaledContentWidth) / 2); + const verticalCanvasOffset = Math.max(0, (canvasSize.height - scaledContentHeight) / 2); - const contentFocal = { - x: clamp(canvasFocalX - canvasOffsetX, 0, scaledWidth), - y: clamp(canvasFocalY - canvasOffsetY, 0, scaledHeight), + // We need to adjust the focal point to take into account the canvas offset + // The focal point cannot be outside of the content's bounds + const adjustedFocalPoint = { + x: MultiGestureCanvasUtils.clamp(focalX - horizontalCanvasOffset, 0, scaledContentWidth), + y: MultiGestureCanvasUtils.clamp(focalY - verticalCanvasOffset, 0, scaledContentHeight), }; + // The center of the canvas const canvasCenter = { x: canvasSize.width / 2, y: canvasSize.height / 2, }; - const originContentCenter = { - x: scaledWidth / 2, - y: scaledHeight / 2, + // The center of the content before zooming + const originalContentCenter = { + x: scaledContentWidth / 2, + y: scaledContentHeight / 2, }; - const targetContentSize = { - width: scaledWidth * doubleTapScale, - height: scaledHeight * doubleTapScale, + // The size of the content after zooming + const zoomedContentSize = { + width: scaledContentWidth * doubleTapScale, + height: scaledContentHeight * doubleTapScale, }; - const targetContentCenter = { - x: targetContentSize.width / 2, - y: targetContentSize.height / 2, + // The center of the zoomed content + const zoomedContentCenter = { + x: zoomedContentSize.width / 2, + y: zoomedContentSize.height / 2, }; - const currentOrigin = { - x: (targetContentCenter.x - canvasCenter.x) * -1, - y: (targetContentCenter.y - canvasCenter.y) * -1, + // By how much the zoomed content is bigger/smaller than the canvas. + const zoomedContentOffset = { + x: zoomedContentCenter.x - canvasCenter.x, + y: zoomedContentCenter.y - canvasCenter.y, }; - const koef = { - x: (1 / originContentCenter.x) * contentFocal.x - 1, - y: (1 / originContentCenter.y) * contentFocal.y - 1, + // How much the content needs to be shifted based on the focal point + const shiftingFactor = { + x: adjustedFocalPoint.x / originalContentCenter.x - 1, + y: adjustedFocalPoint.y / originalContentCenter.y - 1, }; - const target = { - x: currentOrigin.x * koef.x, - y: currentOrigin.y * koef.y, + // The offset after applying the focal point adjusted shift. + // We need to invert the shift, because the content is moving in the opposite direction (* -1) + const offsetAfterZooming = { + x: zoomedContentOffset.x * (shiftingFactor.x * -1), + y: zoomedContentOffset.y * (shiftingFactor.y * -1), }; - if (targetContentSize.height < canvasSize.height) { - target.y = 0; + // If the zoomed content is less tall than the canvas, we need to reset the vertical offset + if (zoomedContentSize.height < canvasSize.height) { + offsetAfterZooming.y = 0; } - offsetX.value = withSpring(target.x, SPRING_CONFIG); - offsetY.value = withSpring(target.y, SPRING_CONFIG); - zoomScale.value = withSpring(doubleTapScale, SPRING_CONFIG); + offsetX.value = withSpring(offsetAfterZooming.x, MultiGestureCanvasUtils.SPRING_CONFIG); + offsetY.value = withSpring(offsetAfterZooming.y, MultiGestureCanvasUtils.SPRING_CONFIG); + zoomScale.value = withSpring(doubleTapScale, MultiGestureCanvasUtils.SPRING_CONFIG); pinchScale.value = doubleTapScale; }, - [scaledWidth, scaledHeight, canvasSize, doubleTapScale], + [scaledContentWidth, scaledContentHeight, canvasSize, doubleTapScale], ); - const doubleTap = Gesture.Tap() + const doubleTapGesture = Gesture.Tap() .numberOfTaps(2) .maxDelay(150) .maxDistance(20) .onEnd((evt) => { + // If the content is already zoomed, we want to reset the zoom, + // otherwwise we want to zoom in if (zoomScale.value > 1) { reset(true); } else { @@ -94,10 +105,10 @@ const useTapGestures = ({canvasSize, contentSize, minContentScale, maxContentSca } }); - const singleTap = Gesture.Tap() + const singleTapGesture = Gesture.Tap() .numberOfTaps(1) .maxDuration(50) - .requireExternalGestureToFail(doubleTap, panGestureRef) + .requireExternalGestureToFail(doubleTapGesture, panGestureRef) .onBegin(() => { stopAnimation(); }) @@ -109,7 +120,7 @@ const useTapGestures = ({canvasSize, contentSize, minContentScale, maxContentSca runOnJS(onTap)(); }); - return {singleTap, doubleTap}; + return {singleTapGesture, doubleTapGesture}; }; export default useTapGestures; From ba75ac922cde2585ecbf3c6037c7c94d34045ec3 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 3 Jan 2024 17:37:38 +0100 Subject: [PATCH 021/107] improve pan gesture code --- .../MultiGestureCanvas/usePanGesture.js | 84 ++++++++----------- 1 file changed, 35 insertions(+), 49 deletions(-) diff --git a/src/components/MultiGestureCanvas/usePanGesture.js b/src/components/MultiGestureCanvas/usePanGesture.js index f842d1fe4329..657b51661145 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.js +++ b/src/components/MultiGestureCanvas/usePanGesture.js @@ -22,12 +22,9 @@ const usePanGesture = ({ isSwipingInPager, stopAnimation, }) => { - // The content size after scaling it with the current (total) zoom value - const zoomScaledContentWidth = useDerivedValue(() => contentSize.width * totalScale.value, [contentSize.width]); - const zoomScaledContentHeight = useDerivedValue(() => contentSize.height * totalScale.value, [contentSize.height]); - - // Used to track previous touch position for the "swipe down to close" gesture - const previousTouch = useSharedValue(null); + // The content size after fitting it to the canvas and zooming + const zoomedContentWidth = useDerivedValue(() => contentSize.width * totalScale.value, [contentSize.width]); + const zoomedContentHeight = useDerivedValue(() => contentSize.height * totalScale.value, [contentSize.height]); // Pan velocity to calculate the decay const panVelocityX = useSharedValue(0); @@ -40,62 +37,57 @@ const usePanGesture = ({ let rightBoundary = 0; let topBoundary = 0; - if (canvasSize.width < zoomScaledContentWidth.value) { - rightBoundary = Math.abs(canvasSize.width - zoomScaledContentWidth.value) / 2; + if (canvasSize.width < zoomedContentWidth.value) { + rightBoundary = Math.abs(canvasSize.width - zoomedContentWidth.value) / 2; } - if (canvasSize.height < zoomScaledContentHeight.value) { - topBoundary = Math.abs(zoomScaledContentHeight.value - canvasSize.height) / 2; + if (canvasSize.height < zoomedContentHeight.value) { + topBoundary = Math.abs(zoomedContentHeight.value - canvasSize.height) / 2; } - const maxVector = {x: rightBoundary, y: topBoundary}; - const minVector = {x: -rightBoundary, y: -topBoundary}; + const minBoundaries = {x: -rightBoundary, y: -topBoundary}; + const maxBoundaries = {x: rightBoundary, y: topBoundary}; - const target = { - x: MultiGestureCanvasUtils.clamp(offsetX.value, minVector.x, maxVector.x), - y: MultiGestureCanvasUtils.clamp(offsetY.value, minVector.y, maxVector.y), + const clampedOffset = { + x: MultiGestureCanvasUtils.clamp(offsetX.value, minBoundaries.x, maxBoundaries.x), + y: MultiGestureCanvasUtils.clamp(offsetY.value, minBoundaries.y, maxBoundaries.y), }; - const isInBoundaryX = target.x === offsetX.value; - const isInBoundaryY = target.y === offsetY.value; + // If the horizontal/vertical offset is the same after clamping to the min/max boundaries, the content is within the boundaries + const isInBoundaryX = clampedOffset.x === offsetX.value; + const isInBoundaryY = clampedOffset.y === offsetY.value; return { - target, + minBoundaries, + maxBoundaries, + clampedOffset, isInBoundaryX, isInBoundaryY, - minVector, - maxVector, - canPanLeft: target.x < maxVector.x, - canPanRight: target.x > minVector.x, }; }, [canvasSize.width, canvasSize.height]); - const returnToBoundaries = MultiGestureCanvasUtils.useWorkletCallback(() => { - const {target, isInBoundaryX, isInBoundaryY, minVector, maxVector} = getBounds(); - - if (zoomScale.value === zoomRange.min && offsetX.value === 0 && offsetY.value === 0 && panTranslateX.value === 0 && panTranslateY.value === 0) { - // We don't need to run any animations + // We want to smoothly gesture by phasing out the pan animation + // In case the content is outside of the boundaries of the canvas, + // we need to return to the view to the boundaries + const finishPanGesture = MultiGestureCanvasUtils.useWorkletCallback(() => { + // If the content is centered within the canvas, we don't need to run any animations + if (offsetX.value === 0 && offsetY.value === 0 && panTranslateX.value === 0 && panTranslateY.value === 0) { return; } - // If we are zoomed out, we want to center the content - if (zoomScale.value <= zoomRange.min) { - offsetX.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); - offsetY.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); - return; - } + const {clampedOffset, isInBoundaryX, isInBoundaryY, minBoundaries, maxBoundaries} = getBounds(); if (isInBoundaryX) { if (Math.abs(panVelocityX.value) > 0 && zoomScale.value <= zoomRange.max) { offsetX.value = withDecay({ velocity: panVelocityX.value, - clamp: [minVector.x, maxVector.x], + clamp: [minBoundaries.x, maxBoundaries.x], deceleration: PAN_DECAY_DECELARATION, rubberBandEffect: false, }); } } else { - offsetX.value = withSpring(target.x, MultiGestureCanvasUtils.SPRING_CONFIG); + offsetX.value = withSpring(clampedOffset.x, MultiGestureCanvasUtils.SPRING_CONFIG); } if (isInBoundaryY) { @@ -103,17 +95,17 @@ const usePanGesture = ({ Math.abs(panVelocityY.value) > 0 && zoomScale.value <= zoomRange.max && // Limit vertical panning when content is smaller than screen - offsetY.value !== minVector.y && - offsetY.value !== maxVector.y + offsetY.value !== minBoundaries.y && + offsetY.value !== maxBoundaries.y ) { offsetY.value = withDecay({ velocity: panVelocityY.value, - clamp: [minVector.y, maxVector.y], + clamp: [minBoundaries.y, maxBoundaries.y], deceleration: PAN_DECAY_DECELARATION, }); } } else { - offsetY.value = withSpring(target.y, MultiGestureCanvasUtils.SPRING_CONFIG); + offsetY.value = withSpring(clampedOffset.y, MultiGestureCanvasUtils.SPRING_CONFIG); } }); @@ -121,16 +113,11 @@ const usePanGesture = ({ .manualActivation(true) .averageTouches(true) .onTouchesMove((evt, state) => { - if (zoomScale.value > 1) { - state.activate(); + if (zoomScale.value <= 1) { + return; } - if (previousTouch.value == null) { - previousTouch.value = { - x: evt.allTouches[0].x, - y: evt.allTouches[0].y, - }; - } + state.activate(); }) .simultaneousWithExternalGesture(pagerRef, singleTapGesture, doubleTapGesture) .onStart(() => { @@ -159,14 +146,13 @@ const usePanGesture = ({ // Reset pan gesture variables panTranslateX.value = 0; panTranslateY.value = 0; - previousTouch.value = null; // If we are swiping (in the pager), we don't want to return to boundaries if (isSwipingInPager.value) { return; } - returnToBoundaries(); + finishPanGesture(); // Reset pan gesture variables panVelocityX.value = 0; From 1de9b257d77ee2ddc6a34674ce5929f4e27e7550 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 3 Jan 2024 17:38:24 +0100 Subject: [PATCH 022/107] fix: pinch gesture --- .../MultiGestureCanvas/usePinchGesture.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.js index 3f79f8aedbf3..ba6d71e87df5 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.js +++ b/src/components/MultiGestureCanvas/usePinchGesture.js @@ -54,7 +54,17 @@ const usePinchGesture = ({ }), [canvasSize.width, canvasSize.height], ); + + const [pinchEnabled, setPinchEnabled] = useState(true); + useEffect(() => { + if (pinchEnabled) { + return; + } + setPinchEnabled(true); + }, [pinchEnabled]); + const pinchGesture = Gesture.Pinch() + .enabled(pinchEnabled) .onTouchesDown((evt, state) => { // We don't want to activate pinch gesture when we are scrolling pager if (!isSwipingInPager.value) { @@ -75,6 +85,11 @@ const usePinchGesture = ({ pinchOrigin.y.value = adjustedFocal.y; }) .onChange((evt) => { + if (evt.numberOfPointers !== 2) { + runOnJS(setPinchEnabled)(false); + return; + } + const newZoomScale = pinchScale.value * evt.scale; // Limit zoom scale to zoom range including bounce range @@ -103,8 +118,8 @@ const usePinchGesture = ({ }) .onEnd(() => { // Add pinch translation to total offset - offsetX.value += totalPinchTranslateX.value; - offsetY.value += totalPinchTranslateX.value; + offsetX.value += pinchTranslateX.value; + offsetY.value += pinchTranslateY.value; // Reset pinch gesture variables pinchTranslateX.value = 0; From 3d93b5e6fcf849e32f2ad18b64218001386f690b Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 3 Jan 2024 18:06:58 +0100 Subject: [PATCH 023/107] improve pan gesture code and remove inter-depdendencies of gestures --- src/components/MultiGestureCanvas/index.js | 17 ++-- .../MultiGestureCanvas/usePanGesture.js | 81 ++++++++----------- .../MultiGestureCanvas/usePinchGesture.js | 4 - .../MultiGestureCanvas/useTapGestures.js | 3 +- src/components/MultiGestureCanvas/utils.ts | 19 +++++ 5 files changed, 59 insertions(+), 65 deletions(-) diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index 0964f63913fd..0577ec79d0d6 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -96,7 +96,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr zoomScale.value = 1; }); - const {singleTapGesture, doubleTapGesture} = useTapGestures({ + const {singleTapGesture: basicSingleTapGesture, doubleTapGesture} = useTapGestures({ canvasSize, contentSize, minContentScale, @@ -111,16 +111,12 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr onScaleChanged, onTap, }); + const singleTapGesture = basicSingleTapGesture.requireExternalGestureToFail(doubleTapGesture, panGestureRef); const panGesture = usePanGesture({ canvasSize, contentSize, - singleTapGesture, - doubleTapGesture, - panGestureRef, - pagerRef, zoomScale, - zoomRange, totalScale, offsetX, offsetY, @@ -128,13 +124,12 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr panTranslateY, isSwipingInPager, stopAnimation, - }); + }) + .simultaneousWithExternalGesture(pagerRef, singleTapGesture, doubleTapGesture) + .withRef(panGestureRef); const pinchGesture = usePinchGesture({ canvasSize, - singleTapGesture, - doubleTapGesture, - panGesture, zoomScale, zoomRange, offsetX, @@ -146,7 +141,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr stopAnimation, onScaleChanged, onPinchGestureChange, - }); + }).simultaneousWithExternalGesture(panGesture, singleTapGesture, doubleTapGesture); // Enables/disables the pager scroll based on the zoom scale // When the content is zoomed in/out, the pager should be disabled diff --git a/src/components/MultiGestureCanvas/usePanGesture.js b/src/components/MultiGestureCanvas/usePanGesture.js index 657b51661145..07498e770aa5 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.js +++ b/src/components/MultiGestureCanvas/usePanGesture.js @@ -5,23 +5,7 @@ import * as MultiGestureCanvasUtils from './utils'; const PAN_DECAY_DECELARATION = 0.9915; -const usePanGesture = ({ - canvasSize, - contentSize, - singleTapGesture, - doubleTapGesture, - panGestureRef, - pagerRef, - zoomScale, - zoomRange, - totalScale, - offsetX, - offsetY, - panTranslateX, - panTranslateY, - isSwipingInPager, - stopAnimation, -}) => { +const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, offsetY, panTranslateX, panTranslateY, isSwipingInPager, stopAnimation}) => { // The content size after fitting it to the canvas and zooming const zoomedContentWidth = useDerivedValue(() => contentSize.width * totalScale.value, [contentSize.width]); const zoomedContentHeight = useDerivedValue(() => contentSize.height * totalScale.value, [contentSize.height]); @@ -34,35 +18,35 @@ const usePanGesture = ({ // Can we pan left/right/up/down // Can be used to limit gesture or implementing tension effect const getBounds = MultiGestureCanvasUtils.useWorkletCallback(() => { - let rightBoundary = 0; - let topBoundary = 0; + let horizontalBoundary = 0; + let verticalBoundary = 0; if (canvasSize.width < zoomedContentWidth.value) { - rightBoundary = Math.abs(canvasSize.width - zoomedContentWidth.value) / 2; + horizontalBoundary = Math.abs(canvasSize.width - zoomedContentWidth.value) / 2; } if (canvasSize.height < zoomedContentHeight.value) { - topBoundary = Math.abs(zoomedContentHeight.value - canvasSize.height) / 2; + verticalBoundary = Math.abs(zoomedContentHeight.value - canvasSize.height) / 2; } - const minBoundaries = {x: -rightBoundary, y: -topBoundary}; - const maxBoundaries = {x: rightBoundary, y: topBoundary}; + const horizontalBoundaries = {min: -horizontalBoundary, max: horizontalBoundary}; + const verticalBoundaries = {min: -verticalBoundary, max: verticalBoundary}; const clampedOffset = { - x: MultiGestureCanvasUtils.clamp(offsetX.value, minBoundaries.x, maxBoundaries.x), - y: MultiGestureCanvasUtils.clamp(offsetY.value, minBoundaries.y, maxBoundaries.y), + x: MultiGestureCanvasUtils.clamp(offsetX.value, horizontalBoundaries.min, horizontalBoundaries.max), + y: MultiGestureCanvasUtils.clamp(offsetY.value, verticalBoundaries.min, verticalBoundaries.max), }; // If the horizontal/vertical offset is the same after clamping to the min/max boundaries, the content is within the boundaries - const isInBoundaryX = clampedOffset.x === offsetX.value; - const isInBoundaryY = clampedOffset.y === offsetY.value; + const isInHoriztontalBoundary = clampedOffset.x === offsetX.value; + const isInVerticalBoundary = clampedOffset.y === offsetY.value; return { - minBoundaries, - maxBoundaries, + horizontalBoundaries, + verticalBoundaries, clampedOffset, - isInBoundaryX, - isInBoundaryY, + isInHoriztontalBoundary, + isInVerticalBoundary, }; }, [canvasSize.width, canvasSize.height]); @@ -75,36 +59,38 @@ const usePanGesture = ({ return; } - const {clampedOffset, isInBoundaryX, isInBoundaryY, minBoundaries, maxBoundaries} = getBounds(); + const {clampedOffset, isInHoriztontalBoundary, isInVerticalBoundary, horizontalBoundaries, verticalBoundaries} = getBounds(); - if (isInBoundaryX) { - if (Math.abs(panVelocityX.value) > 0 && zoomScale.value <= zoomRange.max) { + // If the content is within the horizontal/vertical boundaries of the canvas, we can smoothly phase out the animation + // If not, we need to snap back to the boundaries + if (isInHoriztontalBoundary) { + // If the (absolute) velocity is 0, we don't need to run an animation + if (Math.abs(panVelocityX.value) !== 0) { + // Phase out the pan animation offsetX.value = withDecay({ velocity: panVelocityX.value, - clamp: [minBoundaries.x, maxBoundaries.x], + clamp: [horizontalBoundaries.min, horizontalBoundaries.max], deceleration: PAN_DECAY_DECELARATION, rubberBandEffect: false, }); } } else { + // Animated back to the boundary offsetX.value = withSpring(clampedOffset.x, MultiGestureCanvasUtils.SPRING_CONFIG); } - if (isInBoundaryY) { - if ( - Math.abs(panVelocityY.value) > 0 && - zoomScale.value <= zoomRange.max && - // Limit vertical panning when content is smaller than screen - offsetY.value !== minBoundaries.y && - offsetY.value !== maxBoundaries.y - ) { + if (isInVerticalBoundary) { + // If the (absolute) velocity is 0, we don't need to run an animation + if (Math.abs(panVelocityY.value) !== 0) { + // Phase out the pan animation offsetY.value = withDecay({ velocity: panVelocityY.value, - clamp: [minBoundaries.y, maxBoundaries.y], + clamp: [verticalBoundaries.min, verticalBoundaries.max], deceleration: PAN_DECAY_DECELARATION, }); } } else { + // Animated back to the boundary offsetY.value = withSpring(clampedOffset.y, MultiGestureCanvasUtils.SPRING_CONFIG); } }); @@ -112,14 +98,14 @@ const usePanGesture = ({ const panGesture = Gesture.Pan() .manualActivation(true) .averageTouches(true) - .onTouchesMove((evt, state) => { + .onTouchesMove((_evt, state) => { + // We only allow panning when the content is zoomed in if (zoomScale.value <= 1) { return; } state.activate(); }) - .simultaneousWithExternalGesture(pagerRef, singleTapGesture, doubleTapGesture) .onStart(() => { stopAnimation(); }) @@ -157,8 +143,7 @@ const usePanGesture = ({ // Reset pan gesture variables panVelocityX.value = 0; panVelocityY.value = 0; - }) - .withRef(panGestureRef); + }); return panGesture; }; diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.js index ba6d71e87df5..f40c58955c32 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.js +++ b/src/components/MultiGestureCanvas/usePinchGesture.js @@ -6,9 +6,6 @@ import * as MultiGestureCanvasUtils from './utils'; const usePinchGesture = ({ canvasSize, - singleTapGesture, - doubleTapGesture, - panGesture, zoomScale, zoomRange, offsetX, @@ -73,7 +70,6 @@ const usePinchGesture = ({ state.fail(); }) - .simultaneousWithExternalGesture(panGesture, singleTapGesture, doubleTapGesture) .onStart((evt) => { isPinchGestureRunning.value = true; diff --git a/src/components/MultiGestureCanvas/useTapGestures.js b/src/components/MultiGestureCanvas/useTapGestures.js index 3b64b02e56b5..eefe8c506b33 100644 --- a/src/components/MultiGestureCanvas/useTapGestures.js +++ b/src/components/MultiGestureCanvas/useTapGestures.js @@ -6,7 +6,7 @@ import * as MultiGestureCanvasUtils from './utils'; const DOUBLE_TAP_SCALE = 3; -const useTapGestures = ({canvasSize, contentSize, minContentScale, maxContentScale, panGestureRef, offsetX, offsetY, pinchScale, zoomScale, reset, stopAnimation, onScaleChanged, onTap}) => { +const useTapGestures = ({canvasSize, contentSize, minContentScale, maxContentScale, offsetX, offsetY, pinchScale, zoomScale, reset, stopAnimation, onScaleChanged, onTap}) => { // The content size after scaling it with minimum scale to fit the content into the canvas const scaledContentWidth = useMemo(() => contentSize.width * minContentScale, [contentSize.width, minContentScale]); const scaledContentHeight = useMemo(() => contentSize.height * minContentScale, [contentSize.height, minContentScale]); @@ -108,7 +108,6 @@ const useTapGestures = ({canvasSize, contentSize, minContentScale, maxContentSca const singleTapGesture = Gesture.Tap() .numberOfTaps(1) .maxDuration(50) - .requireExternalGestureToFail(doubleTapGesture, panGestureRef) .onBegin(() => { stopAnimation(); }) diff --git a/src/components/MultiGestureCanvas/utils.ts b/src/components/MultiGestureCanvas/utils.ts index 7a4ba21358c4..5cddd009117a 100644 --- a/src/components/MultiGestureCanvas/utils.ts +++ b/src/components/MultiGestureCanvas/utils.ts @@ -1,22 +1,41 @@ import {useCallback} from 'react'; +// The spring config is used to determine the physics of the spring animation +// Details and a playground for testing different configs can be found at +// https://docs.swmansion.com/react-native-reanimated/docs/animations/withSpring const SPRING_CONFIG = { mass: 1, stiffness: 1000, damping: 500, }; +// The zoom scale bounce factors are used to determine the amount of bounce +// that is allowed when the user zooms more than the min or max zoom levels const zoomScaleBounceFactors = { min: 0.7, max: 1.5, }; +/** + * Clamps a value between a lower and upper bound + * @param value + * @param lowerBound + * @param upperBound + * @returns + */ function clamp(value: number, lowerBound: number, upperBound: number) { 'worklet'; return Math.min(Math.max(lowerBound, value), upperBound); } +/** + * Creates a memoized callback on the UI thread + * Same as `useWorkletCallback` from `react-native-reanimated` but without the deprecation warning + * @param callback + * @param deps + * @returns + */ const useWorkletCallback = (callback: Parameters[0], deps: Parameters[1] = []) => { 'worklet'; From f0c542bef393ad1a612fbbe0eac4d66481246f51 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 3 Jan 2024 18:11:16 +0100 Subject: [PATCH 024/107] improve pan gesture code --- .../MultiGestureCanvas/usePanGesture.js | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/components/MultiGestureCanvas/usePanGesture.js b/src/components/MultiGestureCanvas/usePanGesture.js index 07498e770aa5..8ab2078466de 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.js +++ b/src/components/MultiGestureCanvas/usePanGesture.js @@ -93,6 +93,10 @@ const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, // Animated back to the boundary offsetY.value = withSpring(clampedOffset.y, MultiGestureCanvasUtils.SPRING_CONFIG); } + + // Reset velocity variables after we finished the pan gesture + panVelocityX.value = 0; + panVelocityY.value = 0; }); const panGesture = Gesture.Pan() @@ -100,7 +104,7 @@ const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, .averageTouches(true) .onTouchesMove((_evt, state) => { // We only allow panning when the content is zoomed in - if (zoomScale.value <= 1) { + if (zoomScale.value <= 1 || isSwipingInPager.value) { return; } @@ -111,9 +115,8 @@ const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, }) .onChange((evt) => { // Since we're running both pinch and pan gesture handlers simultaneously, - // we need to make sure that we don't pan when we pinch AND move fingers - // since we track it as pinch focal gesture. - // We also need to prevent panning when we are swiping horizontally (from page to page) + // we need to make sure that we don't pan when we pinch since we track it as pinch focal gesture. + // We also need to prevent panning when we are swiping horizontally in the pager if (evt.numberOfPointers > 1 || isSwipingInPager.value) { return; } @@ -125,11 +128,9 @@ const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, panTranslateY.value += evt.changeY; }) .onEnd(() => { - // Add pan translation to total offset + // Add pan translation to total offset and reset gesture variables offsetX.value += panTranslateX.value; offsetY.value += panTranslateY.value; - - // Reset pan gesture variables panTranslateX.value = 0; panTranslateY.value = 0; @@ -139,10 +140,6 @@ const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, } finishPanGesture(); - - // Reset pan gesture variables - panVelocityX.value = 0; - panVelocityY.value = 0; }); return panGesture; From 58fe25b485ba7b3d6617df2b30757ac5c8368167 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 3 Jan 2024 18:20:38 +0100 Subject: [PATCH 025/107] simplify pinch gesture callback --- .../MultiGestureCanvas/usePinchGesture.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.js index f40c58955c32..2d0b836623a6 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.js +++ b/src/components/MultiGestureCanvas/usePinchGesture.js @@ -18,8 +18,6 @@ const usePinchGesture = ({ onScaleChanged, onPinchGestureChange, }) => { - const isPinchGestureRunning = useSharedValue(false); - // Used to store event scale value when we limit scale const currentPinchScale = useSharedValue(1); @@ -71,8 +69,6 @@ const usePinchGesture = ({ state.fail(); }) .onStart((evt) => { - isPinchGestureRunning.value = true; - stopAnimation(); const adjustedFocal = getAdjustedFocal(evt.focalX, evt.focalY); @@ -121,7 +117,6 @@ const usePinchGesture = ({ pinchTranslateX.value = 0; pinchTranslateY.value = 0; currentPinchScale.value = 1; - isPinchGestureRunning.value = false; // If the content was "overzoomed" or "underzoomed", we need to bounce back with an animation if (pinchBounceTranslateX.value !== 0 || pinchBounceTranslateY.value !== 0) { @@ -148,19 +143,18 @@ const usePinchGesture = ({ }); // The "useAnimatedReaction" triggers a state update to run the "onPinchGestureChange" only once per re-render - const [isPinchGestureInUse, setIsPinchGestureInUse] = useState(false); + const [isPinchGestureRunning, setIsPinchGestureRunning] = useState(false); useAnimatedReaction( () => [zoomScale.value, isPinchGestureRunning.value], - ([zoom, running]) => { - const newIsPinchGestureInUse = zoom !== 1 || running; - if (isPinchGestureInUse !== newIsPinchGestureInUse) { - runOnJS(setIsPinchGestureInUse)(newIsPinchGestureInUse); + ([zoom]) => { + const newIsPinchGestureInUse = zoom !== 1; + if (isPinchGestureRunning !== newIsPinchGestureInUse) { + runOnJS(setIsPinchGestureRunning)(newIsPinchGestureInUse); } }, ); - // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => onPinchGestureChange(isPinchGestureInUse), [isPinchGestureInUse]); + useEffect(() => onPinchGestureChange(isPinchGestureRunning), [isPinchGestureRunning, onPinchGestureChange]); return pinchGesture; }; From 60b6404c3cdabc6b1e23b980770bd9ed57121ef2 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 3 Jan 2024 18:28:40 +0100 Subject: [PATCH 026/107] improve comments --- .../MultiGestureCanvas/usePanGesture.js | 3 +- .../MultiGestureCanvas/usePinchGesture.js | 35 +++++++++++++------ 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/components/MultiGestureCanvas/usePanGesture.js b/src/components/MultiGestureCanvas/usePanGesture.js index 8ab2078466de..4ab872394cb2 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.js +++ b/src/components/MultiGestureCanvas/usePanGesture.js @@ -116,8 +116,7 @@ const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, .onChange((evt) => { // Since we're running both pinch and pan gesture handlers simultaneously, // we need to make sure that we don't pan when we pinch since we track it as pinch focal gesture. - // We also need to prevent panning when we are swiping horizontally in the pager - if (evt.numberOfPointers > 1 || isSwipingInPager.value) { + if (evt.numberOfPointers > 1) { return; } diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.js index 2d0b836623a6..54dd2da7943e 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.js +++ b/src/components/MultiGestureCanvas/usePinchGesture.js @@ -18,7 +18,7 @@ const usePinchGesture = ({ onScaleChanged, onPinchGestureChange, }) => { - // Used to store event scale value when we limit scale + // The current pinch gesture event scale const currentPinchScale = useSharedValue(1); // Origin of the pinch gesture @@ -27,13 +27,18 @@ const usePinchGesture = ({ y: useSharedValue(0), }; + // How much the content is translated during the pinch gesture + // While the pinch gesture is running, the pan gesture is disabled + // Therefore we need to add the translation separately const pinchTranslateX = useSharedValue(0); const pinchTranslateY = useSharedValue(0); - // In order to keep track of the "bounce" effect when pinching over/under the min/max zoom scale + + // In order to keep track of the "bounce" effect when "overzooming"/"underzooming", // we need to have extra "bounce" translation variables const pinchBounceTranslateX = useSharedValue(0); const pinchBounceTranslateY = useSharedValue(0); + // Update the total (pinch) translation based on the regular pinch + bounce useAnimatedReaction( () => [pinchTranslateX.value, pinchTranslateY.value, pinchBounceTranslateX.value, pinchBounceTranslateY.value], ([translateX, translateY, bounceX, bounceY]) => { @@ -42,6 +47,10 @@ const usePinchGesture = ({ }, ); + /** + * Calculates the adjusted focal point of the pinch gesture, + * based on the canvas size and the current offset + */ const getAdjustedFocal = MultiGestureCanvasUtils.useWorkletCallback( (focalX, focalY) => ({ x: focalX - (canvasSize.width / 2 + offsetX.value), @@ -50,6 +59,8 @@ const usePinchGesture = ({ [canvasSize.width, canvasSize.height], ); + // The pinch gesture is disabled when we release one of the fingers + // On the next render, we need to re-enable the pinch gesture const [pinchEnabled, setPinchEnabled] = useState(true); useEffect(() => { if (pinchEnabled) { @@ -60,8 +71,8 @@ const usePinchGesture = ({ const pinchGesture = Gesture.Pinch() .enabled(pinchEnabled) - .onTouchesDown((evt, state) => { - // We don't want to activate pinch gesture when we are scrolling pager + .onTouchesDown((_evt, state) => { + // We don't want to activate pinch gesture when we are swiping in the pager if (!isSwipingInPager.value) { return; } @@ -71,12 +82,14 @@ const usePinchGesture = ({ .onStart((evt) => { stopAnimation(); + // Set the origin focal point of the pinch gesture at the start of the gesture const adjustedFocal = getAdjustedFocal(evt.focalX, evt.focalY); - pinchOrigin.x.value = adjustedFocal.x; pinchOrigin.y.value = adjustedFocal.y; }) .onChange((evt) => { + // Disable the pinch gesture if one finger is released, + // to prevent the content from shaking/jumping if (evt.numberOfPointers !== 2) { runOnJS(setPinchEnabled)(false); return; @@ -84,7 +97,7 @@ const usePinchGesture = ({ const newZoomScale = pinchScale.value * evt.scale; - // Limit zoom scale to zoom range including bounce range + // Limit the zoom scale to zoom range including bounce range if ( zoomScale.value >= zoomRange.min * MultiGestureCanvasUtils.zoomScaleBounceFactors.min && zoomScale.value <= zoomRange.max * MultiGestureCanvasUtils.zoomScaleBounceFactors.max @@ -98,6 +111,8 @@ const usePinchGesture = ({ const newPinchTranslateX = adjustedFocal.x + currentPinchScale.value * pinchOrigin.x.value * -1; const newPinchTranslateY = adjustedFocal.y + currentPinchScale.value * pinchOrigin.y.value * -1; + // If the zoom scale is within the zoom range, we perform the regular pinch translation + // Otherwise it means that we are "overzoomed" or "underzoomed", so we need to bounce back if (zoomScale.value >= zoomRange.min && zoomScale.value <= zoomRange.max) { pinchTranslateX.value = newPinchTranslateX; pinchTranslateY.value = newPinchTranslateY; @@ -109,11 +124,9 @@ const usePinchGesture = ({ } }) .onEnd(() => { - // Add pinch translation to total offset + // Add pinch translation to total offset and reset gesture variables offsetX.value += pinchTranslateX.value; offsetY.value += pinchTranslateY.value; - - // Reset pinch gesture variables pinchTranslateX.value = 0; pinchTranslateY.value = 0; currentPinchScale.value = 1; @@ -142,7 +155,8 @@ const usePinchGesture = ({ } }); - // The "useAnimatedReaction" triggers a state update to run the "onPinchGestureChange" only once per re-render + // The "useAnimatedReaction" triggers a state update only when the value changed, + // which then triggers the "onPinchGestureChange" callback const [isPinchGestureRunning, setIsPinchGestureRunning] = useState(false); useAnimatedReaction( () => [zoomScale.value, isPinchGestureRunning.value], @@ -153,7 +167,6 @@ const usePinchGesture = ({ } }, ); - useEffect(() => onPinchGestureChange(isPinchGestureRunning), [isPinchGestureRunning, onPinchGestureChange]); return pinchGesture; From b1b592a60642b6bf226de80a524f93554577c832 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 3 Jan 2024 18:35:10 +0100 Subject: [PATCH 027/107] add more comments --- src/components/MultiGestureCanvas/index.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index 0577ec79d0d6..128f1100b338 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -34,6 +34,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); const pagerRefFallback = useRef(null); + // If the MultiGestureCanvas used inside a AttachmentCarouselPager, we need to adapt the behaviour based on the pager state const {onTap, pagerRef, shouldPagerScroll, isSwipingInPager, onPinchGestureChange} = attachmentCarouselPagerContext || { onTap: () => undefined, onPinchGestureChange: () => undefined, @@ -43,6 +44,9 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr ...props, }; + // Based on the (original) content size and the canvas size, we calculate the horizontal and vertical scale factors + // to fit the content inside the canvas + // We later use the lower of the two scale factors to fit the content inside the canvas const {minScale: minContentScale, maxScale: maxContentScale} = useMemo(() => getCanvasFitScale({canvasSize, contentSize}), [canvasSize, contentSize]); const zoomScale = useSharedValue(1); @@ -64,11 +68,17 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const offsetX = useSharedValue(0); const offsetY = useSharedValue(0); + /** + * Stops any currently running decay animation from panning + */ const stopAnimation = MultiGestureCanvasUtils.useWorkletCallback(() => { cancelAnimation(offsetX); cancelAnimation(offsetY); }); + /** + * Resets the canvas to the initial state and animates back smoothly + */ const reset = MultiGestureCanvasUtils.useWorkletCallback((animated) => { pinchScale.value = 1; @@ -152,6 +162,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr }, ); + // Trigger a reset when the canvas gets inactive, but only if it was already mounted before const mounted = useRef(false); useEffect(() => { if (!mounted.current) { @@ -164,6 +175,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr } }, [isActive, mounted, reset]); + // Animate the x and y position of the content within the canvas based on all of the gestures const animatedStyles = useAnimatedStyle(() => { const x = pinchTranslateX.value + panTranslateX.value + offsetX.value; const y = pinchTranslateY.value + panTranslateY.value + offsetY.value; From 8856caf3e53be49289c34476d3c780d713cacbd5 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 4 Jan 2024 00:05:56 +0100 Subject: [PATCH 028/107] fix: eslint --- .../Attachments/AttachmentCarousel/Pager/index.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index 693c9b86fae9..2f86bb98f796 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -1,8 +1,11 @@ import React, {useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; -import {createNativeWrapper, NativeViewGestureHandlerProps} from 'react-native-gesture-handler'; -import PagerView, {PagerViewProps} from 'react-native-pager-view'; -import Animated, {AnimatedProps, runOnJS, useAnimatedProps, useAnimatedReaction, useSharedValue} from 'react-native-reanimated'; +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 type {AnimatedProps} from 'react-native-reanimated'; +import Animated, {runOnJS, useAnimatedProps, useAnimatedReaction, useSharedValue} from 'react-native-reanimated'; import useThemeStyles from '@hooks/useThemeStyles'; import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext'; import usePageScrollHandler from './usePageScrollHandler'; From b6bbdece8beda8ea9cc4bea91d81f3fb29b30028 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 4 Jan 2024 00:10:04 +0100 Subject: [PATCH 029/107] fix: eslint --- .../Pager/AttachmentCarouselPagerContext.ts | 4 ++-- .../AttachmentCarousel/Pager/usePageScrollHandler.ts | 2 +- src/components/Lightbox.js | 7 ++++--- src/components/MultiGestureCanvas/index.tsx | 7 ++++--- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts index 85f3e6adbda5..846cafb7d443 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -1,6 +1,6 @@ import {createContext} from 'react'; -import PagerView from 'react-native-pager-view'; -import {SharedValue} from 'react-native-reanimated'; +import type PagerView from 'react-native-pager-view'; +import type {SharedValue} from 'react-native-reanimated'; type AttachmentCarouselPagerContextType = { onTap: () => void; diff --git a/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts b/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts index 9841129d036c..bcc616883d72 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts @@ -1,4 +1,4 @@ -import {PagerViewProps} from 'react-native-pager-view'; +import type {PagerViewProps} from 'react-native-pager-view'; import {useEvent, useHandler} from 'react-native-reanimated'; type PageScrollHandler = NonNullable; diff --git a/src/components/Lightbox.js b/src/components/Lightbox.js index c78a3569d73a..64d6e25c9c36 100644 --- a/src/components/Lightbox.js +++ b/src/components/Lightbox.js @@ -6,7 +6,6 @@ import useStyleUtils from '@hooks/useStyleUtils'; import * as AttachmentsPropTypes from './Attachments/propTypes'; import Image from './Image'; import MultiGestureCanvas from './MultiGestureCanvas'; -import {zoomRangeDefaultProps, zoomRangePropTypes} from './MultiGestureCanvas/propTypes'; import getCanvasFitScale from './MultiGestureCanvas/utils'; // Increase/decrease this number to change the number of concurrent lightboxes @@ -20,7 +19,8 @@ const cachedDimensions = new Map(); * On the native layer, we use a image library to handle zoom functionality */ const propTypes = { - ...zoomRangePropTypes, + // TODO: Add TS types for zoom range + // ...zoomRangePropTypes, /** Function for handle on press */ onPress: PropTypes.func, @@ -48,7 +48,8 @@ const propTypes = { }; const defaultProps = { - ...zoomRangeDefaultProps, + // TODO: Add TS default values + // ...zoomRangeDefaultProps, isAuthTokenRequired: false, index: 0, diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index c3760434fa97..21624f235092 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -1,14 +1,15 @@ import React, {useContext, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; -import PagerView from 'react-native-pager-view'; +import type PagerView from 'react-native-pager-view'; import Animated, {cancelAnimation, runOnUI, useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, withSpring} from 'react-native-reanimated'; -import AttachmentCarouselPagerContext, {AttachmentCarouselPagerContextType} from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; +import type {AttachmentCarouselPagerContextType} from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; +import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import {defaultZoomRange} from './constants'; import getCanvasFitScale from './getCanvasFitScale'; -import {ContentSizeProp, ZoomRangeProp} from './types'; +import type {ContentSizeProp, ZoomRangeProp} from './types'; import usePanGesture from './usePanGesture'; import usePinchGesture from './usePinchGesture'; import useTapGestures from './useTapGestures'; From f8445e493b80c57c660bf22345c44e49f3533e8d Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 4 Jan 2024 16:36:18 +0100 Subject: [PATCH 030/107] remove pager ref --- src/components/MultiGestureCanvas/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index 21624f235092..50b68665556b 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -174,7 +174,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr isSwipingInPager, stopAnimation, }) - .simultaneousWithExternalGesture(pagerRef, singleTapGesture, doubleTapGesture) + .simultaneousWithExternalGesture(singleTapGesture, doubleTapGesture) .withRef(panGestureRef); const pinchGesture = usePinchGesture({ From 937b4b16cbc8c769e0a8b3016face74b0554777a Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 4 Jan 2024 16:50:34 +0100 Subject: [PATCH 031/107] update comments --- src/components/MultiGestureCanvas/usePanGesture.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/MultiGestureCanvas/usePanGesture.js b/src/components/MultiGestureCanvas/usePanGesture.js index 4ab872394cb2..aec24cb2e99e 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.js +++ b/src/components/MultiGestureCanvas/usePanGesture.js @@ -3,6 +3,9 @@ import {Gesture} from 'react-native-gesture-handler'; import {useDerivedValue, useSharedValue, withDecay, withSpring} from 'react-native-reanimated'; import * as MultiGestureCanvasUtils from './utils'; +// This value determines how fast the pan animation should phase out +// We're using a "withDecay" animation to smoothly phase out the pan animation +// https://docs.swmansion.com/react-native-reanimated/docs/animations/withDecay/ const PAN_DECAY_DECELARATION = 0.9915; const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, offsetY, panTranslateX, panTranslateY, isSwipingInPager, stopAnimation}) => { @@ -10,7 +13,8 @@ const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, const zoomedContentWidth = useDerivedValue(() => contentSize.width * totalScale.value, [contentSize.width]); const zoomedContentHeight = useDerivedValue(() => contentSize.height * totalScale.value, [contentSize.height]); - // Pan velocity to calculate the decay + // Velocity of the pan gesture + // We need to keep track of the velocity to properly phase out/decay the pan animation const panVelocityX = useSharedValue(0); const panVelocityY = useSharedValue(0); @@ -50,9 +54,9 @@ const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, }; }, [canvasSize.width, canvasSize.height]); - // We want to smoothly gesture by phasing out the pan animation + // We want to smoothly decay/end the gesture by phasing out the pan animation // In case the content is outside of the boundaries of the canvas, - // we need to return to the view to the boundaries + // we need to move the content back into the boundaries const finishPanGesture = MultiGestureCanvasUtils.useWorkletCallback(() => { // If the content is centered within the canvas, we don't need to run any animations if (offsetX.value === 0 && offsetY.value === 0 && panTranslateX.value === 0 && panTranslateY.value === 0) { From 7e148eefe0fe3e1e8502b3a1476e88574753eef5 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 4 Jan 2024 16:56:51 +0100 Subject: [PATCH 032/107] improve lightbox styles --- src/components/Lightbox.js | 8 +++++++- src/styles/utils/index.ts | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/Lightbox.js b/src/components/Lightbox.js index 0b09ed1e745a..1510f38eea20 100644 --- a/src/components/Lightbox.js +++ b/src/components/Lightbox.js @@ -93,6 +93,12 @@ function Lightbox({isAuthTokenRequired, source, onScaleChanged, onPress, onError }, [activeIndex, index]); const isLightboxVisible = isLightboxInRange && (isActive || isLightboxLoaded || isFallbackLoaded); + // If the fallback image is currently visible, we want to hide the Lightbox until the fallback gets hidden, + // so that we don't see two overlapping images at the same time. + // If there the Lightbox is not used within a carousel, we don't need to hide the Lightbox, + // because it's only going to be rendered after the fallback image is hidden. + const shouldHideLightbox = hasSiblingCarouselItems && isFallbackVisible; + const isLoading = isActive && (!isContainerLoaded || !isImageLoaded); const updateCanvasSize = useCallback( @@ -168,7 +174,7 @@ function Lightbox({isAuthTokenRequired, source, onScaleChanged, onPress, onError {isContainerLoaded && ( <> {isLightboxVisible && ( - + ({ }, getFullscreenCenteredContentStyles: () => [StyleSheet.absoluteFill, styles.justifyContentCenter, styles.alignItemsCenter], + + getLightboxVisibilityStyle: (isHidden: boolean) => ({opacity: isHidden ? 0 : 1}), }); type StyleUtilsType = ReturnType; From 9b6c8fef5b118e039f1850eb6841f1c1c6f110ce Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 4 Jan 2024 18:45:22 +0100 Subject: [PATCH 033/107] remove onPinchGestureChange callback --- .../AttachmentCarousel/Pager/index.js | 14 +++---- src/components/Lightbox.js | 4 ++ src/components/MultiGestureCanvas/index.js | 40 +++++++++++++------ 3 files changed, 39 insertions(+), 19 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.js b/src/components/Attachments/AttachmentCarousel/Pager/index.js index d7d6bda1be29..e0f652e47e4c 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.js +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.js @@ -40,7 +40,7 @@ const pagerPropTypes = { initialIndex: PropTypes.number, onPageSelected: PropTypes.func, onTap: PropTypes.func, - onPinchGestureChange: PropTypes.func, + onScaleChanged: PropTypes.func, forwardedRef: refPropTypes, }; @@ -48,11 +48,11 @@ const pagerDefaultProps = { initialIndex: 0, onPageSelected: () => {}, onTap: () => {}, - onPinchGestureChange: () => {}, + onScaleChanged: () => {}, forwardedRef: null, }; -function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelected, onTap, onPinchGestureChange, forwardedRef}) { +function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelected, onTap, onScaleChanged, forwardedRef}) { const styles = useThemeStyles(); const shouldPagerScroll = useSharedValue(true); const pagerRef = useRef(null); @@ -106,13 +106,13 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte const contextValue = useMemo( () => ({ - isSwipingInPager, + onTap, + onScaleChanged, pagerRef, shouldPagerScroll, - onPinchGestureChange, - onTap, + isSwipingInPager, }), - [isSwipingInPager, pagerRef, shouldPagerScroll, onPinchGestureChange, onTap], + [isSwipingInPager, shouldPagerScroll, onScaleChanged, onTap], ); return ( diff --git a/src/components/Lightbox.js b/src/components/Lightbox.js index 1510f38eea20..366759cc6cd7 100644 --- a/src/components/Lightbox.js +++ b/src/components/Lightbox.js @@ -22,6 +22,9 @@ const cachedDimensions = new Map(); const propTypes = { ...zoomRangePropTypes, + /** Triggers whenever the zoom scale changes */ + onScaleChanged: PropTypes.func, + /** Function for handle on press */ onPress: PropTypes.func, @@ -54,6 +57,7 @@ const defaultProps = { index: 0, activeIndex: 0, hasSiblingCarouselItems: false, + onScaleChanged: () => {}, onPress: () => {}, onError: () => {}, style: {}, diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index 128f1100b338..ed0b0db38124 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -26,23 +26,40 @@ function getDeepDefaultProps({contentSize: contentSizeProp = {}, zoomRange: zoom return {contentSize, zoomRange}; } -function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, children, ...props}) { +function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged: onScaleChangedProp, children, ...props}) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {contentSize, zoomRange} = getDeepDefaultProps(props); - const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); - const pagerRefFallback = useRef(null); + // If the MultiGestureCanvas used inside a AttachmentCarouselPager, we need to adapt the behaviour based on the pager state - const {onTap, pagerRef, shouldPagerScroll, isSwipingInPager, onPinchGestureChange} = attachmentCarouselPagerContext || { - onTap: () => undefined, - onPinchGestureChange: () => undefined, - pagerRef: pagerRefFallback, - shouldPagerScroll: false, - isSwipingInPager: false, - ...props, - }; + const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); + const { + onTap, + onScaleChanged: onScaleChangedContext, + pagerRef, + shouldPagerScroll, + isSwipingInPager, + } = useMemo( + () => + attachmentCarouselPagerContext || { + onTap: () => {}, + onScaleChanged: () => {}, + pagerRef: pagerRefFallback, + shouldPagerScroll: false, + isSwipingInPager: false, + }, + [attachmentCarouselPagerContext], + ); + + const onScaleChanged = useMemo( + (newScale) => { + onScaleChangedProp(newScale); + onScaleChangedContext(newScale); + }, + [onScaleChangedContext, onScaleChangedProp], + ); // Based on the (original) content size and the canvas size, we calculate the horizontal and vertical scale factors // to fit the content inside the canvas @@ -150,7 +167,6 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr isSwipingInPager, stopAnimation, onScaleChanged, - onPinchGestureChange, }).simultaneousWithExternalGesture(panGesture, singleTapGesture, doubleTapGesture); // Enables/disables the pager scroll based on the zoom scale From 351eb53407a240a406e73c8737aca4a8c863a492 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 4 Jan 2024 18:49:43 +0100 Subject: [PATCH 034/107] fix: carousel arrows --- .../AttachmentCarousel/index.native.js | 25 ++++++++----- src/components/MultiGestureCanvas/index.js | 4 +-- .../MultiGestureCanvas/usePinchGesture.js | 36 ++++++++----------- 3 files changed, 34 insertions(+), 31 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js index 003c27844fbc..8f168093c217 100644 --- a/src/components/Attachments/AttachmentCarousel/index.native.js +++ b/src/components/Attachments/AttachmentCarousel/index.native.js @@ -23,7 +23,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, const pagerRef = useRef(null); const [page, setPage] = useState(); const [attachments, setAttachments] = useState([]); - const [isPinchGestureRunning, setIsPinchGestureRunning] = useState(true); + const [isZoomedOut, setIsZoomedOut] = useState(true); const [shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows] = useCarouselArrows(); const [activeSource, setActiveSource] = useState(source); @@ -107,6 +107,20 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, [activeSource, attachments.length, page, setShouldShowArrows, shouldShowArrows], ); + const handleScaleChange = useCallback( + (newScale) => { + const newIsZoomedOut = newScale === 1; + + if (isZoomedOut === newIsZoomedOut) { + return; + } + + setIsZoomedOut(newIsZoomedOut); + setShouldShowArrows(newIsZoomedOut); + }, + [isZoomedOut, setShouldShowArrows], + ); + return ( cycleThroughAttachments(-1)} @@ -141,12 +155,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, renderItem={renderItem} initialIndex={page} onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)} - onPinchGestureChange={(newIsPinchGestureRunning) => { - setIsPinchGestureRunning(newIsPinchGestureRunning); - if (!newIsPinchGestureRunning && !shouldShowArrows) { - setShouldShowArrows(true); - } - }} + onScaleChanged={handleScaleChange} ref={pagerRef} /> diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index ed0b0db38124..8c455836300d 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -1,4 +1,4 @@ -import React, {useContext, useEffect, useMemo, useRef} from 'react'; +import React, {useCallback, useContext, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; import Animated, {cancelAnimation, runOnUI, useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, withSpring} from 'react-native-reanimated'; @@ -53,7 +53,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged: onScal [attachmentCarouselPagerContext], ); - const onScaleChanged = useMemo( + const onScaleChanged = useCallback( (newScale) => { onScaleChangedProp(newScale); onScaleChangedContext(newScale); diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.js index 54dd2da7943e..21c5e55e0117 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.js +++ b/src/components/MultiGestureCanvas/usePinchGesture.js @@ -16,7 +16,6 @@ const usePinchGesture = ({ isSwipingInPager, stopAnimation, onScaleChanged, - onPinchGestureChange, }) => { // The current pinch gesture event scale const currentPinchScale = useSharedValue(1); @@ -104,6 +103,10 @@ const usePinchGesture = ({ ) { zoomScale.value = newZoomScale; currentPinchScale.value = evt.scale; + + if (onScaleChanged != null) { + runOnJS(onScaleChanged)(zoomScale.value); + } } // Calculate new pinch translation @@ -137,38 +140,29 @@ const usePinchGesture = ({ pinchBounceTranslateY.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); } + const triggerScaleChangeCallback = () => { + if (onScaleChanged == null) { + return; + } + + runOnJS(onScaleChanged)(zoomScale.value); + }; + if (zoomScale.value < zoomRange.min) { // If the zoom scale is less than the minimum zoom scale, we need to set the zoom scale to the minimum pinchScale.value = zoomRange.min; - zoomScale.value = withSpring(zoomRange.min, MultiGestureCanvasUtils.SPRING_CONFIG); + zoomScale.value = withSpring(zoomRange.min, MultiGestureCanvasUtils.SPRING_CONFIG, triggerScaleChangeCallback); } else if (zoomScale.value > zoomRange.max) { // If the zoom scale is higher than the maximum zoom scale, we need to set the zoom scale to the maximum pinchScale.value = zoomRange.max; - zoomScale.value = withSpring(zoomRange.max, MultiGestureCanvasUtils.SPRING_CONFIG); + zoomScale.value = withSpring(zoomRange.max, MultiGestureCanvasUtils.SPRING_CONFIG, triggerScaleChangeCallback); } else { // Otherwise, we just update the pinch scale offset pinchScale.value = zoomScale.value; - } - - if (onScaleChanged != null) { - runOnJS(onScaleChanged)(pinchScale.value); + triggerScaleChangeCallback(); } }); - // The "useAnimatedReaction" triggers a state update only when the value changed, - // which then triggers the "onPinchGestureChange" callback - const [isPinchGestureRunning, setIsPinchGestureRunning] = useState(false); - useAnimatedReaction( - () => [zoomScale.value, isPinchGestureRunning.value], - ([zoom]) => { - const newIsPinchGestureInUse = zoom !== 1; - if (isPinchGestureRunning !== newIsPinchGestureInUse) { - runOnJS(setIsPinchGestureRunning)(newIsPinchGestureInUse); - } - }, - ); - useEffect(() => onPinchGestureChange(isPinchGestureRunning), [isPinchGestureRunning, onPinchGestureChange]); - return pinchGesture; }; From fcfa05997786695e6d74995a1c84b237ee9f1889 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 5 Jan 2024 17:32:19 +0100 Subject: [PATCH 035/107] migrate hooks --- src/components/MultiGestureCanvas/index.tsx | 37 ++---------- src/components/MultiGestureCanvas/types.ts | 58 ++++++++++++++++++- .../{usePanGesture.js => usePanGesture.ts} | 18 +++++- ...{usePinchGesture.js => usePinchGesture.ts} | 21 ++++++- .../{useTapGestures.js => useTapGestures.ts} | 35 ++++++++++- src/components/MultiGestureCanvas/utils.ts | 8 ++- 6 files changed, 135 insertions(+), 42 deletions(-) rename src/components/MultiGestureCanvas/{usePanGesture.js => usePanGesture.ts} (88%) rename src/components/MultiGestureCanvas/{usePinchGesture.js => usePinchGesture.ts} (88%) rename src/components/MultiGestureCanvas/{useTapGestures.js => useTapGestures.ts} (80%) diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index 7759c5c9b018..efe75f21af96 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -8,46 +8,19 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import {defaultZoomRange} from './constants'; import getCanvasFitScale from './getCanvasFitScale'; -import type {ContentSizeProp, ZoomRangeProp} from './types'; +import type {ContentSize, MultiGestureCanvasProps, ZoomRange} from './types'; import usePanGesture from './usePanGesture'; import usePinchGesture from './usePinchGesture'; import useTapGestures from './useTapGestures'; import * as MultiGestureCanvasUtils from './utils'; -type MultiGestureCanvasProps = React.PropsWithChildren<{ - /** - * Wheter the canvas is currently active (in the screen) or not. - * Disables certain gestures and functionality - */ - isActive: boolean; - - /** Handles scale changed event */ - onScaleChanged: (zoomScale: number) => void; - - /** The width and height of the canvas. - * This is needed in order to properly scale the content in the canvas - */ - canvasSize: { - width: number; - height: number; - }; - - /** The width and height of the content. - * This is needed in order to properly scale the content in the canvas - */ - contentSize: ContentSizeProp; - - /** Range of zoom that can be applied to the content by pinching or double tapping. */ - zoomRange?: ZoomRangeProp; -}>; - type Props = { - contentSize?: ContentSizeProp; - zoomRange?: ZoomRangeProp; + contentSize?: ContentSize; + zoomRange?: ZoomRange; }; type PropsWithDefault = { - contentSize: ContentSizeProp; - zoomRange: Required; + contentSize: ContentSize; + zoomRange: Required; }; const getDeepDefaultProps = ({contentSize: contentSizeProp, zoomRange: zoomRangeProp}: Props): PropsWithDefault => { const contentSize = { diff --git a/src/components/MultiGestureCanvas/types.ts b/src/components/MultiGestureCanvas/types.ts index 11dfc767aacf..4ca0f5a1fe05 100644 --- a/src/components/MultiGestureCanvas/types.ts +++ b/src/components/MultiGestureCanvas/types.ts @@ -1,11 +1,63 @@ -type ContentSizeProp = { +import type {SharedValue} from 'react-native-reanimated'; +import type {WorkletFunction} from 'react-native-reanimated/lib/typescript/reanimated2/commonTypes'; + +type CanvasSize = { width: number; height: number; }; -type ZoomRangeProp = { +type ContentSize = { + width: number; + height: number; +}; + +type ZoomRange = { min?: number; max?: number; }; -export type {ContentSizeProp, ZoomRangeProp}; +type OnScaleChangedCallback = (zoomScale: number) => void; + +type MultiGestureCanvasProps = React.PropsWithChildren<{ + /** + * Wheter the canvas is currently active (in the screen) or not. + * Disables certain gestures and functionality + */ + isActive: boolean; + + /** Handles scale changed event */ + onScaleChanged: OnScaleChangedCallback; + + /** The width and height of the canvas. + * This is needed in order to properly scale the content in the canvas + */ + canvasSize: CanvasSize; + + /** The width and height of the content. + * This is needed in order to properly scale the content in the canvas + */ + contentSize: ContentSize; + + /** Range of zoom that can be applied to the content by pinching or double tapping. */ + zoomRange?: ZoomRange; +}>; + +type MultiGestureCanvasVariables = { + minContentScale: number; + maxContentScale: number; + isSwipingInPager: SharedValue; + zoomScale: SharedValue; + totalScale: SharedValue; + pinchScale: SharedValue; + offsetX: SharedValue; + offsetY: SharedValue; + panTranslateX: SharedValue; + panTranslateY: SharedValue; + pinchTranslateX: SharedValue; + pinchTranslateY: SharedValue; + stopAnimation: WorkletFunction<[], void>; + reset: WorkletFunction<[boolean], void>; + onTap: () => void; +}; + +export type {MultiGestureCanvasProps, CanvasSize, ContentSize, ZoomRange, OnScaleChangedCallback, MultiGestureCanvasVariables}; diff --git a/src/components/MultiGestureCanvas/usePanGesture.js b/src/components/MultiGestureCanvas/usePanGesture.ts similarity index 88% rename from src/components/MultiGestureCanvas/usePanGesture.js rename to src/components/MultiGestureCanvas/usePanGesture.ts index aec24cb2e99e..7e0aff08368f 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.js +++ b/src/components/MultiGestureCanvas/usePanGesture.ts @@ -1,6 +1,8 @@ /* eslint-disable no-param-reassign */ +import type {PanGesture} from 'react-native-gesture-handler'; import {Gesture} from 'react-native-gesture-handler'; import {useDerivedValue, useSharedValue, withDecay, withSpring} from 'react-native-reanimated'; +import type {CanvasSize, ContentSize, MultiGestureCanvasVariables} from './types'; import * as MultiGestureCanvasUtils from './utils'; // This value determines how fast the pan animation should phase out @@ -8,7 +10,20 @@ import * as MultiGestureCanvasUtils from './utils'; // https://docs.swmansion.com/react-native-reanimated/docs/animations/withDecay/ const PAN_DECAY_DECELARATION = 0.9915; -const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, offsetY, panTranslateX, panTranslateY, isSwipingInPager, stopAnimation}) => { +type UsePanGestureProps = { + canvasSize: CanvasSize; + contentSize: ContentSize; + zoomScale: MultiGestureCanvasVariables['zoomScale']; + totalScale: MultiGestureCanvasVariables['totalScale']; + offsetX: MultiGestureCanvasVariables['offsetX']; + offsetY: MultiGestureCanvasVariables['offsetY']; + panTranslateX: MultiGestureCanvasVariables['panTranslateX']; + panTranslateY: MultiGestureCanvasVariables['panTranslateY']; + isSwipingInPager: MultiGestureCanvasVariables['isSwipingInPager']; + stopAnimation: MultiGestureCanvasVariables['stopAnimation']; +}; + +const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, offsetY, panTranslateX, panTranslateY, isSwipingInPager, stopAnimation}: UsePanGestureProps): PanGesture => { // The content size after fitting it to the canvas and zooming const zoomedContentWidth = useDerivedValue(() => contentSize.width * totalScale.value, [contentSize.width]); const zoomedContentHeight = useDerivedValue(() => contentSize.height * totalScale.value, [contentSize.height]); @@ -106,6 +121,7 @@ const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, const panGesture = Gesture.Pan() .manualActivation(true) .averageTouches(true) + // eslint-disable-next-line @typescript-eslint/naming-convention .onTouchesMove((_evt, state) => { // We only allow panning when the content is zoomed in if (zoomScale.value <= 1 || isSwipingInPager.value) { diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.ts similarity index 88% rename from src/components/MultiGestureCanvas/usePinchGesture.js rename to src/components/MultiGestureCanvas/usePinchGesture.ts index 21c5e55e0117..50c256933af5 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.js +++ b/src/components/MultiGestureCanvas/usePinchGesture.ts @@ -1,9 +1,25 @@ /* eslint-disable no-param-reassign */ import {useEffect, useState} from 'react'; +import type {PinchGesture} from 'react-native-gesture-handler'; import {Gesture} from 'react-native-gesture-handler'; import {runOnJS, useAnimatedReaction, useSharedValue, withSpring} from 'react-native-reanimated'; +import type {CanvasSize, MultiGestureCanvasVariables, OnScaleChangedCallback, ZoomRange} from './types'; import * as MultiGestureCanvasUtils from './utils'; +type UsePinchGestureProps = { + canvasSize: CanvasSize; + zoomScale: MultiGestureCanvasVariables['zoomScale']; + zoomRange: Required; + offsetX: MultiGestureCanvasVariables['offsetX']; + offsetY: MultiGestureCanvasVariables['offsetY']; + pinchTranslateX: MultiGestureCanvasVariables['pinchTranslateX']; + pinchTranslateY: MultiGestureCanvasVariables['pinchTranslateY']; + pinchScale: MultiGestureCanvasVariables['pinchScale']; + isSwipingInPager: MultiGestureCanvasVariables['isSwipingInPager']; + stopAnimation: MultiGestureCanvasVariables['stopAnimation']; + onScaleChanged: OnScaleChangedCallback; +}; + const usePinchGesture = ({ canvasSize, zoomScale, @@ -16,7 +32,7 @@ const usePinchGesture = ({ isSwipingInPager, stopAnimation, onScaleChanged, -}) => { +}: UsePinchGestureProps): PinchGesture => { // The current pinch gesture event scale const currentPinchScale = useSharedValue(1); @@ -51,7 +67,7 @@ const usePinchGesture = ({ * based on the canvas size and the current offset */ const getAdjustedFocal = MultiGestureCanvasUtils.useWorkletCallback( - (focalX, focalY) => ({ + (focalX: number, focalY: number) => ({ x: focalX - (canvasSize.width / 2 + offsetX.value), y: focalY - (canvasSize.height / 2 + offsetY.value), }), @@ -70,6 +86,7 @@ const usePinchGesture = ({ const pinchGesture = Gesture.Pinch() .enabled(pinchEnabled) + // eslint-disable-next-line @typescript-eslint/naming-convention .onTouchesDown((_evt, state) => { // We don't want to activate pinch gesture when we are swiping in the pager if (!isSwipingInPager.value) { diff --git a/src/components/MultiGestureCanvas/useTapGestures.js b/src/components/MultiGestureCanvas/useTapGestures.ts similarity index 80% rename from src/components/MultiGestureCanvas/useTapGestures.js rename to src/components/MultiGestureCanvas/useTapGestures.ts index eefe8c506b33..217981a1238d 100644 --- a/src/components/MultiGestureCanvas/useTapGestures.js +++ b/src/components/MultiGestureCanvas/useTapGestures.ts @@ -1,12 +1,42 @@ /* eslint-disable no-param-reassign */ import {useMemo} from 'react'; +import type {TapGesture} from 'react-native-gesture-handler'; import {Gesture} from 'react-native-gesture-handler'; import {runOnJS, withSpring} from 'react-native-reanimated'; +import type {CanvasSize, ContentSize, MultiGestureCanvasVariables, OnScaleChangedCallback} from './types'; import * as MultiGestureCanvasUtils from './utils'; const DOUBLE_TAP_SCALE = 3; -const useTapGestures = ({canvasSize, contentSize, minContentScale, maxContentScale, offsetX, offsetY, pinchScale, zoomScale, reset, stopAnimation, onScaleChanged, onTap}) => { +type UseTapGesturesProps = { + canvasSize: CanvasSize; + contentSize: ContentSize; + minContentScale: MultiGestureCanvasVariables['minContentScale']; + maxContentScale: MultiGestureCanvasVariables['maxContentScale']; + offsetX: MultiGestureCanvasVariables['offsetX']; + offsetY: MultiGestureCanvasVariables['offsetY']; + pinchScale: MultiGestureCanvasVariables['pinchScale']; + zoomScale: MultiGestureCanvasVariables['zoomScale']; + reset: MultiGestureCanvasVariables['reset']; + stopAnimation: MultiGestureCanvasVariables['stopAnimation']; + onScaleChanged: OnScaleChangedCallback; + onTap: MultiGestureCanvasVariables['onTap']; +}; + +const useTapGestures = ({ + canvasSize, + contentSize, + minContentScale, + maxContentScale, + offsetX, + offsetY, + pinchScale, + zoomScale, + reset, + stopAnimation, + onScaleChanged, + onTap, +}: UseTapGesturesProps): {singleTapGesture: TapGesture; doubleTapGesture: TapGesture} => { // The content size after scaling it with minimum scale to fit the content into the canvas const scaledContentWidth = useMemo(() => contentSize.width * minContentScale, [contentSize.width, minContentScale]); const scaledContentHeight = useMemo(() => contentSize.height * minContentScale, [contentSize.height, minContentScale]); @@ -15,7 +45,7 @@ const useTapGestures = ({canvasSize, contentSize, minContentScale, maxContentSca const doubleTapScale = useMemo(() => Math.max(DOUBLE_TAP_SCALE, maxContentScale / minContentScale), [maxContentScale, minContentScale]); const zoomToCoordinates = MultiGestureCanvasUtils.useWorkletCallback( - (focalX, focalY) => { + (focalX: number, focalY: number) => { 'worklet'; stopAnimation(); @@ -111,6 +141,7 @@ const useTapGestures = ({canvasSize, contentSize, minContentScale, maxContentSca .onBegin(() => { stopAnimation(); }) + // eslint-disable-next-line @typescript-eslint/naming-convention .onFinalize((_evt, success) => { if (!success || !onTap) { return; diff --git a/src/components/MultiGestureCanvas/utils.ts b/src/components/MultiGestureCanvas/utils.ts index 66fea9694180..0f081568462e 100644 --- a/src/components/MultiGestureCanvas/utils.ts +++ b/src/components/MultiGestureCanvas/utils.ts @@ -1,4 +1,5 @@ import {useCallback} from 'react'; +import type {WorkletFunction} from 'react-native-reanimated/lib/typescript/reanimated2/commonTypes'; // The spring config is used to determine the physics of the spring animation // Details and a playground for testing different configs can be found at @@ -37,11 +38,14 @@ function clamp(value: number, lowerBound: number, upperBound: number) { * @returns */ // eslint-disable-next-line @typescript-eslint/ban-types -function useWorkletCallback(callback: Parameters>[0], deps: Parameters[1] = []): T { +function useWorkletCallback( + callback: Parameters ReturnValue>>[0], + deps: Parameters>[1] = [], +): WorkletFunction { 'worklet'; // eslint-disable-next-line react-hooks/exhaustive-deps - return useCallback(callback, deps); + return useCallback<(...args: Args) => ReturnValue>(callback, deps) as WorkletFunction; } export {SPRING_CONFIG, zoomScaleBounceFactors, clamp, useWorkletCallback}; From 945fd35a0e06ca84aafe587b5475ac5663847bd9 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Sun, 7 Jan 2024 12:39:02 +0100 Subject: [PATCH 036/107] move types --- .../MultiGestureCanvas/getCanvasFitScale.ts | 13 +--- src/components/MultiGestureCanvas/index.tsx | 68 ++++++++++++------- src/components/MultiGestureCanvas/types.ts | 26 +------ src/components/MultiGestureCanvas/utils.ts | 2 +- 4 files changed, 49 insertions(+), 60 deletions(-) diff --git a/src/components/MultiGestureCanvas/getCanvasFitScale.ts b/src/components/MultiGestureCanvas/getCanvasFitScale.ts index e3e402fb066b..8fbb72e1f294 100644 --- a/src/components/MultiGestureCanvas/getCanvasFitScale.ts +++ b/src/components/MultiGestureCanvas/getCanvasFitScale.ts @@ -1,13 +1,6 @@ -type GetCanvasFitScale = (props: { - canvasSize: { - width: number; - height: number; - }; - contentSize: { - width: number; - height: number; - }; -}) => {scaleX: number; scaleY: number; minScale: number; maxScale: number}; +import type {CanvasSize, ContentSize} from './types'; + +type GetCanvasFitScale = (props: {canvasSize: CanvasSize; contentSize: ContentSize}) => {scaleX: number; scaleY: number; minScale: number; maxScale: number}; const getCanvasFitScale: GetCanvasFitScale = ({canvasSize, contentSize}) => { const scaleX = canvasSize.width / contentSize.width; diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index efe75f21af96..8b86ca5660b5 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -8,38 +8,39 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import {defaultZoomRange} from './constants'; import getCanvasFitScale from './getCanvasFitScale'; -import type {ContentSize, MultiGestureCanvasProps, ZoomRange} from './types'; +import type {CanvasSize, ContentSize, OnScaleChangedCallback, ZoomRange} from './types'; import usePanGesture from './usePanGesture'; import usePinchGesture from './usePinchGesture'; import useTapGestures from './useTapGestures'; import * as MultiGestureCanvasUtils from './utils'; -type Props = { - contentSize?: ContentSize; - zoomRange?: ZoomRange; -}; -type PropsWithDefault = { +type MultiGestureCanvasProps = React.PropsWithChildren<{ + /** + * Wheter the canvas is currently active (in the screen) or not. + * Disables certain gestures and functionality + */ + isActive: boolean; + + /** Handles scale changed event */ + onScaleChanged: OnScaleChangedCallback; + + /** The width and height of the canvas. + * This is needed in order to properly scale the content in the canvas + */ + canvasSize: CanvasSize; + + /** The width and height of the content. + * This is needed in order to properly scale the content in the canvas + */ contentSize: ContentSize; - zoomRange: Required; -}; -const getDeepDefaultProps = ({contentSize: contentSizeProp, zoomRange: zoomRangeProp}: Props): PropsWithDefault => { - const contentSize = { - width: contentSizeProp?.width ?? 1, - height: contentSizeProp?.height ?? 1, - }; - - const zoomRange = { - min: zoomRangeProp?.min ?? defaultZoomRange.min, - max: zoomRangeProp?.max ?? defaultZoomRange.max, - }; - - return {contentSize, zoomRange}; -}; - -function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged: onScaleChangedProp, children, ...props}: MultiGestureCanvasProps) { + + /** Range of zoom that can be applied to the content by pinching or double tapping. */ + zoomRange?: ZoomRange; +}>; + +function MultiGestureCanvas({canvasSize, contentSize: contentSizeProp, zoomRange: zoomRangeProp, isActive = true, onScaleChanged: onScaleChangedProp, children}: MultiGestureCanvasProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const {contentSize, zoomRange} = getDeepDefaultProps({contentSize: props.contentSize, zoomRange: props.zoomRange}); const pagerRefFallback = useRef(null); const shouldPagerScrollFallback = useSharedValue(false); @@ -64,6 +65,9 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged: onScal [attachmentCarouselPagerContext, isSwipingInPagerFallback, shouldPagerScrollFallback], ); + /** + * Calls the onScaleChanged callback from the both props and the pager context + */ const onScaleChanged = useCallback( (newScale: number) => { onScaleChangedProp(newScale); @@ -72,6 +76,22 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged: onScal [onScaleChangedContext, onScaleChangedProp], ); + const contentSize = useMemo( + () => ({ + width: contentSizeProp?.width ?? 1, + height: contentSizeProp?.height ?? 1, + }), + [contentSizeProp?.height, contentSizeProp?.width], + ); + + const zoomRange = useMemo( + () => ({ + min: zoomRangeProp?.min ?? defaultZoomRange.min, + max: zoomRangeProp?.max ?? defaultZoomRange.max, + }), + [zoomRangeProp?.max, zoomRangeProp?.min], + ); + // Based on the (original) content size and the canvas size, we calculate the horizontal and vertical scale factors // to fit the content inside the canvas // We later use the lower of the two scale factors to fit the content inside the canvas diff --git a/src/components/MultiGestureCanvas/types.ts b/src/components/MultiGestureCanvas/types.ts index 4ca0f5a1fe05..82fee9eb52a6 100644 --- a/src/components/MultiGestureCanvas/types.ts +++ b/src/components/MultiGestureCanvas/types.ts @@ -18,30 +18,6 @@ type ZoomRange = { type OnScaleChangedCallback = (zoomScale: number) => void; -type MultiGestureCanvasProps = React.PropsWithChildren<{ - /** - * Wheter the canvas is currently active (in the screen) or not. - * Disables certain gestures and functionality - */ - isActive: boolean; - - /** Handles scale changed event */ - onScaleChanged: OnScaleChangedCallback; - - /** The width and height of the canvas. - * This is needed in order to properly scale the content in the canvas - */ - canvasSize: CanvasSize; - - /** The width and height of the content. - * This is needed in order to properly scale the content in the canvas - */ - contentSize: ContentSize; - - /** Range of zoom that can be applied to the content by pinching or double tapping. */ - zoomRange?: ZoomRange; -}>; - type MultiGestureCanvasVariables = { minContentScale: number; maxContentScale: number; @@ -60,4 +36,4 @@ type MultiGestureCanvasVariables = { onTap: () => void; }; -export type {MultiGestureCanvasProps, CanvasSize, ContentSize, ZoomRange, OnScaleChangedCallback, MultiGestureCanvasVariables}; +export type {CanvasSize, ContentSize, ZoomRange, OnScaleChangedCallback, MultiGestureCanvasVariables}; diff --git a/src/components/MultiGestureCanvas/utils.ts b/src/components/MultiGestureCanvas/utils.ts index 0f081568462e..26e814313f8f 100644 --- a/src/components/MultiGestureCanvas/utils.ts +++ b/src/components/MultiGestureCanvas/utils.ts @@ -40,7 +40,7 @@ function clamp(value: number, lowerBound: number, upperBound: number) { // eslint-disable-next-line @typescript-eslint/ban-types function useWorkletCallback( callback: Parameters ReturnValue>>[0], - deps: Parameters>[1] = [], + deps: Parameters ReturnValue>>[1] = [], ): WorkletFunction { 'worklet'; From 369d61d25c0206a0a32e6832cd863fdeecbe3817 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Sun, 7 Jan 2024 13:50:03 +0100 Subject: [PATCH 037/107] migrate Lightbox --- src/components/{Lightbox.js => Lightbox.tsx} | 165 ++++++++++--------- 1 file changed, 86 insertions(+), 79 deletions(-) rename src/components/{Lightbox.js => Lightbox.tsx} (65%) diff --git a/src/components/Lightbox.js b/src/components/Lightbox.tsx similarity index 65% rename from src/components/Lightbox.js rename to src/components/Lightbox.tsx index 078cb13d42e5..bffffcf8a8cf 100644 --- a/src/components/Lightbox.js +++ b/src/components/Lightbox.tsx @@ -1,82 +1,93 @@ -/* eslint-disable es/no-optional-chaining */ -import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import type {ImageSourcePropType, LayoutChangeEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native'; import {ActivityIndicator, PixelRatio, StyleSheet, View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; -import * as AttachmentsPropTypes from './Attachments/propTypes'; import Image from './Image'; -import MultiGestureCanvas from './MultiGestureCanvas'; -import getCanvasFitScale from './MultiGestureCanvas/utils'; +import MultiGestureCanvas, {defaultZoomRange} from './MultiGestureCanvas'; +import type {OnScaleChangedCallback, ZoomRange} from './MultiGestureCanvas/types'; +import * as MultiGestureCanvasUtils from './MultiGestureCanvas/utils'; // Increase/decrease this number to change the number of concurrent lightboxes // The more concurrent lighboxes, the worse performance gets (especially on low-end devices) // -1 means unlimited const NUMBER_OF_CONCURRENT_LIGHTBOXES = 3; +const DEFAULT_IMAGE_SIZE = 200; +const DEFAULT_IMAGE_DIMENSIONS = { + width: DEFAULT_IMAGE_SIZE, + height: DEFAULT_IMAGE_SIZE, +}; -const cachedDimensions = new Map(); - -/** - * On the native layer, we use a image library to handle zoom functionality - */ -const propTypes = { - // TODO: Add TS types for zoom range - // ...zoomRangePropTypes, +type LightboxImageDimension = { + lightboxSize?: { + width: number; + height: number; + }; + fallbackSize?: { + width: number; + height: number; + }; +}; - /** Triggers whenever the zoom scale changes */ - onScaleChanged: PropTypes.func, +const cachedDimensions = new Map(); - /** Function for handle on press */ - onPress: PropTypes.func, +type ImageOnLoadEvent = NativeSyntheticEvent<{width: number; height: number}>; - /** Handles errors while displaying the image */ - onError: PropTypes.func, +type LightboxProps = { + /** Whether source url requires authentication */ + isAuthTokenRequired: boolean; /** URL to full-sized attachment, SVG function, or numeric static image on native platforms */ - source: AttachmentsPropTypes.attachmentSourcePropType.isRequired, + source: ImageSourcePropType; - /** Whether source url requires authentication */ - isAuthTokenRequired: PropTypes.bool, + /** Triggers whenever the zoom scale changes */ + onScaleChanged: OnScaleChangedCallback; - /** Whether the Lightbox is used within a carousel component and there are other sibling elements */ - hasSiblingCarouselItems: PropTypes.bool, + /** Handles errors while displaying the image */ + onError: () => void; + + /** Additional styles to add to the component */ + style: StyleProp; /** The index of the carousel item */ - index: PropTypes.number, + index: number; /** The index of the currently active carousel item */ - activeIndex: PropTypes.number, + activeIndex: number; - /** Additional styles to add to the component */ - style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), -}; + /** Whether the Lightbox is used within a carousel component and there are other sibling elements */ + hasSiblingCarouselItems: boolean; -const defaultProps = { - // TODO: Add TS default values - // ...zoomRangeDefaultProps, - - isAuthTokenRequired: false, - index: 0, - activeIndex: 0, - hasSiblingCarouselItems: false, - onScaleChanged: () => {}, - onPress: () => {}, - onError: () => {}, - style: {}, + /** Range of zoom that can be applied to the content by pinching or double tapping. */ + zoomRange: ZoomRange; }; -const DEFAULT_IMAGE_SIZE = 200; - -function Lightbox({isAuthTokenRequired, source, onScaleChanged, onPress, onError, style, index, activeIndex, hasSiblingCarouselItems, zoomRange}) { +/** + * On the native layer, we use a image library to handle zoom functionality + */ +function Lightbox({ + isAuthTokenRequired = false, + source, + onScaleChanged, + onError, + style, + index = 0, + activeIndex = 0, + hasSiblingCarouselItems = false, + zoomRange = defaultZoomRange, +}: LightboxProps) { const StyleUtils = useStyleUtils(); const [containerSize, setContainerSize] = useState({width: 0, height: 0}); const isContainerLoaded = containerSize.width !== 0 && containerSize.height !== 0; - const [imageDimensions, _setImageDimensions] = useState(() => cachedDimensions.get(source)); - const setImageDimensions = (newDimensions) => { - _setImageDimensions(newDimensions); - cachedDimensions.set(source, newDimensions); - }; + const [imageDimensions, setInternalImageDimensions] = useState(() => cachedDimensions.get(source)); + const setImageDimensions = useCallback( + (newDimensions: LightboxImageDimension) => { + setInternalImageDimensions(newDimensions); + cachedDimensions.set(source, newDimensions); + }, + [source], + ); const isItemActive = index === activeIndex; const [isActive, setActive] = useState(isItemActive); @@ -86,8 +97,9 @@ function Lightbox({isAuthTokenRequired, source, onScaleChanged, onPress, onError const [isFallbackVisible, setFallbackVisible] = useState(isInactiveCarouselItem); const [isFallbackLoaded, setFallbackLoaded] = useState(false); - const isLightboxLoaded = imageDimensions?.lightboxSize != null; const isLightboxInRange = useMemo(() => { + // @ts-expect-error TS will throw an error here because -1 and the constantly set number have no overlap + // We can safely ignore this error, because we might change the value in the future if (NUMBER_OF_CONCURRENT_LIGHTBOXES === -1) { return true; } @@ -96,6 +108,7 @@ function Lightbox({isAuthTokenRequired, source, onScaleChanged, onPress, onError const indexOutOfRange = index > activeIndex + indexCanvasOffset || index < activeIndex - indexCanvasOffset; return !indexOutOfRange; }, [activeIndex, index]); + const [isLightboxLoaded, setLightboxLoaded] = useState(false); const isLightboxVisible = isLightboxInRange && (isActive || isLightboxLoaded || isFallbackLoaded); // If the fallback image is currently visible, we want to hide the Lightbox until the fallback gets hidden, @@ -107,10 +120,20 @@ function Lightbox({isAuthTokenRequired, source, onScaleChanged, onPress, onError const isLoading = isActive && (!isContainerLoaded || !isImageLoaded); const updateCanvasSize = useCallback( - ({nativeEvent}) => setContainerSize({width: PixelRatio.roundToNearestPixel(nativeEvent.layout.width), height: PixelRatio.roundToNearestPixel(nativeEvent.layout.height)}), + ({nativeEvent}: LayoutChangeEvent) => + setContainerSize({width: PixelRatio.roundToNearestPixel(nativeEvent.layout.width), height: PixelRatio.roundToNearestPixel(nativeEvent.layout.height)}), [], ); + const updateContentSize = useCallback( + (e: ImageOnLoadEvent) => { + const width = (e.nativeEvent?.width || 0) * PixelRatio.get(); + const height = (e.nativeEvent?.height || 0) * PixelRatio.get(); + setImageDimensions({...imageDimensions, lightboxSize: {width, height}}); + }, + [imageDimensions, setImageDimensions], + ); + // We delay setting a page to active state by a (few) millisecond(s), // to prevent the image transformer from flashing while still rendering // Instead, we show the fallback image while the image transformer is loading the image @@ -154,15 +177,15 @@ function Lightbox({isAuthTokenRequired, source, onScaleChanged, onPress, onError const fallbackSize = useMemo(() => { if (!hasSiblingCarouselItems || (imageDimensions?.lightboxSize == null && imageDimensions?.fallbackSize == null) || containerSize.width === 0 || containerSize.height === 0) { - return { - width: DEFAULT_IMAGE_SIZE, - height: DEFAULT_IMAGE_SIZE, - }; + return DEFAULT_IMAGE_DIMENSIONS; } - const imageSize = imageDimensions.lightboxSize || imageDimensions.fallbackSize; + // If the lightbox size is null, we know that fallback size must not be null, because otherwise we would have returned early + // TypeScript doesn't recognize that, so we need to use the non-null assertion operator + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const imageSize = imageDimensions?.lightboxSize ?? imageDimensions.fallbackSize!; - const {minScale} = getCanvasFitScale({canvasSize: containerSize, contentSize: imageSize}); + const {minScale} = MultiGestureCanvasUtils.getCanvasFitScale({canvasSize: containerSize, contentSize: imageSize}); return { width: PixelRatio.roundToNearestPixel(imageSize.width * minScale), @@ -172,32 +195,27 @@ function Lightbox({isAuthTokenRequired, source, onScaleChanged, onPress, onError return ( {isContainerLoaded && ( <> - {isLightboxVisible && ( + {isLightboxVisible && imageDimensions?.lightboxSize != null && ( setImageLoaded(true)} - onLoad={(e) => { - const width = (e.nativeEvent?.width || 0) * PixelRatio.get(); - const height = (e.nativeEvent?.height || 0) * PixelRatio.get(); - setImageDimensions({...imageDimensions, lightboxSize: {width, height}}); - }} + onLoad={updateContentSize} + onLoadEnd={() => setLightboxLoaded(true)} /> @@ -211,17 +229,8 @@ function Lightbox({isAuthTokenRequired, source, onScaleChanged, onPress, onError resizeMode="contain" style={fallbackSize} isAuthTokenRequired={isAuthTokenRequired} + onLoad={updateContentSize} onLoadEnd={() => setFallbackLoaded(true)} - onLoad={(e) => { - const width = (e.nativeEvent?.width || 0) * PixelRatio.get(); - const height = (e.nativeEvent?.height || 0) * PixelRatio.get(); - - if (imageDimensions?.lightboxSize != null) { - return; - } - - setImageDimensions({...imageDimensions, fallbackSize: {width, height}}); - }} /> )} @@ -239,8 +248,6 @@ function Lightbox({isAuthTokenRequired, source, onScaleChanged, onPress, onError ); } -Lightbox.propTypes = propTypes; -Lightbox.defaultProps = defaultProps; Lightbox.displayName = 'Lightbox'; export default Lightbox; From d209ac2dd8377626261856e980790edf0e7970ff Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Sun, 7 Jan 2024 13:52:44 +0100 Subject: [PATCH 038/107] restructure --- .../MultiGestureCanvas/getCanvasFitScale.ts | 15 --------------- src/components/MultiGestureCanvas/index.tsx | 3 +-- src/components/MultiGestureCanvas/types.ts | 5 +++++ src/components/MultiGestureCanvas/utils.ts | 15 ++++++++++++++- 4 files changed, 20 insertions(+), 18 deletions(-) delete mode 100644 src/components/MultiGestureCanvas/getCanvasFitScale.ts diff --git a/src/components/MultiGestureCanvas/getCanvasFitScale.ts b/src/components/MultiGestureCanvas/getCanvasFitScale.ts deleted file mode 100644 index 8fbb72e1f294..000000000000 --- a/src/components/MultiGestureCanvas/getCanvasFitScale.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type {CanvasSize, ContentSize} from './types'; - -type GetCanvasFitScale = (props: {canvasSize: CanvasSize; contentSize: ContentSize}) => {scaleX: number; scaleY: number; minScale: number; maxScale: number}; - -const getCanvasFitScale: GetCanvasFitScale = ({canvasSize, contentSize}) => { - const scaleX = canvasSize.width / contentSize.width; - const scaleY = canvasSize.height / contentSize.height; - - const minScale = Math.min(scaleX, scaleY); - const maxScale = Math.max(scaleX, scaleY); - - return {scaleX, scaleY, minScale, maxScale}; -}; - -export default getCanvasFitScale; diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index 8b86ca5660b5..778b680cb302 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -7,7 +7,6 @@ import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCa import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import {defaultZoomRange} from './constants'; -import getCanvasFitScale from './getCanvasFitScale'; import type {CanvasSize, ContentSize, OnScaleChangedCallback, ZoomRange} from './types'; import usePanGesture from './usePanGesture'; import usePinchGesture from './usePinchGesture'; @@ -95,7 +94,7 @@ function MultiGestureCanvas({canvasSize, contentSize: contentSizeProp, zoomRange // Based on the (original) content size and the canvas size, we calculate the horizontal and vertical scale factors // to fit the content inside the canvas // We later use the lower of the two scale factors to fit the content inside the canvas - const {minScale: minContentScale, maxScale: maxContentScale} = useMemo(() => getCanvasFitScale({canvasSize, contentSize}), [canvasSize, contentSize]); + const {minScale: minContentScale, maxScale: maxContentScale} = useMemo(() => MultiGestureCanvasUtils.getCanvasFitScale({canvasSize, contentSize}), [canvasSize, contentSize]); const zoomScale = useSharedValue(1); diff --git a/src/components/MultiGestureCanvas/types.ts b/src/components/MultiGestureCanvas/types.ts index 82fee9eb52a6..0309a6cbcdfc 100644 --- a/src/components/MultiGestureCanvas/types.ts +++ b/src/components/MultiGestureCanvas/types.ts @@ -1,23 +1,28 @@ import type {SharedValue} from 'react-native-reanimated'; import type {WorkletFunction} from 'react-native-reanimated/lib/typescript/reanimated2/commonTypes'; +/** Dimensions of the canvas rendered by the MultiGestureCanvas */ type CanvasSize = { width: number; height: number; }; +/** Dimensions of the content passed to the MultiGestureCanvas */ type ContentSize = { width: number; height: number; }; +/** Range of zoom that can be applied to the content by pinching or double tapping. */ type ZoomRange = { min?: number; max?: number; }; +/** Triggered whenever the scale of the MultiGestureCanvas changes */ type OnScaleChangedCallback = (zoomScale: number) => void; +/** Types used of variables used within the MultiGestureCanvas component and it's hooks */ type MultiGestureCanvasVariables = { minContentScale: number; maxContentScale: number; diff --git a/src/components/MultiGestureCanvas/utils.ts b/src/components/MultiGestureCanvas/utils.ts index 26e814313f8f..4e377b3702d9 100644 --- a/src/components/MultiGestureCanvas/utils.ts +++ b/src/components/MultiGestureCanvas/utils.ts @@ -1,5 +1,6 @@ import {useCallback} from 'react'; import type {WorkletFunction} from 'react-native-reanimated/lib/typescript/reanimated2/commonTypes'; +import type {CanvasSize, ContentSize} from './types'; // The spring config is used to determine the physics of the spring animation // Details and a playground for testing different configs can be found at @@ -48,4 +49,16 @@ function useWorkletCallback( return useCallback<(...args: Args) => ReturnValue>(callback, deps) as WorkletFunction; } -export {SPRING_CONFIG, zoomScaleBounceFactors, clamp, useWorkletCallback}; +type GetCanvasFitScale = (props: {canvasSize: CanvasSize; contentSize: ContentSize}) => {scaleX: number; scaleY: number; minScale: number; maxScale: number}; + +const getCanvasFitScale: GetCanvasFitScale = ({canvasSize, contentSize}) => { + const scaleX = canvasSize.width / contentSize.width; + const scaleY = canvasSize.height / contentSize.height; + + const minScale = Math.min(scaleX, scaleY); + const maxScale = Math.max(scaleX, scaleY); + + return {scaleX, scaleY, minScale, maxScale}; +}; + +export {SPRING_CONFIG, zoomScaleBounceFactors, clamp, useWorkletCallback, getCanvasFitScale}; From 24d34a57cb1c8a6277fa21755d1de4553c381fbe Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Sun, 7 Jan 2024 16:20:19 +0100 Subject: [PATCH 039/107] add back propTypes --- .../MultiGestureCanvas/propTypes.js | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/components/MultiGestureCanvas/propTypes.js diff --git a/src/components/MultiGestureCanvas/propTypes.js b/src/components/MultiGestureCanvas/propTypes.js new file mode 100644 index 000000000000..189e661a702c --- /dev/null +++ b/src/components/MultiGestureCanvas/propTypes.js @@ -0,0 +1,65 @@ +import PropTypes from 'prop-types'; + +const defaultZoomRange = { + min: 1, + max: 20, +}; + +const zoomRangePropTypes = { + /** Range of zoom that can be applied to the content by pinching or double tapping. */ + zoomRange: PropTypes.shape({ + min: PropTypes.number, + max: PropTypes.number, + }), +}; + +const zoomRangeDefaultProps = { + zoomRange: { + min: defaultZoomRange.min, + max: defaultZoomRange.max, + }, +}; + +const multiGestureCanvasPropTypes = { + ...zoomRangePropTypes, + + /** + * Wheter the canvas is currently active (in the screen) or not. + * Disables certain gestures and functionality + */ + isActive: PropTypes.bool, + + /** Handles scale changed event */ + onScaleChanged: PropTypes.func, + + /** + * The width and height of the canvas. + * This is needed in order to properly scale the content in the canvas + */ + canvasSize: PropTypes.shape({ + width: PropTypes.number.isRequired, + height: PropTypes.number.isRequired, + }).isRequired, + + /** + * The width and height of the content. + * This is needed in order to properly scale the content in the canvas + */ + contentSize: PropTypes.shape({ + width: PropTypes.number, + height: PropTypes.number, + }), + + /** Content that should be transformed inside the canvas (images, pdf, ...) */ + children: PropTypes.node.isRequired, +}; + +const multiGestureCanvasDefaultProps = { + isActive: true, + onScaleChanged: () => undefined, + contentSize: undefined, + contentScaling: undefined, + zoomRange: undefined, +}; + +export {defaultZoomRange, zoomRangePropTypes, zoomRangeDefaultProps, multiGestureCanvasPropTypes, multiGestureCanvasDefaultProps}; From 083f37ccea17b3e2cdc52336104c98d4fe2276eb Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Sun, 7 Jan 2024 16:20:37 +0100 Subject: [PATCH 040/107] simplify props --- src/components/MultiGestureCanvas/index.tsx | 24 ++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index 778b680cb302..764927f4f390 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -6,6 +6,7 @@ import Animated, {cancelAnimation, runOnUI, useAnimatedReaction, useAnimatedStyl import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; import {defaultZoomRange} from './constants'; import type {CanvasSize, ContentSize, OnScaleChangedCallback, ZoomRange} from './types'; import usePanGesture from './usePanGesture'; @@ -13,7 +14,7 @@ import usePinchGesture from './usePinchGesture'; import useTapGestures from './useTapGestures'; import * as MultiGestureCanvasUtils from './utils'; -type MultiGestureCanvasProps = React.PropsWithChildren<{ +type MultiGestureCanvasProps = ChildrenProps & { /** * Wheter the canvas is currently active (in the screen) or not. * Disables certain gestures and functionality @@ -35,9 +36,16 @@ type MultiGestureCanvasProps = React.PropsWithChildren<{ /** Range of zoom that can be applied to the content by pinching or double tapping. */ zoomRange?: ZoomRange; -}>; - -function MultiGestureCanvas({canvasSize, contentSize: contentSizeProp, zoomRange: zoomRangeProp, isActive = true, onScaleChanged: onScaleChangedProp, children}: MultiGestureCanvasProps) { +}; + +function MultiGestureCanvas({ + canvasSize, + contentSize = {width: 1, height: 1}, + zoomRange: zoomRangeProp, + isActive = true, + onScaleChanged: onScaleChangedProp, + children, +}: MultiGestureCanvasProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -75,14 +83,6 @@ function MultiGestureCanvas({canvasSize, contentSize: contentSizeProp, zoomRange [onScaleChangedContext, onScaleChangedProp], ); - const contentSize = useMemo( - () => ({ - width: contentSizeProp?.width ?? 1, - height: contentSizeProp?.height ?? 1, - }), - [contentSizeProp?.height, contentSizeProp?.width], - ); - const zoomRange = useMemo( () => ({ min: zoomRangeProp?.min ?? defaultZoomRange.min, From 595337073b9c342770f599f496c0fcfeb1e38bce Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Sun, 7 Jan 2024 17:14:24 +0100 Subject: [PATCH 041/107] improve Lightbox --- src/components/ImageView/index.native.js | 2 +- src/components/Lightbox.tsx | 140 +++++++++----------- src/components/MultiGestureCanvas/index.tsx | 2 +- 3 files changed, 63 insertions(+), 81 deletions(-) diff --git a/src/components/ImageView/index.native.js b/src/components/ImageView/index.native.js index 98349b213aa5..a94842b35219 100644 --- a/src/components/ImageView/index.native.js +++ b/src/components/ImageView/index.native.js @@ -31,7 +31,7 @@ function ImageView({isAuthTokenRequired, url, onScaleChanged, onPress, style, zo return ( (); +const cachedImageDimensions = new Map(); -type ImageOnLoadEvent = NativeSyntheticEvent<{width: number; height: number}>; +type ImageOnLoadEvent = NativeSyntheticEvent; type LightboxProps = { /** Whether source url requires authentication */ isAuthTokenRequired: boolean; - /** URL to full-sized attachment, SVG function, or numeric static image on native platforms */ - source: ImageSourcePropType; + /** URI to full-sized attachment, SVG function, or numeric static image on native platforms */ + uri: string; /** Triggers whenever the zoom scale changes */ onScaleChanged: OnScaleChangedCallback; @@ -66,7 +55,7 @@ type LightboxProps = { */ function Lightbox({ isAuthTokenRequired = false, - source, + uri, onScaleChanged, onError, style, @@ -77,25 +66,49 @@ function Lightbox({ }: LightboxProps) { const StyleUtils = useStyleUtils(); - const [containerSize, setContainerSize] = useState({width: 0, height: 0}); - const isContainerLoaded = containerSize.width !== 0 && containerSize.height !== 0; + const [canvasSize, setCanvasSize] = useState({width: 0, height: 0}); + const isCanvasLoaded = canvasSize.width !== 0 && canvasSize.height !== 0; + const updateCanvasSize = useCallback( + ({ + nativeEvent: { + layout: {width, height}, + }, + }: LayoutChangeEvent) => setCanvasSize({width: PixelRatio.roundToNearestPixel(width), height: PixelRatio.roundToNearestPixel(height)}), + [], + ); - const [imageDimensions, setInternalImageDimensions] = useState(() => cachedDimensions.get(source)); - const setImageDimensions = useCallback( - (newDimensions: LightboxImageDimension) => { - setInternalImageDimensions(newDimensions); - cachedDimensions.set(source, newDimensions); + const [contentSize, setInternalContentSize] = useState(() => cachedImageDimensions.get(uri)); + const setContentSize = useCallback( + (newContentSize: ContentSize | undefined) => { + setInternalContentSize(newContentSize); + cachedImageDimensions.set(uri, newContentSize); }, - [source], + [uri], + ); + const updateContentSize = useCallback( + ({nativeEvent: {width, height}}: ImageOnLoadEvent) => setContentSize({width: width * PixelRatio.get(), height: height * PixelRatio.get()}), + [setContentSize], ); + const contentLoaded = contentSize != null; const isItemActive = index === activeIndex; const [isActive, setActive] = useState(isItemActive); - const [isImageLoaded, setImageLoaded] = useState(false); const isInactiveCarouselItem = hasSiblingCarouselItems && !isActive; const [isFallbackVisible, setFallbackVisible] = useState(isInactiveCarouselItem); - const [isFallbackLoaded, setFallbackLoaded] = useState(false); + const [isFallbackImageLoaded, setFallbackImageLoaded] = useState(false); + const fallbackSize = useMemo(() => { + if (!hasSiblingCarouselItems || contentSize == null || canvasSize.width === 0 || canvasSize.height === 0) { + return DEFAULT_IMAGE_DIMENSIONS; + } + + const {minScale} = MultiGestureCanvasUtils.getCanvasFitScale({canvasSize, contentSize}); + + return { + width: PixelRatio.roundToNearestPixel(contentSize.width * minScale), + height: PixelRatio.roundToNearestPixel(contentSize.height * minScale), + }; + }, [canvasSize, hasSiblingCarouselItems, contentSize]); const isLightboxInRange = useMemo(() => { // @ts-expect-error TS will throw an error here because -1 and the constantly set number have no overlap @@ -108,31 +121,17 @@ function Lightbox({ const indexOutOfRange = index > activeIndex + indexCanvasOffset || index < activeIndex - indexCanvasOffset; return !indexOutOfRange; }, [activeIndex, index]); - const [isLightboxLoaded, setLightboxLoaded] = useState(false); - const isLightboxVisible = isLightboxInRange && (isActive || isLightboxLoaded || isFallbackLoaded); + const [isLightboxImageLoaded, setLightboxImageLoaded] = useState(false); + const isLightboxVisible = isLightboxInRange && (isActive || isLightboxImageLoaded || isFallbackImageLoaded); // If the fallback image is currently visible, we want to hide the Lightbox until the fallback gets hidden, // so that we don't see two overlapping images at the same time. + // We cannot NOT render it, because we need to render the Lightbox to get the correct dimensions for the fallback image. // If there the Lightbox is not used within a carousel, we don't need to hide the Lightbox, // because it's only going to be rendered after the fallback image is hidden. const shouldHideLightbox = hasSiblingCarouselItems && isFallbackVisible; - const isLoading = isActive && (!isContainerLoaded || !isImageLoaded); - - const updateCanvasSize = useCallback( - ({nativeEvent}: LayoutChangeEvent) => - setContainerSize({width: PixelRatio.roundToNearestPixel(nativeEvent.layout.width), height: PixelRatio.roundToNearestPixel(nativeEvent.layout.height)}), - [], - ); - - const updateContentSize = useCallback( - (e: ImageOnLoadEvent) => { - const width = (e.nativeEvent?.width || 0) * PixelRatio.get(); - const height = (e.nativeEvent?.height || 0) * PixelRatio.get(); - setImageDimensions({...imageDimensions, lightboxSize: {width, height}}); - }, - [imageDimensions, setImageDimensions], - ); + const isLoading = isActive && (!isCanvasLoaded || !contentLoaded); // We delay setting a page to active state by a (few) millisecond(s), // to prevent the image transformer from flashing while still rendering @@ -149,8 +148,8 @@ function Lightbox({ if (isLightboxVisible) { return; } - setImageLoaded(false); - }, [isLightboxVisible]); + setContentSize(undefined); + }, [isLightboxVisible, setContentSize]); useEffect(() => { if (!hasSiblingCarouselItems) { @@ -158,64 +157,47 @@ function Lightbox({ } if (isActive) { - if (isImageLoaded && isFallbackVisible) { + if (contentLoaded && isFallbackVisible) { // We delay hiding the fallback image while image transformer is still rendering setTimeout(() => { setFallbackVisible(false); - setFallbackLoaded(false); + setFallbackImageLoaded(false); }, 100); } } else { - if (isLightboxVisible && isLightboxLoaded) { + if (isLightboxVisible && isLightboxImageLoaded) { return; } // Show fallback when the image goes out of focus or when the image is loading setFallbackVisible(true); } - }, [hasSiblingCarouselItems, isActive, isImageLoaded, isFallbackVisible, isLightboxLoaded, isLightboxVisible]); - - const fallbackSize = useMemo(() => { - if (!hasSiblingCarouselItems || (imageDimensions?.lightboxSize == null && imageDimensions?.fallbackSize == null) || containerSize.width === 0 || containerSize.height === 0) { - return DEFAULT_IMAGE_DIMENSIONS; - } - - // If the lightbox size is null, we know that fallback size must not be null, because otherwise we would have returned early - // TypeScript doesn't recognize that, so we need to use the non-null assertion operator - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const imageSize = imageDimensions?.lightboxSize ?? imageDimensions.fallbackSize!; - - const {minScale} = MultiGestureCanvasUtils.getCanvasFitScale({canvasSize: containerSize, contentSize: imageSize}); - - return { - width: PixelRatio.roundToNearestPixel(imageSize.width * minScale), - height: PixelRatio.roundToNearestPixel(imageSize.height * minScale), - }; - }, [containerSize, hasSiblingCarouselItems, imageDimensions]); + }, [hasSiblingCarouselItems, isActive, isFallbackVisible, isLightboxImageLoaded, isLightboxVisible, contentLoaded]); return ( - {isContainerLoaded && ( + {isCanvasLoaded && ( <> - {isLightboxVisible && imageDimensions?.lightboxSize != null && ( + {isLightboxVisible && ( setLightboxLoaded(true)} + onLoadEnd={() => setLightboxImageLoaded(true)} /> @@ -225,12 +207,12 @@ function Lightbox({ {isFallbackVisible && ( setFallbackLoaded(true)} + onLoadEnd={() => setFallbackImageLoaded(true)} /> )} diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index 764927f4f390..65a14d25a492 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -32,7 +32,7 @@ type MultiGestureCanvasProps = ChildrenProps & { /** The width and height of the content. * This is needed in order to properly scale the content in the canvas */ - contentSize: ContentSize; + contentSize?: ContentSize; /** Range of zoom that can be applied to the content by pinching or double tapping. */ zoomRange?: ZoomRange; From 616e8cb3d4794d8f583402064b58250276951fcf Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 8 Jan 2024 09:19:52 +0100 Subject: [PATCH 042/107] remove comment --- .../AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts index 981cdb797f9f..e595b8c5c4d1 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -4,8 +4,6 @@ import type {SharedValue} from 'react-native-reanimated'; type AttachmentCarouselPagerContextType = { onTap: () => void; - // onSwipe: (y: number) => void; - // onSwipeSuccess: () => void; onScaleChanged: (scale: number) => void; pagerRef: React.Ref; shouldPagerScroll: SharedValue; From 3055579c8bcc22807e2dd8e652c07f096c6935ce Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 8 Jan 2024 09:26:41 +0100 Subject: [PATCH 043/107] update error supression --- src/components/Lightbox.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/Lightbox.tsx b/src/components/Lightbox.tsx index a4d9203fba34..74ad03bf4678 100644 --- a/src/components/Lightbox.tsx +++ b/src/components/Lightbox.tsx @@ -10,7 +10,11 @@ import * as MultiGestureCanvasUtils from './MultiGestureCanvas/utils'; // Increase/decrease this number to change the number of concurrent lightboxes // The more concurrent lighboxes, the worse performance gets (especially on low-end devices) // -1 means unlimited -const NUMBER_OF_CONCURRENT_LIGHTBOXES = 3; +// We need to define a type for this constant and therefore ignore this ESLint error, although the type is inferable, +// because otherwise TS will throw an error later in the code since "-1" and this constant have no overlap. +// We can safely ignore this error, because we might change the value in the future +// eslint-disable-next-line @typescript-eslint/no-inferrable-types +const NUMBER_OF_CONCURRENT_LIGHTBOXES: number = 3; const DEFAULT_IMAGE_SIZE = 200; const DEFAULT_IMAGE_DIMENSIONS = { width: DEFAULT_IMAGE_SIZE, @@ -111,8 +115,6 @@ function Lightbox({ }, [canvasSize, hasSiblingCarouselItems, contentSize]); const isLightboxInRange = useMemo(() => { - // @ts-expect-error TS will throw an error here because -1 and the constantly set number have no overlap - // We can safely ignore this error, because we might change the value in the future if (NUMBER_OF_CONCURRENT_LIGHTBOXES === -1) { return true; } From 0b741dd040545f2d02cf7ff14176d936f240af4b Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 8 Jan 2024 11:03:59 +0100 Subject: [PATCH 044/107] revert Lightbox changes --- src/components/Lightbox.tsx | 146 ++++++++++-------- .../MultiGestureCanvas/getCanvasFitScale.ts | 15 ++ src/components/MultiGestureCanvas/utils.ts | 15 +- 3 files changed, 98 insertions(+), 78 deletions(-) create mode 100644 src/components/MultiGestureCanvas/getCanvasFitScale.ts diff --git a/src/components/Lightbox.tsx b/src/components/Lightbox.tsx index 74ad03bf4678..1684a489c0da 100644 --- a/src/components/Lightbox.tsx +++ b/src/components/Lightbox.tsx @@ -1,11 +1,12 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import type {LayoutChangeEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native'; +import type {ImageSourcePropType, LayoutChangeEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native'; import {ActivityIndicator, PixelRatio, StyleSheet, View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; import Image from './Image'; import MultiGestureCanvas, {defaultZoomRange} from './MultiGestureCanvas'; import type {ContentSize, OnScaleChangedCallback, ZoomRange} from './MultiGestureCanvas/types'; import * as MultiGestureCanvasUtils from './MultiGestureCanvas/utils'; +import getCanvasFitScale from '@components/MultiGestureCanvas/getCanvasFitScale'; // Increase/decrease this number to change the number of concurrent lightboxes // The more concurrent lighboxes, the worse performance gets (especially on low-end devices) @@ -16,12 +17,13 @@ import * as MultiGestureCanvasUtils from './MultiGestureCanvas/utils'; // eslint-disable-next-line @typescript-eslint/no-inferrable-types const NUMBER_OF_CONCURRENT_LIGHTBOXES: number = 3; const DEFAULT_IMAGE_SIZE = 200; -const DEFAULT_IMAGE_DIMENSIONS = { - width: DEFAULT_IMAGE_SIZE, - height: DEFAULT_IMAGE_SIZE, -}; -const cachedImageDimensions = new Map(); +type LightboxImageDimensions = { + lightboxSize?: ContentSize; + fallbackSize?: ContentSize; +}; +} +const cachedDimensions = new Map(); type ImageOnLoadEvent = NativeSyntheticEvent; @@ -30,7 +32,7 @@ type LightboxProps = { isAuthTokenRequired: boolean; /** URI to full-sized attachment, SVG function, or numeric static image on native platforms */ - uri: string; + source: ImageSourcePropType; /** Triggers whenever the zoom scale changes */ onScaleChanged: OnScaleChangedCallback; @@ -59,7 +61,7 @@ type LightboxProps = { */ function Lightbox({ isAuthTokenRequired = false, - uri, + source, onScaleChanged, onError, style, @@ -70,50 +72,26 @@ function Lightbox({ }: LightboxProps) { const StyleUtils = useStyleUtils(); - const [canvasSize, setCanvasSize] = useState({width: 0, height: 0}); - const isCanvasLoaded = canvasSize.width !== 0 && canvasSize.height !== 0; - const updateCanvasSize = useCallback( - ({ - nativeEvent: { - layout: {width, height}, - }, - }: LayoutChangeEvent) => setCanvasSize({width: PixelRatio.roundToNearestPixel(width), height: PixelRatio.roundToNearestPixel(height)}), - [], - ); + const [containerSize, setContainerSize] = useState({width: 0, height: 0}); + const isContainerLoaded = containerSize.width !== 0 && containerSize.height !== 0; - const [contentSize, setInternalContentSize] = useState(() => cachedImageDimensions.get(uri)); - const setContentSize = useCallback( - (newContentSize: ContentSize | undefined) => { - setInternalContentSize(newContentSize); - cachedImageDimensions.set(uri, newContentSize); + const [imageDimensions, setInternalImageDimensions] = useState(() => cachedDimensions.get(source)); + const setImageDimensions = useCallback( + (newDimensions: LightboxImageDimensions) => { + setInternalImageDimensions(newDimensions); + cachedDimensions.set(source, newDimensions); }, - [uri], - ); - const updateContentSize = useCallback( - ({nativeEvent: {width, height}}: ImageOnLoadEvent) => setContentSize({width: width * PixelRatio.get(), height: height * PixelRatio.get()}), - [setContentSize], + [source], ); - const contentLoaded = contentSize != null; - const isItemActive = index === activeIndex; const [isActive, setActive] = useState(isItemActive); + const [isImageLoaded, setImageLoaded] = useState(false); const isInactiveCarouselItem = hasSiblingCarouselItems && !isActive; const [isFallbackVisible, setFallbackVisible] = useState(isInactiveCarouselItem); - const [isFallbackImageLoaded, setFallbackImageLoaded] = useState(false); - const fallbackSize = useMemo(() => { - if (!hasSiblingCarouselItems || contentSize == null || canvasSize.width === 0 || canvasSize.height === 0) { - return DEFAULT_IMAGE_DIMENSIONS; - } - - const {minScale} = MultiGestureCanvasUtils.getCanvasFitScale({canvasSize, contentSize}); - - return { - width: PixelRatio.roundToNearestPixel(contentSize.width * minScale), - height: PixelRatio.roundToNearestPixel(contentSize.height * minScale), - }; - }, [canvasSize, hasSiblingCarouselItems, contentSize]); + const [isFallbackLoaded, setFallbackLoaded] = useState(false); + const [isLightboxLoaded, setLightboxLoaded] = useState(false); const isLightboxInRange = useMemo(() => { if (NUMBER_OF_CONCURRENT_LIGHTBOXES === -1) { return true; @@ -123,17 +101,24 @@ function Lightbox({ const indexOutOfRange = index > activeIndex + indexCanvasOffset || index < activeIndex - indexCanvasOffset; return !indexOutOfRange; }, [activeIndex, index]); - const [isLightboxImageLoaded, setLightboxImageLoaded] = useState(false); - const isLightboxVisible = isLightboxInRange && (isActive || isLightboxImageLoaded || isFallbackImageLoaded); + const isLightboxVisible = isLightboxInRange && (isActive || isLightboxLoaded || isFallbackLoaded); - // If the fallback image is currently visible, we want to hide the Lightbox until the fallback gets hidden, + // If the fallback image is currently visible, we want to hide the Lightbox until the fallback gets hidden, // so that we don't see two overlapping images at the same time. - // We cannot NOT render it, because we need to render the Lightbox to get the correct dimensions for the fallback image. // If there the Lightbox is not used within a carousel, we don't need to hide the Lightbox, // because it's only going to be rendered after the fallback image is hidden. const shouldHideLightbox = hasSiblingCarouselItems && isFallbackVisible; - const isLoading = isActive && (!isCanvasLoaded || !contentLoaded); + const isLoading = isActive && (!isContainerLoaded || !isImageLoaded); + + const updateCanvasSize = useCallback( + ({ + nativeEvent: { + layout: {width, height}, + }, + }: LayoutChangeEvent) => setContainerSize({width: PixelRatio.roundToNearestPixel(width), height: PixelRatio.roundToNearestPixel(height)}), + [], + ); // We delay setting a page to active state by a (few) millisecond(s), // to prevent the image transformer from flashing while still rendering @@ -150,8 +135,8 @@ function Lightbox({ if (isLightboxVisible) { return; } - setContentSize(undefined); - }, [isLightboxVisible, setContentSize]); + setImageLoaded(false); + }, [isLightboxVisible, setImageDimensions]); useEffect(() => { if (!hasSiblingCarouselItems) { @@ -159,47 +144,71 @@ function Lightbox({ } if (isActive) { - if (contentLoaded && isFallbackVisible) { + if (isImageLoaded && isFallbackVisible) { // We delay hiding the fallback image while image transformer is still rendering setTimeout(() => { setFallbackVisible(false); - setFallbackImageLoaded(false); + setFallbackLoaded(false); }, 100); } } else { - if (isLightboxVisible && isLightboxImageLoaded) { + if (isLightboxVisible && isLightboxLoaded) { return; } // Show fallback when the image goes out of focus or when the image is loading setFallbackVisible(true); } - }, [hasSiblingCarouselItems, isActive, isFallbackVisible, isLightboxImageLoaded, isLightboxVisible, contentLoaded]); + }, [hasSiblingCarouselItems, isActive, isFallbackVisible, isLightboxLoaded, isLightboxVisible, isImageLoaded]); + + const fallbackSize = useMemo(() => { + if (!hasSiblingCarouselItems || (imageDimensions?.lightboxSize == null && imageDimensions?.fallbackSize == null) || containerSize.width === 0 || containerSize.height === 0) { + return { + width: DEFAULT_IMAGE_SIZE, + height: DEFAULT_IMAGE_SIZE, + }; + } + + // If the lightbox size is undefined, th fallback size cannot be undefined, + // because we already checked for that before and would have returned early. + const imageSize = imageDimensions.lightboxSize || imageDimensions.fallbackSize!; + + const {minScale} = getCanvasFitScale({canvasSize: containerSize, contentSize: imageSize}); + + return { + width: PixelRatio.roundToNearestPixel(imageSize.width * minScale), + height: PixelRatio.roundToNearestPixel(imageSize.height * minScale), + }; + }, [containerSize, hasSiblingCarouselItems, imageDimensions]); return ( - {isCanvasLoaded && ( + {isContainerLoaded && ( <> {isLightboxVisible && ( setLightboxImageLoaded(true)} + onLoadEnd={() => setImageLoaded(true)} + onLoad={(e: ImageOnLoadEvent) => { + const width = (e.nativeEvent?.width || 0) * PixelRatio.get(); + const height = (e.nativeEvent?.height || 0) * PixelRatio.get(); + setImageDimensions({...imageDimensions, lightboxSize: {width, height}}); + }} /> @@ -209,12 +218,21 @@ function Lightbox({ {isFallbackVisible && ( setFallbackImageLoaded(true)} + onLoadEnd={() => setFallbackLoaded(true)} + onLoad={(e: ImageOnLoadEvent) => { + const width = (e.nativeEvent?.width || 0) * PixelRatio.get(); + const height = (e.nativeEvent?.height || 0) * PixelRatio.get(); + + if (imageDimensions?.lightboxSize != null) { + return; + } + + setImageDimensions({...imageDimensions, fallbackSize: {width, height}}); + }} /> )} diff --git a/src/components/MultiGestureCanvas/getCanvasFitScale.ts b/src/components/MultiGestureCanvas/getCanvasFitScale.ts new file mode 100644 index 000000000000..8fbb72e1f294 --- /dev/null +++ b/src/components/MultiGestureCanvas/getCanvasFitScale.ts @@ -0,0 +1,15 @@ +import type {CanvasSize, ContentSize} from './types'; + +type GetCanvasFitScale = (props: {canvasSize: CanvasSize; contentSize: ContentSize}) => {scaleX: number; scaleY: number; minScale: number; maxScale: number}; + +const getCanvasFitScale: GetCanvasFitScale = ({canvasSize, contentSize}) => { + const scaleX = canvasSize.width / contentSize.width; + const scaleY = canvasSize.height / contentSize.height; + + const minScale = Math.min(scaleX, scaleY); + const maxScale = Math.max(scaleX, scaleY); + + return {scaleX, scaleY, minScale, maxScale}; +}; + +export default getCanvasFitScale; diff --git a/src/components/MultiGestureCanvas/utils.ts b/src/components/MultiGestureCanvas/utils.ts index 4e377b3702d9..26e814313f8f 100644 --- a/src/components/MultiGestureCanvas/utils.ts +++ b/src/components/MultiGestureCanvas/utils.ts @@ -1,6 +1,5 @@ import {useCallback} from 'react'; import type {WorkletFunction} from 'react-native-reanimated/lib/typescript/reanimated2/commonTypes'; -import type {CanvasSize, ContentSize} from './types'; // The spring config is used to determine the physics of the spring animation // Details and a playground for testing different configs can be found at @@ -49,16 +48,4 @@ function useWorkletCallback( return useCallback<(...args: Args) => ReturnValue>(callback, deps) as WorkletFunction; } -type GetCanvasFitScale = (props: {canvasSize: CanvasSize; contentSize: ContentSize}) => {scaleX: number; scaleY: number; minScale: number; maxScale: number}; - -const getCanvasFitScale: GetCanvasFitScale = ({canvasSize, contentSize}) => { - const scaleX = canvasSize.width / contentSize.width; - const scaleY = canvasSize.height / contentSize.height; - - const minScale = Math.min(scaleX, scaleY); - const maxScale = Math.max(scaleX, scaleY); - - return {scaleX, scaleY, minScale, maxScale}; -}; - -export {SPRING_CONFIG, zoomScaleBounceFactors, clamp, useWorkletCallback, getCanvasFitScale}; +export {SPRING_CONFIG, zoomScaleBounceFactors, clamp, useWorkletCallback}; From 9429921fb69c00262b00c26350e4058fe6a1e40c Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 8 Jan 2024 11:35:25 +0100 Subject: [PATCH 045/107] fix: revert changes --- src/components/Lightbox.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/Lightbox.tsx b/src/components/Lightbox.tsx index 1684a489c0da..a673aa6c4b3e 100644 --- a/src/components/Lightbox.tsx +++ b/src/components/Lightbox.tsx @@ -4,9 +4,8 @@ import {ActivityIndicator, PixelRatio, StyleSheet, View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; import Image from './Image'; import MultiGestureCanvas, {defaultZoomRange} from './MultiGestureCanvas'; +import getCanvasFitScale from './MultiGestureCanvas/getCanvasFitScale'; import type {ContentSize, OnScaleChangedCallback, ZoomRange} from './MultiGestureCanvas/types'; -import * as MultiGestureCanvasUtils from './MultiGestureCanvas/utils'; -import getCanvasFitScale from '@components/MultiGestureCanvas/getCanvasFitScale'; // Increase/decrease this number to change the number of concurrent lightboxes // The more concurrent lighboxes, the worse performance gets (especially on low-end devices) @@ -22,7 +21,7 @@ type LightboxImageDimensions = { lightboxSize?: ContentSize; fallbackSize?: ContentSize; }; -} + const cachedDimensions = new Map(); type ImageOnLoadEvent = NativeSyntheticEvent; @@ -91,7 +90,7 @@ function Lightbox({ const [isFallbackVisible, setFallbackVisible] = useState(isInactiveCarouselItem); const [isFallbackLoaded, setFallbackLoaded] = useState(false); - const [isLightboxLoaded, setLightboxLoaded] = useState(false); + const isLightboxLoaded = imageDimensions?.lightboxSize != null; const isLightboxInRange = useMemo(() => { if (NUMBER_OF_CONCURRENT_LIGHTBOXES === -1) { return true; @@ -103,7 +102,7 @@ function Lightbox({ }, [activeIndex, index]); const isLightboxVisible = isLightboxInRange && (isActive || isLightboxLoaded || isFallbackLoaded); - // If the fallback image is currently visible, we want to hide the Lightbox until the fallback gets hidden, + // If the fallback image is currently visible, we want to hide the Lightbox until the fallback gets hidden, // so that we don't see two overlapping images at the same time. // If there the Lightbox is not used within a carousel, we don't need to hide the Lightbox, // because it's only going to be rendered after the fallback image is hidden. @@ -171,7 +170,8 @@ function Lightbox({ // If the lightbox size is undefined, th fallback size cannot be undefined, // because we already checked for that before and would have returned early. - const imageSize = imageDimensions.lightboxSize || imageDimensions.fallbackSize!; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const imageSize = imageDimensions.lightboxSize ?? imageDimensions.fallbackSize!; const {minScale} = getCanvasFitScale({canvasSize: containerSize, contentSize: imageSize}); From a869e6336acad911496d0bd9268a8222783a26ba Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 8 Jan 2024 11:36:36 +0100 Subject: [PATCH 046/107] fix: hook deps --- src/components/Lightbox.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Lightbox.tsx b/src/components/Lightbox.tsx index a673aa6c4b3e..a96afc9c741b 100644 --- a/src/components/Lightbox.tsx +++ b/src/components/Lightbox.tsx @@ -135,7 +135,7 @@ function Lightbox({ return; } setImageLoaded(false); - }, [isLightboxVisible, setImageDimensions]); + }, [isLightboxVisible]); useEffect(() => { if (!hasSiblingCarouselItems) { @@ -158,7 +158,7 @@ function Lightbox({ // Show fallback when the image goes out of focus or when the image is loading setFallbackVisible(true); } - }, [hasSiblingCarouselItems, isActive, isFallbackVisible, isLightboxLoaded, isLightboxVisible, isImageLoaded]); + }, [hasSiblingCarouselItems, isActive, isImageLoaded, isFallbackVisible, isLightboxLoaded, isLightboxVisible]); const fallbackSize = useMemo(() => { if (!hasSiblingCarouselItems || (imageDimensions?.lightboxSize == null && imageDimensions?.fallbackSize == null) || containerSize.width === 0 || containerSize.height === 0) { From b42422baf94f79c830b8f2dcfbe8a627d61016de Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 8 Jan 2024 11:37:26 +0100 Subject: [PATCH 047/107] more fixes --- src/components/Lightbox.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/Lightbox.tsx b/src/components/Lightbox.tsx index a96afc9c741b..db662e6e8776 100644 --- a/src/components/Lightbox.tsx +++ b/src/components/Lightbox.tsx @@ -198,9 +198,8 @@ function Lightbox({ zoomRange={zoomRange} > setImageLoaded(true)} From b10e6008b7d41b7c5e0d3ed9ea0d786b92f5fd62 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 8 Jan 2024 11:51:52 +0100 Subject: [PATCH 048/107] update propTypes --- .../MultiGestureCanvas/propTypes.js | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/src/components/MultiGestureCanvas/propTypes.js b/src/components/MultiGestureCanvas/propTypes.js index f1961ec0e156..4fba5123fee5 100644 --- a/src/components/MultiGestureCanvas/propTypes.js +++ b/src/components/MultiGestureCanvas/propTypes.js @@ -23,15 +23,6 @@ const zoomRangeDefaultProps = { const multiGestureCanvasPropTypes = { ...zoomRangePropTypes, - /** - * Wheter the canvas is currently active (in the screen) or not. - * Disables certain gestures and functionality - */ - isActive: PropTypes.bool, - - /** Handles scale changed event */ - onScaleChanged: PropTypes.func, - /** The width and height of the canvas. * This is needed in order to properly scale the content in the canvas */ @@ -48,15 +39,14 @@ const multiGestureCanvasPropTypes = { height: PropTypes.number, }), - /** The scale factors (scaleX, scaleY) that are used to scale the content (width/height) to the canvas size. - * `scaledWidth` and `scaledHeight` reflect the actual size of the content after scaling. + /** + * Wheter the canvas is currently active (in the screen) or not. + * Disables certain gestures and functionality */ - contentScaling: PropTypes.shape({ - scaleX: PropTypes.number, - scaleY: PropTypes.number, - scaledWidth: PropTypes.number, - scaledHeight: PropTypes.number, - }), + isActive: PropTypes.bool, + + /** Handles scale changed event */ + onScaleChanged: PropTypes.func, /** Content that should be transformed inside the canvas (images, pdf, ...) */ children: PropTypes.node.isRequired, @@ -67,7 +57,7 @@ const multiGestureCanvasDefaultProps = { onScaleChanged: () => undefined, contentSize: undefined, contentScaling: undefined, - zoomRange: undefined, + zoomRange: defaultZoomRange, }; export {defaultZoomRange, zoomRangePropTypes, zoomRangeDefaultProps, multiGestureCanvasPropTypes, multiGestureCanvasDefaultProps}; From f1a6f9bd1f0e7f0f36ffe1cc5b85af255ebb8b2e Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 8 Jan 2024 12:18:53 +0100 Subject: [PATCH 049/107] fix: import --- src/components/MultiGestureCanvas/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index 65a14d25a492..60a138b44606 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -8,6 +8,7 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import {defaultZoomRange} from './constants'; +import getCanvasFitScale from './getCanvasFitScale'; import type {CanvasSize, ContentSize, OnScaleChangedCallback, ZoomRange} from './types'; import usePanGesture from './usePanGesture'; import usePinchGesture from './usePinchGesture'; @@ -94,7 +95,7 @@ function MultiGestureCanvas({ // Based on the (original) content size and the canvas size, we calculate the horizontal and vertical scale factors // to fit the content inside the canvas // We later use the lower of the two scale factors to fit the content inside the canvas - const {minScale: minContentScale, maxScale: maxContentScale} = useMemo(() => MultiGestureCanvasUtils.getCanvasFitScale({canvasSize, contentSize}), [canvasSize, contentSize]); + const {minScale: minContentScale, maxScale: maxContentScale} = useMemo(() => getCanvasFitScale({canvasSize, contentSize}), [canvasSize, contentSize]); const zoomScale = useSharedValue(1); From 95b2e8429936b8fe84af87db46913c56f098233d Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 12 Jan 2024 17:09:12 +0100 Subject: [PATCH 050/107] remove propTypes --- src/components/ImageView/index.native.js | 15 ++++- .../MultiGestureCanvas/propTypes.js | 67 ------------------- 2 files changed, 12 insertions(+), 70 deletions(-) delete mode 100644 src/components/MultiGestureCanvas/propTypes.js diff --git a/src/components/ImageView/index.native.js b/src/components/ImageView/index.native.js index a94842b35219..82a5a1bdb978 100644 --- a/src/components/ImageView/index.native.js +++ b/src/components/ImageView/index.native.js @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import Lightbox from '@components/Lightbox'; -import {zoomRangeDefaultProps, zoomRangePropTypes} from '@components/MultiGestureCanvas/propTypes'; +import {defaultZoomRange} from '@components/MultiGestureCanvas'; import {imageViewDefaultProps, imageViewPropTypes} from './propTypes'; /** @@ -9,7 +9,12 @@ import {imageViewDefaultProps, imageViewPropTypes} from './propTypes'; */ const propTypes = { ...imageViewPropTypes, - ...zoomRangePropTypes, + + /** Range of zoom that can be applied to the content by pinching or double tapping. */ + zoomRange: PropTypes.shape({ + min: PropTypes.number, + max: PropTypes.number, + }), /** Function for handle on press */ onPress: PropTypes.func, @@ -20,7 +25,11 @@ const propTypes = { const defaultProps = { ...imageViewDefaultProps, - ...zoomRangeDefaultProps, + + zoomRange: { + min: defaultZoomRange.min, + max: defaultZoomRange.max, + }, onPress: () => {}, style: {}, diff --git a/src/components/MultiGestureCanvas/propTypes.js b/src/components/MultiGestureCanvas/propTypes.js deleted file mode 100644 index 392ea27a6533..000000000000 --- a/src/components/MultiGestureCanvas/propTypes.js +++ /dev/null @@ -1,67 +0,0 @@ -import PropTypes from 'prop-types'; - -const defaultZoomRange = { - min: 1, - max: 20, -}; - -const zoomRangePropTypes = { - /** Range of zoom that can be applied to the content by pinching or double tapping. */ - zoomRange: PropTypes.shape({ - min: PropTypes.number, - max: PropTypes.number, - }), -}; - -const zoomRangeDefaultProps = { - zoomRange: { - min: defaultZoomRange.min, - max: defaultZoomRange.max, - }, -}; - -const multiGestureCanvasPropTypes = { - ...zoomRangePropTypes, - - /** The width and height of the canvas. - * This is needed in order to properly scale the content in the canvas - /** - * Wheter the canvas is currently active (in the screen) or not. - * Disables certain gestures and functionality - */ - isActive: PropTypes.bool, - - /** Handles scale changed event */ - onScaleChanged: PropTypes.func, - - /** - * The width and height of the canvas. - * This is needed in order to properly scale the content in the canvas - */ - canvasSize: PropTypes.shape({ - width: PropTypes.number.isRequired, - height: PropTypes.number.isRequired, - }).isRequired, - - /** - * The width and height of the content. - * This is needed in order to properly scale the content in the canvas - */ - contentSize: PropTypes.shape({ - width: PropTypes.number, - height: PropTypes.number, - }), - - /** Content that should be transformed inside the canvas (images, pdf, ...) */ - children: PropTypes.node.isRequired, -}; - -const multiGestureCanvasDefaultProps = { - isActive: true, - onScaleChanged: () => undefined, - contentSize: undefined, - contentScaling: undefined, - zoomRange: defaultZoomRange, -}; - -export {defaultZoomRange, zoomRangePropTypes, zoomRangeDefaultProps, multiGestureCanvasPropTypes, multiGestureCanvasDefaultProps}; From b21c8a719bf8e2aeaec7864f114de244c7a871e8 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 15 Jan 2024 12:04:04 +0100 Subject: [PATCH 051/107] Update src/components/MultiGestureCanvas/useTapGestures.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Błażej Kustra <46095609+blazejkustra@users.noreply.github.com> --- src/components/MultiGestureCanvas/useTapGestures.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MultiGestureCanvas/useTapGestures.ts b/src/components/MultiGestureCanvas/useTapGestures.ts index 217981a1238d..18439e07e626 100644 --- a/src/components/MultiGestureCanvas/useTapGestures.ts +++ b/src/components/MultiGestureCanvas/useTapGestures.ts @@ -123,7 +123,7 @@ const useTapGestures = ({ .maxDistance(20) .onEnd((evt) => { // If the content is already zoomed, we want to reset the zoom, - // otherwwise we want to zoom in + // otherwise we want to zoom in if (zoomScale.value > 1) { reset(true); } else { From e944dc212c5eb2080ad0e3d133cffd4d84df6641 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 15 Jan 2024 13:01:32 +0100 Subject: [PATCH 052/107] address GH comments --- .../Pager/AttachmentCarouselPagerContext.ts | 2 +- .../AttachmentCarousel/Pager/index.tsx | 36 +++------- .../attachmentCarouselPropTypes.js | 4 -- .../BaseAttachmentViewPdf.js | 2 +- src/components/ImageView/index.native.js | 12 +--- src/components/Lightbox.tsx | 65 ++++++++----------- .../MultiGestureCanvas/constants.ts | 21 +++++- src/components/MultiGestureCanvas/index.tsx | 26 ++++---- src/components/MultiGestureCanvas/types.ts | 7 +- .../MultiGestureCanvas/usePanGesture.ts | 5 +- .../MultiGestureCanvas/usePinchGesture.ts | 18 +++-- .../MultiGestureCanvas/useTapGestures.ts | 11 ++-- src/components/MultiGestureCanvas/utils.ts | 29 +-------- src/styles/utils/index.ts | 7 +- 14 files changed, 102 insertions(+), 143 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts index e595b8c5c4d1..aff8b4a0cae9 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -3,7 +3,7 @@ import type PagerView from 'react-native-pager-view'; import type {SharedValue} from 'react-native-reanimated'; type AttachmentCarouselPagerContextType = { - onTap: () => void; + onTap?: () => void; onScaleChanged: (scale: number) => void; pagerRef: React.Ref; shouldPagerScroll: SharedValue; diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index f61b3c160d67..0e41c4be56b3 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -1,24 +1,19 @@ +import type {Ref} from 'react'; import React, {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 type {AnimatedProps} from 'react-native-reanimated'; import Animated, {runOnJS, useAnimatedProps, useAnimatedReaction, useSharedValue} from 'react-native-reanimated'; import useThemeStyles from '@hooks/useThemeStyles'; import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext'; import usePageScrollHandler from './usePageScrollHandler'; -type PagerViewPropsObject = { - [K in keyof PagerViewProps]: PagerViewProps[K]; -}; - -type AnimatedNativeWrapperComponent

, C> = React.ForwardRefExoticComponent< - React.PropsWithoutRef & NativeViewGestureHandlerProps> & React.RefAttributes +const WrappedPagerView = createNativeWrapper(PagerView) as React.ForwardRefExoticComponent< + PagerViewProps & NativeViewGestureHandlerProps & React.RefAttributes> >; - -const AnimatedPagerView = Animated.createAnimatedComponent(createNativeWrapper(PagerView)) as AnimatedNativeWrapperComponent; +const AnimatedPagerView = Animated.createAnimatedComponent(WrappedPagerView); type AttachmentCarouselPagerHandle = { setPage: (selectedPage: number) => void; @@ -30,17 +25,15 @@ type PagerItem = { source: string; }; -type AttachmentCarouselPagerProps = React.PropsWithChildren<{ +type AttachmentCarouselPagerProps = { items: PagerItem[]; renderItem: (props: {item: PagerItem; index: number; isActive: boolean}) => React.ReactNode; initialIndex: number; onPageSelected: () => void; - onTap: () => void; onScaleChanged: (scale: number) => void; - forwardedRef: React.Ref; -}>; +}; -function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelected, onTap, onScaleChanged, forwardedRef}: AttachmentCarouselPagerProps) { +function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelected, onScaleChanged}: AttachmentCarouselPagerProps, ref: Ref) { const styles = useThemeStyles(); const shouldPagerScroll = useSharedValue(true); const pagerRef = useRef(null); @@ -81,7 +74,7 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte ); useImperativeHandle( - forwardedRef, + ref, () => ({ setPage: (selectedPage) => { pagerRef.current?.setPage(selectedPage); @@ -96,13 +89,12 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte const contextValue = useMemo( () => ({ - onTap, onScaleChanged, pagerRef, shouldPagerScroll, isSwipingInPager, }), - [isSwipingInPager, shouldPagerScroll, onScaleChanged, onTap], + [isSwipingInPager, shouldPagerScroll, onScaleChanged], ); return ( @@ -131,12 +123,4 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte } AttachmentCarouselPager.displayName = 'AttachmentCarouselPager'; -const AttachmentCarouselPagerWithRef = React.forwardRef>((props, ref) => ( - -)); - -export default AttachmentCarouselPagerWithRef; +export default React.forwardRef(AttachmentCarouselPager); diff --git a/src/components/Attachments/AttachmentCarousel/attachmentCarouselPropTypes.js b/src/components/Attachments/AttachmentCarousel/attachmentCarouselPropTypes.js index 72a554de68be..5aa665683162 100644 --- a/src/components/Attachments/AttachmentCarousel/attachmentCarouselPropTypes.js +++ b/src/components/Attachments/AttachmentCarousel/attachmentCarouselPropTypes.js @@ -10,9 +10,6 @@ const propTypes = { /** Callback to update the parent modal's state with a source and name from the attachments array */ onNavigate: PropTypes.func, - /** Callback to close carousel when user swipes down (on native) */ - onClose: PropTypes.func, - /** Function to change the download button Visibility */ setDownloadButtonVisibility: PropTypes.func, @@ -39,7 +36,6 @@ const defaultProps = { parentReportActions: {}, transaction: {}, onNavigate: () => {}, - onClose: () => {}, setDownloadButtonVisibility: () => {}, }; diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js index de14f848c37e..022a89753476 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js @@ -21,7 +21,7 @@ function BaseAttachmentViewPdf({ if (!attachmentCarouselPagerContext) { return; } - attachmentCarouselPagerContext.onPinchGestureChange(false); + attachmentCarouselPagerContext.onScaleChanged(1); // eslint-disable-next-line react-hooks/exhaustive-deps -- we just want to call this function when component is mounted }, []); diff --git a/src/components/ImageView/index.native.js b/src/components/ImageView/index.native.js index 82a5a1bdb978..ba10162ec1e2 100644 --- a/src/components/ImageView/index.native.js +++ b/src/components/ImageView/index.native.js @@ -16,9 +16,6 @@ const propTypes = { max: PropTypes.number, }), - /** Function for handle on press */ - onPress: PropTypes.func, - /** Additional styles to add to the component */ style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), }; @@ -26,16 +23,12 @@ const propTypes = { const defaultProps = { ...imageViewDefaultProps, - zoomRange: { - min: defaultZoomRange.min, - max: defaultZoomRange.max, - }, + zoomRange: defaultZoomRange, - onPress: () => {}, style: {}, }; -function ImageView({isAuthTokenRequired, url, onScaleChanged, onPress, style, zoomRange, onError, isUsedInCarousel, isSingleCarouselItem, carouselItemIndex, carouselActiveItemIndex}) { +function ImageView({isAuthTokenRequired, url, onScaleChanged, style, zoomRange, onError, isUsedInCarousel, isSingleCarouselItem, carouselItemIndex, carouselActiveItemIndex}) { const hasSiblingCarouselItems = isUsedInCarousel && !isSingleCarouselItem; return ( @@ -44,7 +37,6 @@ function ImageView({isAuthTokenRequired, url, onScaleChanged, onPress, style, zo zoomRange={zoomRange} isAuthTokenRequired={isAuthTokenRequired} onScaleChanged={onScaleChanged} - onPress={onPress} onError={onError} index={carouselItemIndex} activeIndex={carouselActiveItemIndex} diff --git a/src/components/Lightbox.tsx b/src/components/Lightbox.tsx index db662e6e8776..f7eccdf3724f 100644 --- a/src/components/Lightbox.tsx +++ b/src/components/Lightbox.tsx @@ -1,5 +1,5 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import type {ImageSourcePropType, LayoutChangeEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native'; +import type {LayoutChangeEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native'; import {ActivityIndicator, PixelRatio, StyleSheet, View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; import Image from './Image'; @@ -9,20 +9,17 @@ import type {ContentSize, OnScaleChangedCallback, ZoomRange} from './MultiGestur // Increase/decrease this number to change the number of concurrent lightboxes // The more concurrent lighboxes, the worse performance gets (especially on low-end devices) -// -1 means unlimited -// We need to define a type for this constant and therefore ignore this ESLint error, although the type is inferable, -// because otherwise TS will throw an error later in the code since "-1" and this constant have no overlap. -// We can safely ignore this error, because we might change the value in the future -// eslint-disable-next-line @typescript-eslint/no-inferrable-types -const NUMBER_OF_CONCURRENT_LIGHTBOXES: number = 3; +type LightboxConcurrencyLimit = number | 'UNLIMITED'; +const NUMBER_OF_CONCURRENT_LIGHTBOXES: LightboxConcurrencyLimit = 3; const DEFAULT_IMAGE_SIZE = 200; +const DEFAULT_IMAGE_DIMENSION: ContentSize = {width: DEFAULT_IMAGE_SIZE, height: DEFAULT_IMAGE_SIZE}; type LightboxImageDimensions = { lightboxSize?: ContentSize; fallbackSize?: ContentSize; }; -const cachedDimensions = new Map(); +const cachedDimensions = new Map(); type ImageOnLoadEvent = NativeSyntheticEvent; @@ -31,10 +28,10 @@ type LightboxProps = { isAuthTokenRequired: boolean; /** URI to full-sized attachment, SVG function, or numeric static image on native platforms */ - source: ImageSourcePropType; + uri: string; /** Triggers whenever the zoom scale changes */ - onScaleChanged: OnScaleChangedCallback; + onScaleChanged?: OnScaleChangedCallback; /** Handles errors while displaying the image */ onError: () => void; @@ -60,7 +57,7 @@ type LightboxProps = { */ function Lightbox({ isAuthTokenRequired = false, - source, + uri, onScaleChanged, onError, style, @@ -71,16 +68,16 @@ function Lightbox({ }: LightboxProps) { const StyleUtils = useStyleUtils(); - const [containerSize, setContainerSize] = useState({width: 0, height: 0}); + const [containerSize, setContainerSize] = useState({width: 0, height: 0}); const isContainerLoaded = containerSize.width !== 0 && containerSize.height !== 0; - const [imageDimensions, setInternalImageDimensions] = useState(() => cachedDimensions.get(source)); + const [imageDimensions, setInternalImageDimensions] = useState(() => cachedDimensions.get(uri)); const setImageDimensions = useCallback( (newDimensions: LightboxImageDimensions) => { setInternalImageDimensions(newDimensions); - cachedDimensions.set(source, newDimensions); + cachedDimensions.set(uri, newDimensions); }, - [source], + [uri], ); const isItemActive = index === activeIndex; const [isActive, setActive] = useState(isItemActive); @@ -90,9 +87,9 @@ function Lightbox({ const [isFallbackVisible, setFallbackVisible] = useState(isInactiveCarouselItem); const [isFallbackLoaded, setFallbackLoaded] = useState(false); - const isLightboxLoaded = imageDimensions?.lightboxSize != null; + const isLightboxLoaded = imageDimensions?.lightboxSize !== undefined; const isLightboxInRange = useMemo(() => { - if (NUMBER_OF_CONCURRENT_LIGHTBOXES === -1) { + if (NUMBER_OF_CONCURRENT_LIGHTBOXES === 'UNLIMITED') { return true; } @@ -161,17 +158,11 @@ function Lightbox({ }, [hasSiblingCarouselItems, isActive, isImageLoaded, isFallbackVisible, isLightboxLoaded, isLightboxVisible]); const fallbackSize = useMemo(() => { - if (!hasSiblingCarouselItems || (imageDimensions?.lightboxSize == null && imageDimensions?.fallbackSize == null) || containerSize.width === 0 || containerSize.height === 0) { - return { - width: DEFAULT_IMAGE_SIZE, - height: DEFAULT_IMAGE_SIZE, - }; - } + const imageSize = imageDimensions?.lightboxSize ?? imageDimensions?.fallbackSize; - // If the lightbox size is undefined, th fallback size cannot be undefined, - // because we already checked for that before and would have returned early. - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const imageSize = imageDimensions.lightboxSize ?? imageDimensions.fallbackSize!; + if (!hasSiblingCarouselItems || !imageSize || !isContainerLoaded) { + return DEFAULT_IMAGE_DIMENSION; + } const {minScale} = getCanvasFitScale({canvasSize: containerSize, contentSize: imageSize}); @@ -179,7 +170,7 @@ function Lightbox({ width: PixelRatio.roundToNearestPixel(imageSize.width * minScale), height: PixelRatio.roundToNearestPixel(imageSize.height * minScale), }; - }, [containerSize, hasSiblingCarouselItems, imageDimensions]); + }, [containerSize, hasSiblingCarouselItems, imageDimensions?.fallbackSize, imageDimensions?.lightboxSize, isContainerLoaded]); return ( {isLightboxVisible && ( - + setImageLoaded(true)} onLoad={(e: ImageOnLoadEvent) => { - const width = (e.nativeEvent?.width || 0) * PixelRatio.get(); - const height = (e.nativeEvent?.height || 0) * PixelRatio.get(); + const width = e.nativeEvent.width * PixelRatio.get(); + const height = e.nativeEvent.height * PixelRatio.get(); setImageDimensions({...imageDimensions, lightboxSize: {width, height}}); }} /> @@ -217,16 +208,16 @@ function Lightbox({ {isFallbackVisible && ( setFallbackLoaded(true)} onLoad={(e: ImageOnLoadEvent) => { - const width = (e.nativeEvent?.width || 0) * PixelRatio.get(); - const height = (e.nativeEvent?.height || 0) * PixelRatio.get(); + const width = e.nativeEvent.width * PixelRatio.get(); + const height = e.nativeEvent.height * PixelRatio.get(); - if (imageDimensions?.lightboxSize != null) { + if (isLightboxLoaded) { return; } diff --git a/src/components/MultiGestureCanvas/constants.ts b/src/components/MultiGestureCanvas/constants.ts index 0103d07c55c2..1d3e143c970c 100644 --- a/src/components/MultiGestureCanvas/constants.ts +++ b/src/components/MultiGestureCanvas/constants.ts @@ -1,11 +1,26 @@ -const defaultZoomRange = { +import type {WithSpringConfig} from 'react-native-reanimated'; +import type {ZoomRange} from './types'; + +// The spring config is used to determine the physics of the spring animation +// Details and a playground for testing different configs can be found at +// https://docs.swmansion.com/react-native-reanimated/docs/animations/withSpring +const SPRING_CONFIG: WithSpringConfig = { + mass: 1, + stiffness: 1000, + damping: 500, +}; + +// The default zoom range within the user can pinch to zoom the content inside the canvas +const defaultZoomRange: Required = { min: 1, max: 20, }; -const zoomScaleBounceFactors = { +// The zoom scale bounce factors are used to determine the amount of bounce +// that is allowed when the user zooms more than the min or max zoom levels +const zoomScaleBounceFactors: Required = { min: 0.7, max: 1.5, }; -export {defaultZoomRange, zoomScaleBounceFactors}; +export {SPRING_CONFIG, defaultZoomRange, zoomScaleBounceFactors}; diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index 60a138b44606..7388d1942dad 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -7,7 +7,7 @@ import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCa import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; -import {defaultZoomRange} from './constants'; +import {defaultZoomRange, SPRING_CONFIG} from './constants'; import getCanvasFitScale from './getCanvasFitScale'; import type {CanvasSize, ContentSize, OnScaleChangedCallback, ZoomRange} from './types'; import usePanGesture from './usePanGesture'; @@ -22,9 +22,6 @@ type MultiGestureCanvasProps = ChildrenProps & { */ isActive: boolean; - /** Handles scale changed event */ - onScaleChanged: OnScaleChangedCallback; - /** The width and height of the canvas. * This is needed in order to properly scale the content in the canvas */ @@ -37,6 +34,9 @@ type MultiGestureCanvasProps = ChildrenProps & { /** Range of zoom that can be applied to the content by pinching or double tapping. */ zoomRange?: ZoomRange; + + /** Handles scale changed event */ + onScaleChanged?: OnScaleChangedCallback; }; function MultiGestureCanvas({ @@ -78,7 +78,7 @@ function MultiGestureCanvas({ */ const onScaleChanged = useCallback( (newScale: number) => { - onScaleChangedProp(newScale); + onScaleChangedProp?.(newScale); onScaleChangedContext(newScale); }, [onScaleChangedContext, onScaleChangedProp], @@ -135,13 +135,13 @@ function MultiGestureCanvas({ pinchScale.value = 1; if (animated) { - offsetX.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); - offsetY.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); - panTranslateX.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); - panTranslateY.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); - pinchTranslateX.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); - pinchTranslateY.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); - zoomScale.value = withSpring(1, MultiGestureCanvasUtils.SPRING_CONFIG); + offsetX.value = withSpring(0, SPRING_CONFIG); + offsetY.value = withSpring(0, SPRING_CONFIG); + panTranslateX.value = withSpring(0, SPRING_CONFIG); + panTranslateY.value = withSpring(0, SPRING_CONFIG); + pinchTranslateX.value = withSpring(0, SPRING_CONFIG); + pinchTranslateY.value = withSpring(0, SPRING_CONFIG); + zoomScale.value = withSpring(1, SPRING_CONFIG); return; } @@ -270,5 +270,5 @@ MultiGestureCanvas.displayName = 'MultiGestureCanvas'; export default MultiGestureCanvas; export {defaultZoomRange}; -export {zoomScaleBounceFactors} from './utils'; +export {zoomScaleBounceFactors} from './constants'; export type {MultiGestureCanvasProps}; diff --git a/src/components/MultiGestureCanvas/types.ts b/src/components/MultiGestureCanvas/types.ts index 0309a6cbcdfc..b4de586cd9da 100644 --- a/src/components/MultiGestureCanvas/types.ts +++ b/src/components/MultiGestureCanvas/types.ts @@ -20,7 +20,10 @@ type ZoomRange = { }; /** Triggered whenever the scale of the MultiGestureCanvas changes */ -type OnScaleChangedCallback = (zoomScale: number) => void; +type OnScaleChangedCallback = ((zoomScale: number) => void) | undefined; + +/** Triggered when the canvas is tapped (single tap) */ +type OnTapCallback = (() => void) | undefined; /** Types used of variables used within the MultiGestureCanvas component and it's hooks */ type MultiGestureCanvasVariables = { @@ -38,7 +41,7 @@ type MultiGestureCanvasVariables = { pinchTranslateY: SharedValue; stopAnimation: WorkletFunction<[], void>; reset: WorkletFunction<[boolean], void>; - onTap: () => void; + onTap: OnTapCallback; }; export type {CanvasSize, ContentSize, ZoomRange, OnScaleChangedCallback, MultiGestureCanvasVariables}; diff --git a/src/components/MultiGestureCanvas/usePanGesture.ts b/src/components/MultiGestureCanvas/usePanGesture.ts index 7e0aff08368f..7f7c2152d126 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.ts +++ b/src/components/MultiGestureCanvas/usePanGesture.ts @@ -2,6 +2,7 @@ import type {PanGesture} from 'react-native-gesture-handler'; import {Gesture} from 'react-native-gesture-handler'; import {useDerivedValue, useSharedValue, withDecay, withSpring} from 'react-native-reanimated'; +import {SPRING_CONFIG} from './constants'; import type {CanvasSize, ContentSize, MultiGestureCanvasVariables} from './types'; import * as MultiGestureCanvasUtils from './utils'; @@ -95,7 +96,7 @@ const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, } } else { // Animated back to the boundary - offsetX.value = withSpring(clampedOffset.x, MultiGestureCanvasUtils.SPRING_CONFIG); + offsetX.value = withSpring(clampedOffset.x, SPRING_CONFIG); } if (isInVerticalBoundary) { @@ -110,7 +111,7 @@ const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, } } else { // Animated back to the boundary - offsetY.value = withSpring(clampedOffset.y, MultiGestureCanvasUtils.SPRING_CONFIG); + offsetY.value = withSpring(clampedOffset.y, SPRING_CONFIG); } // Reset velocity variables after we finished the pan gesture diff --git a/src/components/MultiGestureCanvas/usePinchGesture.ts b/src/components/MultiGestureCanvas/usePinchGesture.ts index 50c256933af5..d149316f6e85 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.ts +++ b/src/components/MultiGestureCanvas/usePinchGesture.ts @@ -3,6 +3,7 @@ import {useEffect, useState} from 'react'; import type {PinchGesture} from 'react-native-gesture-handler'; import {Gesture} from 'react-native-gesture-handler'; import {runOnJS, useAnimatedReaction, useSharedValue, withSpring} from 'react-native-reanimated'; +import {SPRING_CONFIG, zoomScaleBounceFactors} from './constants'; import type {CanvasSize, MultiGestureCanvasVariables, OnScaleChangedCallback, ZoomRange} from './types'; import * as MultiGestureCanvasUtils from './utils'; @@ -114,14 +115,11 @@ const usePinchGesture = ({ const newZoomScale = pinchScale.value * evt.scale; // Limit the zoom scale to zoom range including bounce range - if ( - zoomScale.value >= zoomRange.min * MultiGestureCanvasUtils.zoomScaleBounceFactors.min && - zoomScale.value <= zoomRange.max * MultiGestureCanvasUtils.zoomScaleBounceFactors.max - ) { + if (zoomScale.value >= zoomRange.min * zoomScaleBounceFactors.min && zoomScale.value <= zoomRange.max * zoomScaleBounceFactors.max) { zoomScale.value = newZoomScale; currentPinchScale.value = evt.scale; - if (onScaleChanged != null) { + if (onScaleChanged !== undefined) { runOnJS(onScaleChanged)(zoomScale.value); } } @@ -153,12 +151,12 @@ const usePinchGesture = ({ // If the content was "overzoomed" or "underzoomed", we need to bounce back with an animation if (pinchBounceTranslateX.value !== 0 || pinchBounceTranslateY.value !== 0) { - pinchBounceTranslateX.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); - pinchBounceTranslateY.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); + pinchBounceTranslateX.value = withSpring(0, SPRING_CONFIG); + pinchBounceTranslateY.value = withSpring(0, SPRING_CONFIG); } const triggerScaleChangeCallback = () => { - if (onScaleChanged == null) { + if (onScaleChanged === undefined) { return; } @@ -168,11 +166,11 @@ const usePinchGesture = ({ if (zoomScale.value < zoomRange.min) { // If the zoom scale is less than the minimum zoom scale, we need to set the zoom scale to the minimum pinchScale.value = zoomRange.min; - zoomScale.value = withSpring(zoomRange.min, MultiGestureCanvasUtils.SPRING_CONFIG, triggerScaleChangeCallback); + zoomScale.value = withSpring(zoomRange.min, SPRING_CONFIG, triggerScaleChangeCallback); } else if (zoomScale.value > zoomRange.max) { // If the zoom scale is higher than the maximum zoom scale, we need to set the zoom scale to the maximum pinchScale.value = zoomRange.max; - zoomScale.value = withSpring(zoomRange.max, MultiGestureCanvasUtils.SPRING_CONFIG, triggerScaleChangeCallback); + zoomScale.value = withSpring(zoomRange.max, SPRING_CONFIG, triggerScaleChangeCallback); } else { // Otherwise, we just update the pinch scale offset pinchScale.value = zoomScale.value; diff --git a/src/components/MultiGestureCanvas/useTapGestures.ts b/src/components/MultiGestureCanvas/useTapGestures.ts index 18439e07e626..ba928d08349c 100644 --- a/src/components/MultiGestureCanvas/useTapGestures.ts +++ b/src/components/MultiGestureCanvas/useTapGestures.ts @@ -3,6 +3,7 @@ import {useMemo} from 'react'; import type {TapGesture} from 'react-native-gesture-handler'; import {Gesture} from 'react-native-gesture-handler'; import {runOnJS, withSpring} from 'react-native-reanimated'; +import {SPRING_CONFIG} from './constants'; import type {CanvasSize, ContentSize, MultiGestureCanvasVariables, OnScaleChangedCallback} from './types'; import * as MultiGestureCanvasUtils from './utils'; @@ -109,9 +110,9 @@ const useTapGestures = ({ offsetAfterZooming.y = 0; } - offsetX.value = withSpring(offsetAfterZooming.x, MultiGestureCanvasUtils.SPRING_CONFIG); - offsetY.value = withSpring(offsetAfterZooming.y, MultiGestureCanvasUtils.SPRING_CONFIG); - zoomScale.value = withSpring(doubleTapScale, MultiGestureCanvasUtils.SPRING_CONFIG); + offsetX.value = withSpring(offsetAfterZooming.x, SPRING_CONFIG); + offsetY.value = withSpring(offsetAfterZooming.y, SPRING_CONFIG); + zoomScale.value = withSpring(doubleTapScale, SPRING_CONFIG); pinchScale.value = doubleTapScale; }, [scaledContentWidth, scaledContentHeight, canvasSize, doubleTapScale], @@ -130,7 +131,7 @@ const useTapGestures = ({ zoomToCoordinates(evt.x, evt.y); } - if (onScaleChanged != null) { + if (onScaleChanged !== undefined) { runOnJS(onScaleChanged)(zoomScale.value); } }); @@ -143,7 +144,7 @@ const useTapGestures = ({ }) // eslint-disable-next-line @typescript-eslint/naming-convention .onFinalize((_evt, success) => { - if (!success || !onTap) { + if (!success || onTap === undefined) { return; } diff --git a/src/components/MultiGestureCanvas/utils.ts b/src/components/MultiGestureCanvas/utils.ts index 26e814313f8f..d6d018f5be25 100644 --- a/src/components/MultiGestureCanvas/utils.ts +++ b/src/components/MultiGestureCanvas/utils.ts @@ -1,29 +1,7 @@ import {useCallback} from 'react'; import type {WorkletFunction} from 'react-native-reanimated/lib/typescript/reanimated2/commonTypes'; -// The spring config is used to determine the physics of the spring animation -// Details and a playground for testing different configs can be found at -// https://docs.swmansion.com/react-native-reanimated/docs/animations/withSpring -const SPRING_CONFIG = { - mass: 1, - stiffness: 1000, - damping: 500, -}; - -// The zoom scale bounce factors are used to determine the amount of bounce -// that is allowed when the user zooms more than the min or max zoom levels -const zoomScaleBounceFactors = { - min: 0.7, - max: 1.5, -}; - -/** - * Clamps a value between a lower and upper bound - * @param value - * @param lowerBound - * @param upperBound - * @returns - */ +/** Clamps a value between a lower and upper bound */ function clamp(value: number, lowerBound: number, upperBound: number) { 'worklet'; @@ -33,9 +11,6 @@ function clamp(value: number, lowerBound: number, upperBound: number) { /** * Creates a memoized callback on the UI thread * Same as `useWorkletCallback` from `react-native-reanimated` but without the deprecation warning - * @param callback - * @param deps - * @returns */ // eslint-disable-next-line @typescript-eslint/ban-types function useWorkletCallback( @@ -48,4 +23,4 @@ function useWorkletCallback( return useCallback<(...args: Args) => ReturnValue>(callback, deps) as WorkletFunction; } -export {SPRING_CONFIG, zoomScaleBounceFactors, clamp, useWorkletCallback}; +export {clamp, useWorkletCallback}; diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 449489a33cce..059bc227393b 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1008,6 +1008,10 @@ function getTransparentColor(color: string) { return `${color}00`; } +function getOpacityStyle(isHidden: boolean) { + return {opacity: isHidden ? 0 : 1}; +} + const staticStyleUtils = { positioning, combineStyles, @@ -1071,6 +1075,7 @@ const staticStyleUtils = { getEReceiptColorCode, getNavigationModalCardStyle, getCardStyles, + getOpacityStyle, }; const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ @@ -1432,8 +1437,6 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ }, getFullscreenCenteredContentStyles: () => [StyleSheet.absoluteFill, styles.justifyContentCenter, styles.alignItemsCenter], - - getLightboxVisibilityStyle: (isHidden: boolean) => ({opacity: isHidden ? 0 : 1}), }); type StyleUtilsType = ReturnType; From 905ba178ec28a20fc29b80569a7f2d10a8759a55 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 16 Jan 2024 13:43:11 +0100 Subject: [PATCH 053/107] improve and clean up PR --- .../Pager/AttachmentCarouselPagerContext.ts | 5 +-- .../AttachmentCarousel/Pager/index.tsx | 4 +-- .../BaseAttachmentViewPdf.js | 2 +- src/components/Lightbox.tsx | 2 +- .../MultiGestureCanvas/constants.ts | 8 ++--- .../MultiGestureCanvas/getCanvasFitScale.ts | 15 --------- src/components/MultiGestureCanvas/index.tsx | 22 ++++++------- src/components/MultiGestureCanvas/types.ts | 15 +++++---- .../MultiGestureCanvas/usePanGesture.ts | 24 +++++--------- .../MultiGestureCanvas/usePinchGesture.ts | 30 ++++++----------- .../MultiGestureCanvas/useTapGestures.ts | 24 ++++---------- src/components/MultiGestureCanvas/utils.ts | 32 ++++++++----------- 12 files changed, 69 insertions(+), 114 deletions(-) delete mode 100644 src/components/MultiGestureCanvas/getCanvasFitScale.ts diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts index aff8b4a0cae9..ae318a8c7eef 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -1,11 +1,12 @@ +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 AttachmentCarouselPagerContextType = { - onTap?: () => void; + onTap: () => void; onScaleChanged: (scale: number) => void; - pagerRef: React.Ref; + pagerRef: ForwardedRef; shouldPagerScroll: SharedValue; isSwipingInPager: SharedValue; }; diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index 0e41c4be56b3..687ed64d9ca3 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -1,4 +1,4 @@ -import type {Ref} from 'react'; +import type {ForwardedRef} from 'react'; import React, {useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import type {NativeViewGestureHandlerProps} from 'react-native-gesture-handler'; @@ -33,7 +33,7 @@ type AttachmentCarouselPagerProps = { onScaleChanged: (scale: number) => void; }; -function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelected, onScaleChanged}: AttachmentCarouselPagerProps, ref: Ref) { +function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelected, onScaleChanged}: AttachmentCarouselPagerProps, ref: ForwardedRef) { const styles = useThemeStyles(); const shouldPagerScroll = useSharedValue(true); const pagerRef = useRef(null); diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js index 022a89753476..0c3b8835186b 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js @@ -33,7 +33,7 @@ function BaseAttachmentViewPdf({ if (isUsedInCarousel && attachmentCarouselPagerContext) { const shouldPagerScroll = scale === 1; - attachmentCarouselPagerContext.onPinchGestureChange(!shouldPagerScroll); + attachmentCarouselPagerContext.onScaleChanged(1); if (attachmentCarouselPagerContext.shouldPagerScroll.value === shouldPagerScroll) { return; diff --git a/src/components/Lightbox.tsx b/src/components/Lightbox.tsx index f7eccdf3724f..6bf57ab4d9a8 100644 --- a/src/components/Lightbox.tsx +++ b/src/components/Lightbox.tsx @@ -27,7 +27,7 @@ type LightboxProps = { /** Whether source url requires authentication */ isAuthTokenRequired: boolean; - /** URI to full-sized attachment, SVG function, or numeric static image on native platforms */ + /** URI to full-sized attachment */ uri: string; /** Triggers whenever the zoom scale changes */ diff --git a/src/components/MultiGestureCanvas/constants.ts b/src/components/MultiGestureCanvas/constants.ts index 1d3e143c970c..7dba3e568ea4 100644 --- a/src/components/MultiGestureCanvas/constants.ts +++ b/src/components/MultiGestureCanvas/constants.ts @@ -11,16 +11,16 @@ const SPRING_CONFIG: WithSpringConfig = { }; // The default zoom range within the user can pinch to zoom the content inside the canvas -const defaultZoomRange: Required = { +const DEFAULT_ZOOM_RANGE: Required = { min: 1, max: 20, }; -// The zoom scale bounce factors are used to determine the amount of bounce +// The zoom range bounce factors are used to determine the amount of bounce // that is allowed when the user zooms more than the min or max zoom levels -const zoomScaleBounceFactors: Required = { +const ZOOM_RANGE_BOUNCE_FACTORS: Required = { min: 0.7, max: 1.5, }; -export {SPRING_CONFIG, defaultZoomRange, zoomScaleBounceFactors}; +export {SPRING_CONFIG, DEFAULT_ZOOM_RANGE, ZOOM_RANGE_BOUNCE_FACTORS}; diff --git a/src/components/MultiGestureCanvas/getCanvasFitScale.ts b/src/components/MultiGestureCanvas/getCanvasFitScale.ts deleted file mode 100644 index 8fbb72e1f294..000000000000 --- a/src/components/MultiGestureCanvas/getCanvasFitScale.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type {CanvasSize, ContentSize} from './types'; - -type GetCanvasFitScale = (props: {canvasSize: CanvasSize; contentSize: ContentSize}) => {scaleX: number; scaleY: number; minScale: number; maxScale: number}; - -const getCanvasFitScale: GetCanvasFitScale = ({canvasSize, contentSize}) => { - const scaleX = canvasSize.width / contentSize.width; - const scaleY = canvasSize.height / contentSize.height; - - const minScale = Math.min(scaleX, scaleY); - const maxScale = Math.max(scaleX, scaleY); - - return {scaleX, scaleY, minScale, maxScale}; -}; - -export default getCanvasFitScale; diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index 7388d1942dad..aba12c2eec59 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -2,13 +2,12 @@ import React, {useCallback, useContext, useEffect, useMemo, useRef} from 'react' import {View} from 'react-native'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; import type PagerView from 'react-native-pager-view'; -import Animated, {cancelAnimation, runOnUI, useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, withSpring} from 'react-native-reanimated'; +import Animated, {cancelAnimation, runOnUI, useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, useWorkletCallback, withSpring} from 'react-native-reanimated'; import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; -import {defaultZoomRange, SPRING_CONFIG} from './constants'; -import getCanvasFitScale from './getCanvasFitScale'; +import {DEFAULT_ZOOM_RANGE, SPRING_CONFIG, ZOOM_RANGE_BOUNCE_FACTORS} from './constants'; import type {CanvasSize, ContentSize, OnScaleChangedCallback, ZoomRange} from './types'; import usePanGesture from './usePanGesture'; import usePinchGesture from './usePinchGesture'; @@ -20,7 +19,7 @@ type MultiGestureCanvasProps = ChildrenProps & { * Wheter the canvas is currently active (in the screen) or not. * Disables certain gestures and functionality */ - isActive: boolean; + isActive?: boolean; /** The width and height of the canvas. * This is needed in order to properly scale the content in the canvas @@ -33,7 +32,7 @@ type MultiGestureCanvasProps = ChildrenProps & { contentSize?: ContentSize; /** Range of zoom that can be applied to the content by pinching or double tapping. */ - zoomRange?: ZoomRange; + zoomRange?: Partial; /** Handles scale changed event */ onScaleChanged?: OnScaleChangedCallback; @@ -86,8 +85,8 @@ function MultiGestureCanvas({ const zoomRange = useMemo( () => ({ - min: zoomRangeProp?.min ?? defaultZoomRange.min, - max: zoomRangeProp?.max ?? defaultZoomRange.max, + min: zoomRangeProp?.min ?? DEFAULT_ZOOM_RANGE.min, + max: zoomRangeProp?.max ?? DEFAULT_ZOOM_RANGE.max, }), [zoomRangeProp?.max, zoomRangeProp?.min], ); @@ -95,7 +94,7 @@ function MultiGestureCanvas({ // Based on the (original) content size and the canvas size, we calculate the horizontal and vertical scale factors // to fit the content inside the canvas // We later use the lower of the two scale factors to fit the content inside the canvas - const {minScale: minContentScale, maxScale: maxContentScale} = useMemo(() => getCanvasFitScale({canvasSize, contentSize}), [canvasSize, contentSize]); + const {minScale: minContentScale, maxScale: maxContentScale} = useMemo(() => MultiGestureCanvasUtils.getCanvasFitScale({canvasSize, contentSize}), [canvasSize, contentSize]); const zoomScale = useSharedValue(1); @@ -119,7 +118,7 @@ function MultiGestureCanvas({ /** * Stops any currently running decay animation from panning */ - const stopAnimation = MultiGestureCanvasUtils.useWorkletCallback(() => { + const stopAnimation = useWorkletCallback(() => { cancelAnimation(offsetX); cancelAnimation(offsetY); }); @@ -127,7 +126,7 @@ function MultiGestureCanvas({ /** * Resets the canvas to the initial state and animates back smoothly */ - const reset = MultiGestureCanvasUtils.useWorkletCallback((animated: boolean) => { + const reset = useWorkletCallback((animated: boolean) => { pinchScale.value = 1; stopAnimation(); @@ -269,6 +268,5 @@ function MultiGestureCanvas({ MultiGestureCanvas.displayName = 'MultiGestureCanvas'; export default MultiGestureCanvas; -export {defaultZoomRange}; -export {zoomScaleBounceFactors} from './constants'; +export {DEFAULT_ZOOM_RANGE, ZOOM_RANGE_BOUNCE_FACTORS}; export type {MultiGestureCanvasProps}; diff --git a/src/components/MultiGestureCanvas/types.ts b/src/components/MultiGestureCanvas/types.ts index b4de586cd9da..d0f8a775f2aa 100644 --- a/src/components/MultiGestureCanvas/types.ts +++ b/src/components/MultiGestureCanvas/types.ts @@ -1,5 +1,4 @@ import type {SharedValue} from 'react-native-reanimated'; -import type {WorkletFunction} from 'react-native-reanimated/lib/typescript/reanimated2/commonTypes'; /** Dimensions of the canvas rendered by the MultiGestureCanvas */ type CanvasSize = { @@ -15,8 +14,8 @@ type ContentSize = { /** Range of zoom that can be applied to the content by pinching or double tapping. */ type ZoomRange = { - min?: number; - max?: number; + min: number; + max: number; }; /** Triggered whenever the scale of the MultiGestureCanvas changes */ @@ -27,6 +26,9 @@ type OnTapCallback = (() => void) | undefined; /** Types used of variables used within the MultiGestureCanvas component and it's hooks */ type MultiGestureCanvasVariables = { + canvasSize: CanvasSize; + contentSize: ContentSize; + zoomRange: ZoomRange; minContentScale: number; maxContentScale: number; isSwipingInPager: SharedValue; @@ -39,9 +41,10 @@ type MultiGestureCanvasVariables = { panTranslateY: SharedValue; pinchTranslateX: SharedValue; pinchTranslateY: SharedValue; - stopAnimation: WorkletFunction<[], void>; - reset: WorkletFunction<[boolean], void>; - onTap: OnTapCallback; + stopAnimation: () => void; + reset: (animated: boolean) => void; + onTap: OnTapCallback | undefined; + onScaleChanged: OnScaleChangedCallback | undefined; }; export type {CanvasSize, ContentSize, ZoomRange, OnScaleChangedCallback, MultiGestureCanvasVariables}; diff --git a/src/components/MultiGestureCanvas/usePanGesture.ts b/src/components/MultiGestureCanvas/usePanGesture.ts index 7f7c2152d126..3ef791ad64b0 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.ts +++ b/src/components/MultiGestureCanvas/usePanGesture.ts @@ -1,9 +1,9 @@ /* eslint-disable no-param-reassign */ import type {PanGesture} from 'react-native-gesture-handler'; import {Gesture} from 'react-native-gesture-handler'; -import {useDerivedValue, useSharedValue, withDecay, withSpring} from 'react-native-reanimated'; +import {useDerivedValue, useSharedValue, useWorkletCallback, withDecay, withSpring} from 'react-native-reanimated'; import {SPRING_CONFIG} from './constants'; -import type {CanvasSize, ContentSize, MultiGestureCanvasVariables} from './types'; +import type {MultiGestureCanvasVariables} from './types'; import * as MultiGestureCanvasUtils from './utils'; // This value determines how fast the pan animation should phase out @@ -11,18 +11,10 @@ import * as MultiGestureCanvasUtils from './utils'; // https://docs.swmansion.com/react-native-reanimated/docs/animations/withDecay/ const PAN_DECAY_DECELARATION = 0.9915; -type UsePanGestureProps = { - canvasSize: CanvasSize; - contentSize: ContentSize; - zoomScale: MultiGestureCanvasVariables['zoomScale']; - totalScale: MultiGestureCanvasVariables['totalScale']; - offsetX: MultiGestureCanvasVariables['offsetX']; - offsetY: MultiGestureCanvasVariables['offsetY']; - panTranslateX: MultiGestureCanvasVariables['panTranslateX']; - panTranslateY: MultiGestureCanvasVariables['panTranslateY']; - isSwipingInPager: MultiGestureCanvasVariables['isSwipingInPager']; - stopAnimation: MultiGestureCanvasVariables['stopAnimation']; -}; +type UsePanGestureProps = Pick< + MultiGestureCanvasVariables, + 'canvasSize' | 'contentSize' | 'zoomScale' | 'totalScale' | 'offsetX' | 'offsetY' | 'panTranslateX' | 'panTranslateY' | 'isSwipingInPager' | 'stopAnimation' +>; const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, offsetY, panTranslateX, panTranslateY, isSwipingInPager, stopAnimation}: UsePanGestureProps): PanGesture => { // The content size after fitting it to the canvas and zooming @@ -37,7 +29,7 @@ const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, // Calculates bounds of the scaled content // Can we pan left/right/up/down // Can be used to limit gesture or implementing tension effect - const getBounds = MultiGestureCanvasUtils.useWorkletCallback(() => { + const getBounds = useWorkletCallback(() => { let horizontalBoundary = 0; let verticalBoundary = 0; @@ -73,7 +65,7 @@ const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, // We want to smoothly decay/end the gesture by phasing out the pan animation // In case the content is outside of the boundaries of the canvas, // we need to move the content back into the boundaries - const finishPanGesture = MultiGestureCanvasUtils.useWorkletCallback(() => { + const finishPanGesture = useWorkletCallback(() => { // If the content is centered within the canvas, we don't need to run any animations if (offsetX.value === 0 && offsetY.value === 0 && panTranslateX.value === 0 && panTranslateY.value === 0) { return; diff --git a/src/components/MultiGestureCanvas/usePinchGesture.ts b/src/components/MultiGestureCanvas/usePinchGesture.ts index d149316f6e85..a2a1f58864d6 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.ts +++ b/src/components/MultiGestureCanvas/usePinchGesture.ts @@ -2,24 +2,14 @@ import {useEffect, useState} from 'react'; import type {PinchGesture} from 'react-native-gesture-handler'; import {Gesture} from 'react-native-gesture-handler'; -import {runOnJS, useAnimatedReaction, useSharedValue, withSpring} from 'react-native-reanimated'; -import {SPRING_CONFIG, zoomScaleBounceFactors} from './constants'; -import type {CanvasSize, MultiGestureCanvasVariables, OnScaleChangedCallback, ZoomRange} from './types'; -import * as MultiGestureCanvasUtils from './utils'; - -type UsePinchGestureProps = { - canvasSize: CanvasSize; - zoomScale: MultiGestureCanvasVariables['zoomScale']; - zoomRange: Required; - offsetX: MultiGestureCanvasVariables['offsetX']; - offsetY: MultiGestureCanvasVariables['offsetY']; - pinchTranslateX: MultiGestureCanvasVariables['pinchTranslateX']; - pinchTranslateY: MultiGestureCanvasVariables['pinchTranslateY']; - pinchScale: MultiGestureCanvasVariables['pinchScale']; - isSwipingInPager: MultiGestureCanvasVariables['isSwipingInPager']; - stopAnimation: MultiGestureCanvasVariables['stopAnimation']; - onScaleChanged: OnScaleChangedCallback; -}; +import {runOnJS, useAnimatedReaction, useSharedValue, useWorkletCallback, withSpring} from 'react-native-reanimated'; +import {SPRING_CONFIG, ZOOM_RANGE_BOUNCE_FACTORS} from './constants'; +import type {MultiGestureCanvasVariables} from './types'; + +type UsePinchGestureProps = Pick< + MultiGestureCanvasVariables, + 'canvasSize' | 'zoomScale' | 'zoomRange' | 'offsetX' | 'offsetY' | 'pinchTranslateX' | 'pinchTranslateY' | 'pinchScale' | 'isSwipingInPager' | 'stopAnimation' | 'onScaleChanged' +>; const usePinchGesture = ({ canvasSize, @@ -67,7 +57,7 @@ const usePinchGesture = ({ * Calculates the adjusted focal point of the pinch gesture, * based on the canvas size and the current offset */ - const getAdjustedFocal = MultiGestureCanvasUtils.useWorkletCallback( + const getAdjustedFocal = useWorkletCallback( (focalX: number, focalY: number) => ({ x: focalX - (canvasSize.width / 2 + offsetX.value), y: focalY - (canvasSize.height / 2 + offsetY.value), @@ -115,7 +105,7 @@ const usePinchGesture = ({ const newZoomScale = pinchScale.value * evt.scale; // Limit the zoom scale to zoom range including bounce range - if (zoomScale.value >= zoomRange.min * zoomScaleBounceFactors.min && zoomScale.value <= zoomRange.max * zoomScaleBounceFactors.max) { + if (zoomScale.value >= zoomRange.min * ZOOM_RANGE_BOUNCE_FACTORS.min && zoomScale.value <= zoomRange.max * ZOOM_RANGE_BOUNCE_FACTORS.max) { zoomScale.value = newZoomScale; currentPinchScale.value = evt.scale; diff --git a/src/components/MultiGestureCanvas/useTapGestures.ts b/src/components/MultiGestureCanvas/useTapGestures.ts index ba928d08349c..0a1102a4c9a3 100644 --- a/src/components/MultiGestureCanvas/useTapGestures.ts +++ b/src/components/MultiGestureCanvas/useTapGestures.ts @@ -2,27 +2,17 @@ import {useMemo} from 'react'; import type {TapGesture} from 'react-native-gesture-handler'; import {Gesture} from 'react-native-gesture-handler'; -import {runOnJS, withSpring} from 'react-native-reanimated'; +import {runOnJS, useWorkletCallback, withSpring} from 'react-native-reanimated'; import {SPRING_CONFIG} from './constants'; -import type {CanvasSize, ContentSize, MultiGestureCanvasVariables, OnScaleChangedCallback} from './types'; +import type {MultiGestureCanvasVariables} from './types'; import * as MultiGestureCanvasUtils from './utils'; const DOUBLE_TAP_SCALE = 3; -type UseTapGesturesProps = { - canvasSize: CanvasSize; - contentSize: ContentSize; - minContentScale: MultiGestureCanvasVariables['minContentScale']; - maxContentScale: MultiGestureCanvasVariables['maxContentScale']; - offsetX: MultiGestureCanvasVariables['offsetX']; - offsetY: MultiGestureCanvasVariables['offsetY']; - pinchScale: MultiGestureCanvasVariables['pinchScale']; - zoomScale: MultiGestureCanvasVariables['zoomScale']; - reset: MultiGestureCanvasVariables['reset']; - stopAnimation: MultiGestureCanvasVariables['stopAnimation']; - onScaleChanged: OnScaleChangedCallback; - onTap: MultiGestureCanvasVariables['onTap']; -}; +type UseTapGesturesProps = Pick< + MultiGestureCanvasVariables, + 'canvasSize' | 'contentSize' | 'minContentScale' | 'maxContentScale' | 'offsetX' | 'offsetY' | 'pinchScale' | 'zoomScale' | 'reset' | 'stopAnimation' | 'onScaleChanged' | 'onTap' +>; const useTapGestures = ({ canvasSize, @@ -45,7 +35,7 @@ const useTapGestures = ({ // On double tap the content should be zoomed to fill, but at least zoomed by DOUBLE_TAP_SCALE const doubleTapScale = useMemo(() => Math.max(DOUBLE_TAP_SCALE, maxContentScale / minContentScale), [maxContentScale, minContentScale]); - const zoomToCoordinates = MultiGestureCanvasUtils.useWorkletCallback( + const zoomToCoordinates = useWorkletCallback( (focalX: number, focalY: number) => { 'worklet'; diff --git a/src/components/MultiGestureCanvas/utils.ts b/src/components/MultiGestureCanvas/utils.ts index d6d018f5be25..e5688489c048 100644 --- a/src/components/MultiGestureCanvas/utils.ts +++ b/src/components/MultiGestureCanvas/utils.ts @@ -1,5 +1,16 @@ -import {useCallback} from 'react'; -import type {WorkletFunction} from 'react-native-reanimated/lib/typescript/reanimated2/commonTypes'; +import type {CanvasSize, ContentSize} from './types'; + +type GetCanvasFitScale = (props: {canvasSize: CanvasSize; contentSize: ContentSize}) => {scaleX: number; scaleY: number; minScale: number; maxScale: number}; + +const getCanvasFitScale: GetCanvasFitScale = ({canvasSize, contentSize}) => { + const scaleX = canvasSize.width / contentSize.width; + const scaleY = canvasSize.height / contentSize.height; + + const minScale = Math.min(scaleX, scaleY); + const maxScale = Math.max(scaleX, scaleY); + + return {scaleX, scaleY, minScale, maxScale}; +}; /** Clamps a value between a lower and upper bound */ function clamp(value: number, lowerBound: number, upperBound: number) { @@ -8,19 +19,4 @@ function clamp(value: number, lowerBound: number, upperBound: number) { return Math.min(Math.max(lowerBound, value), upperBound); } -/** - * Creates a memoized callback on the UI thread - * Same as `useWorkletCallback` from `react-native-reanimated` but without the deprecation warning - */ -// eslint-disable-next-line @typescript-eslint/ban-types -function useWorkletCallback( - callback: Parameters ReturnValue>>[0], - deps: Parameters ReturnValue>>[1] = [], -): WorkletFunction { - 'worklet'; - - // eslint-disable-next-line react-hooks/exhaustive-deps - return useCallback<(...args: Args) => ReturnValue>(callback, deps) as WorkletFunction; -} - -export {clamp, useWorkletCallback}; +export {getCanvasFitScale, clamp}; From 71135aa3970d195e48d8ab15eed144c0117be885 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 16 Jan 2024 14:55:00 +0100 Subject: [PATCH 054/107] more improvements --- src/components/Lightbox.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/Lightbox.tsx b/src/components/Lightbox.tsx index 6bf57ab4d9a8..5b554b84fe33 100644 --- a/src/components/Lightbox.tsx +++ b/src/components/Lightbox.tsx @@ -3,9 +3,9 @@ import type {LayoutChangeEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from import {ActivityIndicator, PixelRatio, StyleSheet, View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; import Image from './Image'; -import MultiGestureCanvas, {defaultZoomRange} from './MultiGestureCanvas'; -import getCanvasFitScale from './MultiGestureCanvas/getCanvasFitScale'; +import MultiGestureCanvas, {DEFAULT_ZOOM_RANGE} from './MultiGestureCanvas'; import type {ContentSize, OnScaleChangedCallback, ZoomRange} from './MultiGestureCanvas/types'; +import {getCanvasFitScale} from './MultiGestureCanvas/utils'; // Increase/decrease this number to change the number of concurrent lightboxes // The more concurrent lighboxes, the worse performance gets (especially on low-end devices) @@ -25,7 +25,7 @@ type ImageOnLoadEvent = NativeSyntheticEvent; type LightboxProps = { /** Whether source url requires authentication */ - isAuthTokenRequired: boolean; + isAuthTokenRequired?: boolean; /** URI to full-sized attachment */ uri: string; @@ -37,19 +37,19 @@ type LightboxProps = { onError: () => void; /** Additional styles to add to the component */ - style: StyleProp; + style?: StyleProp; /** The index of the carousel item */ - index: number; + index?: number; /** The index of the currently active carousel item */ - activeIndex: number; + activeIndex?: number; /** Whether the Lightbox is used within a carousel component and there are other sibling elements */ - hasSiblingCarouselItems: boolean; + hasSiblingCarouselItems?: boolean; /** Range of zoom that can be applied to the content by pinching or double tapping. */ - zoomRange: ZoomRange; + zoomRange?: Partial; }; /** @@ -64,7 +64,7 @@ function Lightbox({ index = 0, activeIndex = 0, hasSiblingCarouselItems = false, - zoomRange = defaultZoomRange, + zoomRange = DEFAULT_ZOOM_RANGE, }: LightboxProps) { const StyleUtils = useStyleUtils(); From 397c1aa82866f08bd35b693baaf18aa509c7fb31 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 16 Jan 2024 14:56:13 +0100 Subject: [PATCH 055/107] rename context type --- .../Pager/AttachmentCarouselPagerContext.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts index ae318a8c7eef..b901fa0eacf0 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -3,7 +3,7 @@ import {createContext} from 'react'; import type PagerView from 'react-native-pager-view'; import type {SharedValue} from 'react-native-reanimated'; -type AttachmentCarouselPagerContextType = { +type AttachmentCarouselPagerContextValue = { onTap: () => void; onScaleChanged: (scale: number) => void; pagerRef: ForwardedRef; @@ -11,7 +11,7 @@ type AttachmentCarouselPagerContextType = { isSwipingInPager: SharedValue; }; -const AttachmentCarouselPagerContext = createContext(null); +const AttachmentCarouselPagerContext = createContext(null); export default AttachmentCarouselPagerContext; -export type {AttachmentCarouselPagerContextType}; +export type {AttachmentCarouselPagerContextValue}; From 993afa86543c3e0ba17f550f1bce9c1ce899bbb7 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 16 Jan 2024 15:09:48 +0100 Subject: [PATCH 056/107] further address comments --- .../Pager/AttachmentCarouselPagerContext.ts | 1 - src/styles/utils/index.ts | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts index b901fa0eacf0..8ce3d41b97f9 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -4,7 +4,6 @@ import type PagerView from 'react-native-pager-view'; import type {SharedValue} from 'react-native-reanimated'; type AttachmentCarouselPagerContextValue = { - onTap: () => void; onScaleChanged: (scale: number) => void; pagerRef: ForwardedRef; shouldPagerScroll: SharedValue; diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 059bc227393b..0b5acb959dd3 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1008,8 +1008,8 @@ function getTransparentColor(color: string) { return `${color}00`; } -function getOpacityStyle(isHidden: boolean) { - return {opacity: isHidden ? 0 : 1}; +function getOpacityStyle(opacity: number) { + return {opacity}; } const staticStyleUtils = { From 74343c5f277a84816a180a36d32f7266c82cf2c5 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 16 Jan 2024 15:20:21 +0100 Subject: [PATCH 057/107] simplify Lightbox from https://github.com/Expensify/App/pull/34080 --- src/components/Lightbox.tsx | 129 ++++++++++++++++-------------------- 1 file changed, 56 insertions(+), 73 deletions(-) diff --git a/src/components/Lightbox.tsx b/src/components/Lightbox.tsx index 5b554b84fe33..3c092b5b4840 100644 --- a/src/components/Lightbox.tsx +++ b/src/components/Lightbox.tsx @@ -14,15 +14,10 @@ const NUMBER_OF_CONCURRENT_LIGHTBOXES: LightboxConcurrencyLimit = 3; const DEFAULT_IMAGE_SIZE = 200; const DEFAULT_IMAGE_DIMENSION: ContentSize = {width: DEFAULT_IMAGE_SIZE, height: DEFAULT_IMAGE_SIZE}; -type LightboxImageDimensions = { - lightboxSize?: ContentSize; - fallbackSize?: ContentSize; -}; - -const cachedDimensions = new Map(); - type ImageOnLoadEvent = NativeSyntheticEvent; +const cachedImageDimensions = new Map(); + type LightboxProps = { /** Whether source url requires authentication */ isAuthTokenRequired?: boolean; @@ -68,26 +63,50 @@ function Lightbox({ }: LightboxProps) { const StyleUtils = useStyleUtils(); - const [containerSize, setContainerSize] = useState({width: 0, height: 0}); - const isContainerLoaded = containerSize.width !== 0 && containerSize.height !== 0; + const [canvasSize, setCanvasSize] = useState({width: 0, height: 0}); + const isCanvasLoaded = canvasSize.width !== 0 && canvasSize.height !== 0; + const updateCanvasSize = useCallback( + ({ + nativeEvent: { + layout: {width, height}, + }, + }: LayoutChangeEvent) => setCanvasSize({width: PixelRatio.roundToNearestPixel(width), height: PixelRatio.roundToNearestPixel(height)}), + [], + ); - const [imageDimensions, setInternalImageDimensions] = useState(() => cachedDimensions.get(uri)); - const setImageDimensions = useCallback( - (newDimensions: LightboxImageDimensions) => { - setInternalImageDimensions(newDimensions); - cachedDimensions.set(uri, newDimensions); + const [contentSize, setInternalContentSize] = useState(() => cachedImageDimensions.get(uri)); + const setContentSize = useCallback( + (newDimensions: ContentSize | undefined) => { + setInternalContentSize(newDimensions); + cachedImageDimensions.set(uri, newDimensions); }, [uri], ); + const updateContentSize = useCallback( + ({nativeEvent: {width, height}}: ImageOnLoadEvent) => setContentSize({width: width * PixelRatio.get(), height: height * PixelRatio.get()}), + [setContentSize], + ); + const contentLoaded = contentSize != null; + const isItemActive = index === activeIndex; const [isActive, setActive] = useState(isItemActive); - const [isImageLoaded, setImageLoaded] = useState(false); - const isInactiveCarouselItem = hasSiblingCarouselItems && !isActive; + const [isFallbackVisible, setFallbackVisible] = useState(isInactiveCarouselItem); - const [isFallbackLoaded, setFallbackLoaded] = useState(false); + const [isFallbackImageLoaded, setFallbackImageLoaded] = useState(false); + const fallbackSize = useMemo(() => { + if (!hasSiblingCarouselItems || !contentSize || !isCanvasLoaded) { + return DEFAULT_IMAGE_DIMENSION; + } + + const {minScale} = getCanvasFitScale({canvasSize, contentSize}); + + return { + width: PixelRatio.roundToNearestPixel(contentSize.width * minScale), + height: PixelRatio.roundToNearestPixel(contentSize.height * minScale), + }; + }, [hasSiblingCarouselItems, contentSize, isCanvasLoaded, canvasSize]); - const isLightboxLoaded = imageDimensions?.lightboxSize !== undefined; const isLightboxInRange = useMemo(() => { if (NUMBER_OF_CONCURRENT_LIGHTBOXES === 'UNLIMITED') { return true; @@ -97,24 +116,16 @@ function Lightbox({ const indexOutOfRange = index > activeIndex + indexCanvasOffset || index < activeIndex - indexCanvasOffset; return !indexOutOfRange; }, [activeIndex, index]); - const isLightboxVisible = isLightboxInRange && (isActive || isLightboxLoaded || isFallbackLoaded); + const [isLightboxImageLoaded, setLightboxImageLoaded] = useState(false); + const isLightboxVisible = isLightboxInRange && (isActive || isLightboxImageLoaded || isFallbackImageLoaded); // If the fallback image is currently visible, we want to hide the Lightbox until the fallback gets hidden, // so that we don't see two overlapping images at the same time. // If there the Lightbox is not used within a carousel, we don't need to hide the Lightbox, // because it's only going to be rendered after the fallback image is hidden. - const shouldHideLightbox = hasSiblingCarouselItems && isFallbackVisible; - - const isLoading = isActive && (!isContainerLoaded || !isImageLoaded); + const shouldShowLightbox = !hasSiblingCarouselItems || !isFallbackVisible; - const updateCanvasSize = useCallback( - ({ - nativeEvent: { - layout: {width, height}, - }, - }: LayoutChangeEvent) => setContainerSize({width: PixelRatio.roundToNearestPixel(width), height: PixelRatio.roundToNearestPixel(height)}), - [], - ); + const isLoading = isActive && (!isCanvasLoaded || !contentLoaded); // We delay setting a page to active state by a (few) millisecond(s), // to prevent the image transformer from flashing while still rendering @@ -131,8 +142,8 @@ function Lightbox({ if (isLightboxVisible) { return; } - setImageLoaded(false); - }, [isLightboxVisible]); + setContentSize(undefined); + }, [isLightboxVisible, setContentSize]); useEffect(() => { if (!hasSiblingCarouselItems) { @@ -140,65 +151,46 @@ function Lightbox({ } if (isActive) { - if (isImageLoaded && isFallbackVisible) { + if (contentLoaded && isFallbackVisible) { // We delay hiding the fallback image while image transformer is still rendering setTimeout(() => { setFallbackVisible(false); - setFallbackLoaded(false); + setFallbackImageLoaded(false); }, 100); } } else { - if (isLightboxVisible && isLightboxLoaded) { + if (isLightboxVisible && isLightboxImageLoaded) { return; } // Show fallback when the image goes out of focus or when the image is loading setFallbackVisible(true); } - }, [hasSiblingCarouselItems, isActive, isImageLoaded, isFallbackVisible, isLightboxLoaded, isLightboxVisible]); - - const fallbackSize = useMemo(() => { - const imageSize = imageDimensions?.lightboxSize ?? imageDimensions?.fallbackSize; - - if (!hasSiblingCarouselItems || !imageSize || !isContainerLoaded) { - return DEFAULT_IMAGE_DIMENSION; - } - - const {minScale} = getCanvasFitScale({canvasSize: containerSize, contentSize: imageSize}); - - return { - width: PixelRatio.roundToNearestPixel(imageSize.width * minScale), - height: PixelRatio.roundToNearestPixel(imageSize.height * minScale), - }; - }, [containerSize, hasSiblingCarouselItems, imageDimensions?.fallbackSize, imageDimensions?.lightboxSize, isContainerLoaded]); + }, [contentLoaded, hasSiblingCarouselItems, isActive, isFallbackVisible, isLightboxImageLoaded, isLightboxVisible]); return ( - {isContainerLoaded && ( + {isCanvasLoaded && ( <> {isLightboxVisible && ( - + setImageLoaded(true)} - onLoad={(e: ImageOnLoadEvent) => { - const width = e.nativeEvent.width * PixelRatio.get(); - const height = e.nativeEvent.height * PixelRatio.get(); - setImageDimensions({...imageDimensions, lightboxSize: {width, height}}); - }} + onLoad={updateContentSize} + onLoadEnd={() => setLightboxImageLoaded(true)} /> @@ -212,17 +204,8 @@ function Lightbox({ resizeMode="contain" style={fallbackSize} isAuthTokenRequired={isAuthTokenRequired} - onLoadEnd={() => setFallbackLoaded(true)} - onLoad={(e: ImageOnLoadEvent) => { - const width = e.nativeEvent.width * PixelRatio.get(); - const height = e.nativeEvent.height * PixelRatio.get(); - - if (isLightboxLoaded) { - return; - } - - setImageDimensions({...imageDimensions, fallbackSize: {width, height}}); - }} + onLoad={updateContentSize} + onLoadEnd={() => setFallbackImageLoaded(true)} /> )} From 27aeea97851e7e3badb5c97b8ccdf6f5ddd3aefe Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 16 Jan 2024 15:25:58 +0100 Subject: [PATCH 058/107] fix: arrow buttons --- .../Pager/AttachmentCarouselPagerContext.ts | 1 + .../AttachmentCarousel/Pager/index.tsx | 8 +++++--- .../AttachmentCarousel/index.native.js | 15 ++++++++------- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts index 8ce3d41b97f9..b901fa0eacf0 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -4,6 +4,7 @@ import type PagerView from 'react-native-pager-view'; import type {SharedValue} from 'react-native-reanimated'; type AttachmentCarouselPagerContextValue = { + onTap: () => void; onScaleChanged: (scale: number) => void; pagerRef: ForwardedRef; shouldPagerScroll: SharedValue; diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index 687ed64d9ca3..efe360213d4d 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -29,11 +29,12 @@ type AttachmentCarouselPagerProps = { items: PagerItem[]; renderItem: (props: {item: PagerItem; index: number; isActive: boolean}) => React.ReactNode; initialIndex: number; + onTap: () => void; onPageSelected: () => void; onScaleChanged: (scale: number) => void; }; -function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelected, onScaleChanged}: AttachmentCarouselPagerProps, ref: ForwardedRef) { +function AttachmentCarouselPager({items, renderItem, initialIndex, onTap, onPageSelected, onScaleChanged}: AttachmentCarouselPagerProps, ref: ForwardedRef) { const styles = useThemeStyles(); const shouldPagerScroll = useSharedValue(true); const pagerRef = useRef(null); @@ -89,12 +90,13 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte const contextValue = useMemo( () => ({ - onScaleChanged, pagerRef, shouldPagerScroll, isSwipingInPager, + onTap, + onScaleChanged, }), - [isSwipingInPager, shouldPagerScroll, onScaleChanged], + [shouldPagerScroll, isSwipingInPager, onTap, onScaleChanged], ); return ( diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js index 8f168093c217..062a5da05ce2 100644 --- a/src/components/Attachments/AttachmentCarousel/index.native.js +++ b/src/components/Attachments/AttachmentCarousel/index.native.js @@ -101,10 +101,9 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, index={index} activeIndex={page} isFocused={isActive && activeSource === item.source} - onPress={() => setShouldShowArrows(!shouldShowArrows)} /> ), - [activeSource, attachments.length, page, setShouldShowArrows, shouldShowArrows], + [activeSource, attachments.length, page], ); const handleScaleChange = useCallback( @@ -122,11 +121,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, ); return ( - setShouldShowArrows(true)} - onMouseLeave={() => setShouldShowArrows(false)} - > + {page == null ? ( ) : ( @@ -154,6 +149,12 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, items={attachments} renderItem={renderItem} initialIndex={page} + onTap={() => { + if (!isZoomedOut) { + return; + } + setShouldShowArrows(!shouldShowArrows); + }} onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)} onScaleChanged={handleScaleChange} ref={pagerRef} From 6d92147e1b31c6b4bb510e98a26db4a7b8ff6488 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 16 Jan 2024 16:48:25 +0100 Subject: [PATCH 059/107] fix: pager not swiping --- .../Pager/AttachmentCarouselPagerContext.ts | 5 +- .../AttachmentCarousel/Pager/index.tsx | 64 +++++++------------ .../AttachmentCarousel/index.native.js | 1 + src/components/MultiGestureCanvas/index.tsx | 47 +++++++------- src/components/MultiGestureCanvas/types.ts | 4 +- .../MultiGestureCanvas/usePanGesture.ts | 8 +-- .../MultiGestureCanvas/usePinchGesture.ts | 34 +++++----- .../MultiGestureCanvas/useTapGestures.ts | 24 ++++--- 8 files changed, 87 insertions(+), 100 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts index b901fa0eacf0..f23153f5e2e2 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -6,9 +6,8 @@ import type {SharedValue} from 'react-native-reanimated'; type AttachmentCarouselPagerContextValue = { onTap: () => void; onScaleChanged: (scale: number) => void; - pagerRef: ForwardedRef; - shouldPagerScroll: SharedValue; - isSwipingInPager: SharedValue; + pagerRef: ForwardedRef; // + isPagerSwiping: SharedValue; }; const AttachmentCarouselPagerContext = createContext(null); diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index efe360213d4d..0331fc4044ce 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -5,7 +5,7 @@ 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, {runOnJS, useAnimatedProps, useAnimatedReaction, useSharedValue} from 'react-native-reanimated'; +import Animated, {useSharedValue} from 'react-native-reanimated'; import useThemeStyles from '@hooks/useThemeStyles'; import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext'; import usePageScrollHandler from './usePageScrollHandler'; @@ -27,6 +27,7 @@ type PagerItem = { type AttachmentCarouselPagerProps = { items: PagerItem[]; + scrollEnabled?: boolean; renderItem: (props: {item: PagerItem; index: number; isActive: boolean}) => React.ReactNode; initialIndex: number; onTap: () => void; @@ -34,44 +35,42 @@ type AttachmentCarouselPagerProps = { onScaleChanged: (scale: number) => void; }; -function AttachmentCarouselPager({items, renderItem, initialIndex, onTap, onPageSelected, onScaleChanged}: AttachmentCarouselPagerProps, ref: ForwardedRef) { +function AttachmentCarouselPager( + {items, scrollEnabled = true, renderItem, initialIndex, onTap, onPageSelected, onScaleChanged}: AttachmentCarouselPagerProps, + ref: ForwardedRef, +) { const styles = useThemeStyles(); - const shouldPagerScroll = useSharedValue(true); const pagerRef = useRef(null); - const isSwipingInPager = useSharedValue(false); - const activeIndex = useSharedValue(initialIndex); + const isPagerSwiping = useSharedValue(false); + const activePage = useSharedValue(initialIndex); + const [activePageState, setActivePageState] = useState(initialIndex); const pageScrollHandler = usePageScrollHandler( { onPageScroll: (e) => { 'worklet'; - activeIndex.value = e.position; - isSwipingInPager.value = e.offset !== 0; + activePage.value = e.position; + isPagerSwiping.value = e.offset !== 0; }, }, [], ); - const [activePage, setActivePage] = useState(initialIndex); - useEffect(() => { - setActivePage(initialIndex); - activeIndex.value = initialIndex; - }, [activeIndex, initialIndex]); - - // we use reanimated for this since onPageSelected is called - // in the middle of the pager animation - useAnimatedReaction( - () => isSwipingInPager.value, - (stillScrolling) => { - if (stillScrolling) { - return; - } + setActivePageState(initialIndex); + activePage.value = initialIndex; + }, [activePage, initialIndex]); - runOnJS(setActivePage)(activeIndex.value); - }, + const contextValue = useMemo( + () => ({ + pagerRef, + isPagerSwiping, + onTap, + onScaleChanged, + }), + [isPagerSwiping, onTap, onScaleChanged], ); useImperativeHandle( @@ -84,28 +83,13 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onTap, onPage [], ); - const animatedProps = useAnimatedProps(() => ({ - scrollEnabled: shouldPagerScroll.value, - })); - - const contextValue = useMemo( - () => ({ - pagerRef, - shouldPagerScroll, - isSwipingInPager, - onTap, - onScaleChanged, - }), - [shouldPagerScroll, isSwipingInPager, onTap, onScaleChanged], - ); - return ( - {renderItem({item, index, isActive: index === activePage})} + {renderItem({item, index, isActive: index === activePageState})} ))} diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js index 062a5da05ce2..f3be3c97a8c6 100644 --- a/src/components/Attachments/AttachmentCarousel/index.native.js +++ b/src/components/Attachments/AttachmentCarousel/index.native.js @@ -147,6 +147,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, { diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index aba12c2eec59..3bbc201f255f 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -1,8 +1,8 @@ import React, {useCallback, useContext, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; -import type PagerView from 'react-native-pager-view'; -import Animated, {cancelAnimation, runOnUI, useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, useWorkletCallback, withSpring} from 'react-native-reanimated'; +import type {GestureRef} from 'react-native-gesture-handler/lib/typescript/handlers/gestures/gesture'; +import Animated, {cancelAnimation, runOnUI, useAnimatedStyle, useDerivedValue, useSharedValue, useWorkletCallback, withSpring} from 'react-native-reanimated'; import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -43,14 +43,12 @@ function MultiGestureCanvas({ contentSize = {width: 1, height: 1}, zoomRange: zoomRangeProp, isActive = true, - onScaleChanged: onScaleChangedProp, children, + onScaleChanged: onScaleChangedProp, }: MultiGestureCanvasProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const pagerRefFallback = useRef(null); - const shouldPagerScrollFallback = useSharedValue(false); const isSwipingInPagerFallback = useSharedValue(false); // If the MultiGestureCanvas used inside a AttachmentCarouselPager, we need to adapt the behaviour based on the pager state @@ -58,18 +56,17 @@ function MultiGestureCanvas({ const { onTap, onScaleChanged: onScaleChangedContext, - shouldPagerScroll, - isSwipingInPager, + isPagerSwiping, + pagerRef, } = useMemo( () => attachmentCarouselPagerContext ?? { onTap: () => {}, onScaleChanged: () => {}, - pagerRef: pagerRefFallback, - shouldPagerScroll: shouldPagerScrollFallback, - isSwipingInPager: isSwipingInPagerFallback, + pagerRef: undefined, + isPagerSwiping: isSwipingInPagerFallback, }, - [attachmentCarouselPagerContext, isSwipingInPagerFallback, shouldPagerScrollFallback], + [attachmentCarouselPagerContext, isSwipingInPagerFallback], ); /** @@ -126,7 +123,9 @@ function MultiGestureCanvas({ /** * Resets the canvas to the initial state and animates back smoothly */ - const reset = useWorkletCallback((animated: boolean) => { + const reset = useWorkletCallback((animated: boolean, callbackProp?: () => void) => { + const callback = callbackProp ?? (() => {}); + pinchScale.value = 1; stopAnimation(); @@ -140,7 +139,7 @@ function MultiGestureCanvas({ panTranslateY.value = withSpring(0, SPRING_CONFIG); pinchTranslateX.value = withSpring(0, SPRING_CONFIG); pinchTranslateY.value = withSpring(0, SPRING_CONFIG); - zoomScale.value = withSpring(1, SPRING_CONFIG); + zoomScale.value = withSpring(1, SPRING_CONFIG, callback); return; } @@ -151,6 +150,8 @@ function MultiGestureCanvas({ pinchTranslateX.value = 0; pinchTranslateY.value = 0; zoomScale.value = 1; + + callback(); }); const {singleTapGesture: basicSingleTapGesture, doubleTapGesture} = useTapGestures({ @@ -169,6 +170,11 @@ function MultiGestureCanvas({ }); const singleTapGesture = basicSingleTapGesture.requireExternalGestureToFail(doubleTapGesture, panGestureRef); + const panGestureSimultaneousList = useMemo( + () => (pagerRef === undefined ? [singleTapGesture, doubleTapGesture] : [pagerRef as unknown as Exclude, singleTapGesture, doubleTapGesture]), + [doubleTapGesture, pagerRef, singleTapGesture], + ); + const panGesture = usePanGesture({ canvasSize, contentSize, @@ -178,10 +184,10 @@ function MultiGestureCanvas({ offsetY, panTranslateX, panTranslateY, - isSwipingInPager, + isPagerSwiping, stopAnimation, }) - .simultaneousWithExternalGesture(singleTapGesture, doubleTapGesture) + .simultaneousWithExternalGesture(...panGestureSimultaneousList) .withRef(panGestureRef); const pinchGesture = usePinchGesture({ @@ -193,20 +199,11 @@ function MultiGestureCanvas({ pinchTranslateX, pinchTranslateY, pinchScale, - isSwipingInPager, + isPagerSwiping, stopAnimation, onScaleChanged, }).simultaneousWithExternalGesture(panGesture, singleTapGesture, doubleTapGesture); - // Enables/disables the pager scroll based on the zoom scale - // When the content is zoomed in/out, the pager should be disabled - useAnimatedReaction( - () => zoomScale.value, - () => { - shouldPagerScroll.value = zoomScale.value === 1; - }, - ); - // Trigger a reset when the canvas gets inactive, but only if it was already mounted before const mounted = useRef(false); useEffect(() => { diff --git a/src/components/MultiGestureCanvas/types.ts b/src/components/MultiGestureCanvas/types.ts index d0f8a775f2aa..6d4bc2d252ba 100644 --- a/src/components/MultiGestureCanvas/types.ts +++ b/src/components/MultiGestureCanvas/types.ts @@ -31,7 +31,7 @@ type MultiGestureCanvasVariables = { zoomRange: ZoomRange; minContentScale: number; maxContentScale: number; - isSwipingInPager: SharedValue; + isPagerSwiping: SharedValue; zoomScale: SharedValue; totalScale: SharedValue; pinchScale: SharedValue; @@ -42,7 +42,7 @@ type MultiGestureCanvasVariables = { pinchTranslateX: SharedValue; pinchTranslateY: SharedValue; stopAnimation: () => void; - reset: (animated: boolean) => void; + reset: (animated: boolean, callbackProp: () => void) => void; onTap: OnTapCallback | undefined; onScaleChanged: OnScaleChangedCallback | undefined; }; diff --git a/src/components/MultiGestureCanvas/usePanGesture.ts b/src/components/MultiGestureCanvas/usePanGesture.ts index 3ef791ad64b0..8a646446fad4 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.ts +++ b/src/components/MultiGestureCanvas/usePanGesture.ts @@ -13,10 +13,10 @@ const PAN_DECAY_DECELARATION = 0.9915; type UsePanGestureProps = Pick< MultiGestureCanvasVariables, - 'canvasSize' | 'contentSize' | 'zoomScale' | 'totalScale' | 'offsetX' | 'offsetY' | 'panTranslateX' | 'panTranslateY' | 'isSwipingInPager' | 'stopAnimation' + 'canvasSize' | 'contentSize' | 'zoomScale' | 'totalScale' | 'offsetX' | 'offsetY' | 'panTranslateX' | 'panTranslateY' | 'isPagerSwiping' | 'stopAnimation' >; -const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, offsetY, panTranslateX, panTranslateY, isSwipingInPager, stopAnimation}: UsePanGestureProps): PanGesture => { +const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, offsetY, panTranslateX, panTranslateY, isPagerSwiping, stopAnimation}: UsePanGestureProps): PanGesture => { // The content size after fitting it to the canvas and zooming const zoomedContentWidth = useDerivedValue(() => contentSize.width * totalScale.value, [contentSize.width]); const zoomedContentHeight = useDerivedValue(() => contentSize.height * totalScale.value, [contentSize.height]); @@ -117,7 +117,7 @@ const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, // eslint-disable-next-line @typescript-eslint/naming-convention .onTouchesMove((_evt, state) => { // We only allow panning when the content is zoomed in - if (zoomScale.value <= 1 || isSwipingInPager.value) { + if (zoomScale.value <= 1 || isPagerSwiping.value) { return; } @@ -147,7 +147,7 @@ const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, panTranslateY.value = 0; // If we are swiping (in the pager), we don't want to return to boundaries - if (isSwipingInPager.value) { + if (isPagerSwiping.value) { return; } diff --git a/src/components/MultiGestureCanvas/usePinchGesture.ts b/src/components/MultiGestureCanvas/usePinchGesture.ts index a2a1f58864d6..2ff375dc7edd 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.ts +++ b/src/components/MultiGestureCanvas/usePinchGesture.ts @@ -8,7 +8,7 @@ import type {MultiGestureCanvasVariables} from './types'; type UsePinchGestureProps = Pick< MultiGestureCanvasVariables, - 'canvasSize' | 'zoomScale' | 'zoomRange' | 'offsetX' | 'offsetY' | 'pinchTranslateX' | 'pinchTranslateY' | 'pinchScale' | 'isSwipingInPager' | 'stopAnimation' | 'onScaleChanged' + 'canvasSize' | 'zoomScale' | 'zoomRange' | 'offsetX' | 'offsetY' | 'pinchTranslateX' | 'pinchTranslateY' | 'pinchScale' | 'isPagerSwiping' | 'stopAnimation' | 'onScaleChanged' >; const usePinchGesture = ({ @@ -20,7 +20,7 @@ const usePinchGesture = ({ pinchTranslateX: totalPinchTranslateX, pinchTranslateY: totalPinchTranslateY, pinchScale, - isSwipingInPager, + isPagerSwiping, stopAnimation, onScaleChanged, }: UsePinchGestureProps): PinchGesture => { @@ -44,6 +44,16 @@ const usePinchGesture = ({ const pinchBounceTranslateX = useSharedValue(0); const pinchBounceTranslateY = useSharedValue(0); + const triggerScaleChangedEvent = () => { + 'worklet'; + + if (onScaleChanged === undefined) { + return; + } + + runOnJS(onScaleChanged)(zoomScale.value); + }; + // Update the total (pinch) translation based on the regular pinch + bounce useAnimatedReaction( () => [pinchTranslateX.value, pinchTranslateY.value, pinchBounceTranslateX.value, pinchBounceTranslateY.value], @@ -80,7 +90,7 @@ const usePinchGesture = ({ // eslint-disable-next-line @typescript-eslint/naming-convention .onTouchesDown((_evt, state) => { // We don't want to activate pinch gesture when we are swiping in the pager - if (!isSwipingInPager.value) { + if (!isPagerSwiping.value) { return; } @@ -109,9 +119,7 @@ const usePinchGesture = ({ zoomScale.value = newZoomScale; currentPinchScale.value = evt.scale; - if (onScaleChanged !== undefined) { - runOnJS(onScaleChanged)(zoomScale.value); - } + triggerScaleChangedEvent(); } // Calculate new pinch translation @@ -145,26 +153,18 @@ const usePinchGesture = ({ pinchBounceTranslateY.value = withSpring(0, SPRING_CONFIG); } - const triggerScaleChangeCallback = () => { - if (onScaleChanged === undefined) { - return; - } - - runOnJS(onScaleChanged)(zoomScale.value); - }; - if (zoomScale.value < zoomRange.min) { // If the zoom scale is less than the minimum zoom scale, we need to set the zoom scale to the minimum pinchScale.value = zoomRange.min; - zoomScale.value = withSpring(zoomRange.min, SPRING_CONFIG, triggerScaleChangeCallback); + zoomScale.value = withSpring(zoomRange.min, SPRING_CONFIG, triggerScaleChangedEvent); } else if (zoomScale.value > zoomRange.max) { // If the zoom scale is higher than the maximum zoom scale, we need to set the zoom scale to the maximum pinchScale.value = zoomRange.max; - zoomScale.value = withSpring(zoomRange.max, SPRING_CONFIG, triggerScaleChangeCallback); + zoomScale.value = withSpring(zoomRange.max, SPRING_CONFIG, triggerScaleChangedEvent); } else { // Otherwise, we just update the pinch scale offset pinchScale.value = zoomScale.value; - triggerScaleChangeCallback(); + triggerScaleChangedEvent(); } }); diff --git a/src/components/MultiGestureCanvas/useTapGestures.ts b/src/components/MultiGestureCanvas/useTapGestures.ts index 0a1102a4c9a3..137c2287e8cc 100644 --- a/src/components/MultiGestureCanvas/useTapGestures.ts +++ b/src/components/MultiGestureCanvas/useTapGestures.ts @@ -36,7 +36,7 @@ const useTapGestures = ({ const doubleTapScale = useMemo(() => Math.max(DOUBLE_TAP_SCALE, maxContentScale / minContentScale), [maxContentScale, minContentScale]); const zoomToCoordinates = useWorkletCallback( - (focalX: number, focalY: number) => { + (focalX: number, focalY: number, callbackProp: () => void) => { 'worklet'; stopAnimation(); @@ -100,9 +100,11 @@ const useTapGestures = ({ offsetAfterZooming.y = 0; } + const callback = callbackProp || (() => {}); + offsetX.value = withSpring(offsetAfterZooming.x, SPRING_CONFIG); offsetY.value = withSpring(offsetAfterZooming.y, SPRING_CONFIG); - zoomScale.value = withSpring(doubleTapScale, SPRING_CONFIG); + zoomScale.value = withSpring(doubleTapScale, SPRING_CONFIG, callback); pinchScale.value = doubleTapScale; }, [scaledContentWidth, scaledContentHeight, canvasSize, doubleTapScale], @@ -113,22 +115,26 @@ const useTapGestures = ({ .maxDelay(150) .maxDistance(20) .onEnd((evt) => { + const triggerScaleChangedEvent = () => { + 'worklet'; + + if (onScaleChanged != null) { + runOnJS(onScaleChanged)(zoomScale.value); + } + }; + // If the content is already zoomed, we want to reset the zoom, // otherwise we want to zoom in if (zoomScale.value > 1) { - reset(true); + reset(true, triggerScaleChangedEvent); } else { - zoomToCoordinates(evt.x, evt.y); - } - - if (onScaleChanged !== undefined) { - runOnJS(onScaleChanged)(zoomScale.value); + zoomToCoordinates(evt.x, evt.y, triggerScaleChangedEvent); } }); const singleTapGesture = Gesture.Tap() .numberOfTaps(1) - .maxDuration(50) + .maxDuration(125) .onBegin(() => { stopAnimation(); }) From 710a3fb137ea33d2f5c8fed55eadaa56d5c5aa07 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 16 Jan 2024 16:50:38 +0100 Subject: [PATCH 060/107] remove undefined --- src/components/MultiGestureCanvas/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MultiGestureCanvas/types.ts b/src/components/MultiGestureCanvas/types.ts index 6d4bc2d252ba..cd3c5eb1cae5 100644 --- a/src/components/MultiGestureCanvas/types.ts +++ b/src/components/MultiGestureCanvas/types.ts @@ -19,7 +19,7 @@ type ZoomRange = { }; /** Triggered whenever the scale of the MultiGestureCanvas changes */ -type OnScaleChangedCallback = ((zoomScale: number) => void) | undefined; +type OnScaleChangedCallback = (zoomScale: number) => void; /** Triggered when the canvas is tapped (single tap) */ type OnTapCallback = (() => void) | undefined; From 5d8799f203b80547e6bc00b6a895a12d30f34c6a Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 17 Jan 2024 12:28:23 +0100 Subject: [PATCH 061/107] fix: problems --- .../Pager/AttachmentCarouselPagerContext.ts | 5 ++-- .../AttachmentCarousel/Pager/index.tsx | 6 +++-- .../BaseAttachmentViewPdf.js | 6 ++--- .../AttachmentViewPdf/index.android.js | 8 +++---- src/components/Lightbox.tsx | 24 ++++++++++++------- src/components/MultiGestureCanvas/types.ts | 2 +- 6 files changed, 30 insertions(+), 21 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts index f23153f5e2e2..481c11ee0397 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -4,10 +4,11 @@ import type PagerView from 'react-native-pager-view'; import type {SharedValue} from 'react-native-reanimated'; type AttachmentCarouselPagerContextValue = { + pagerRef: ForwardedRef; + isPagerSwiping: SharedValue; + isPdfZooming: SharedValue; onTap: () => void; onScaleChanged: (scale: number) => void; - pagerRef: ForwardedRef; // - isPagerSwiping: SharedValue; }; const AttachmentCarouselPagerContext = createContext(null); diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index 0331fc4044ce..cf95382ccc30 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -42,6 +42,7 @@ function AttachmentCarouselPager( const styles = useThemeStyles(); const pagerRef = useRef(null); + const isPdfZooming = useSharedValue(false); const isPagerSwiping = useSharedValue(false); const activePage = useSharedValue(initialIndex); const [activePageState, setActivePageState] = useState(initialIndex); @@ -67,10 +68,11 @@ function AttachmentCarouselPager( () => ({ pagerRef, isPagerSwiping, + isPdfZooming, onTap, onScaleChanged, }), - [isPagerSwiping, onTap, onScaleChanged], + [isPagerSwiping, isPdfZooming, onTap, onScaleChanged], ); useImperativeHandle( @@ -88,7 +90,7 @@ function AttachmentCarouselPager( { - if (offsetX.value !== 0 && offsetY.value !== 0 && shouldPagerScroll) { + if (offsetX.value !== 0 && offsetY.value !== 0 && isPdfZooming) { // if the value of X is greater than Y and the pdf is not zoomed in, // enable the pager scroll so that the user // can swipe to the next attachment otherwise disable it. if (Math.abs(evt.allTouches[0].absoluteX - offsetX.value) > Math.abs(evt.allTouches[0].absoluteY - offsetY.value) && scaleRef.value === 1) { - shouldPagerScroll.value = true; + isPdfZooming.value = true; } else { - shouldPagerScroll.value = false; + isPdfZooming.value = false; } } offsetX.value = evt.allTouches[0].absoluteX; diff --git a/src/components/Lightbox.tsx b/src/components/Lightbox.tsx index 3c092b5b4840..d814e34933c0 100644 --- a/src/components/Lightbox.tsx +++ b/src/components/Lightbox.tsx @@ -4,7 +4,7 @@ import {ActivityIndicator, PixelRatio, StyleSheet, View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; import Image from './Image'; import MultiGestureCanvas, {DEFAULT_ZOOM_RANGE} from './MultiGestureCanvas'; -import type {ContentSize, OnScaleChangedCallback, ZoomRange} from './MultiGestureCanvas/types'; +import type {CanvasSize, ContentSize, OnScaleChangedCallback, ZoomRange} from './MultiGestureCanvas/types'; import {getCanvasFitScale} from './MultiGestureCanvas/utils'; // Increase/decrease this number to change the number of concurrent lightboxes @@ -63,8 +63,8 @@ function Lightbox({ }: LightboxProps) { const StyleUtils = useStyleUtils(); - const [canvasSize, setCanvasSize] = useState({width: 0, height: 0}); - const isCanvasLoaded = canvasSize.width !== 0 && canvasSize.height !== 0; + const [canvasSize, setCanvasSize] = useState(); + const isCanvasLoaded = canvasSize !== undefined; const updateCanvasSize = useCallback( ({ nativeEvent: { @@ -75,6 +75,7 @@ function Lightbox({ ); const [contentSize, setInternalContentSize] = useState(() => cachedImageDimensions.get(uri)); + const isContentLoaded = contentSize !== undefined; const setContentSize = useCallback( (newDimensions: ContentSize | undefined) => { setInternalContentSize(newDimensions); @@ -83,10 +84,15 @@ function Lightbox({ [uri], ); const updateContentSize = useCallback( - ({nativeEvent: {width, height}}: ImageOnLoadEvent) => setContentSize({width: width * PixelRatio.get(), height: height * PixelRatio.get()}), - [setContentSize], + ({nativeEvent: {width, height}}: ImageOnLoadEvent) => { + if (contentSize !== undefined) { + return; + } + + setContentSize({width: width * PixelRatio.get(), height: height * PixelRatio.get()}); + }, + [contentSize, setContentSize], ); - const contentLoaded = contentSize != null; const isItemActive = index === activeIndex; const [isActive, setActive] = useState(isItemActive); @@ -125,7 +131,7 @@ function Lightbox({ // because it's only going to be rendered after the fallback image is hidden. const shouldShowLightbox = !hasSiblingCarouselItems || !isFallbackVisible; - const isLoading = isActive && (!isCanvasLoaded || !contentLoaded); + const isLoading = isActive && (!isCanvasLoaded || !isContentLoaded); // We delay setting a page to active state by a (few) millisecond(s), // to prevent the image transformer from flashing while still rendering @@ -151,7 +157,7 @@ function Lightbox({ } if (isActive) { - if (contentLoaded && isFallbackVisible) { + if (isContentLoaded && isFallbackVisible) { // We delay hiding the fallback image while image transformer is still rendering setTimeout(() => { setFallbackVisible(false); @@ -166,7 +172,7 @@ function Lightbox({ // Show fallback when the image goes out of focus or when the image is loading setFallbackVisible(true); } - }, [contentLoaded, hasSiblingCarouselItems, isActive, isFallbackVisible, isLightboxImageLoaded, isLightboxVisible]); + }, [isContentLoaded, hasSiblingCarouselItems, isActive, isFallbackVisible, isLightboxImageLoaded, isLightboxVisible]); return ( void; reset: (animated: boolean, callbackProp: () => void) => void; onTap: OnTapCallback | undefined; - onScaleChanged: OnScaleChangedCallback | undefined; + onScaleChanged?: OnScaleChangedCallback; }; export type {CanvasSize, ContentSize, ZoomRange, OnScaleChangedCallback, MultiGestureCanvasVariables}; From c06219a96e83ec87adcfdbca0eec1fdb28eaaf94 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 17 Jan 2024 12:40:12 +0100 Subject: [PATCH 062/107] fix: arrows not showable --- .../AttachmentViewPdf/BaseAttachmentViewPdf.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js index 177927edaeac..6d2e8ec6242a 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js @@ -8,7 +8,7 @@ function BaseAttachmentViewPdf({ encryptedSourceUrl, isFocused, isUsedInCarousel, - onPress, + onPress: onPressProp, onScaleChanged: onScaleChangedProp, onToggleKeyboard, onLoadComplete, @@ -45,6 +45,18 @@ function BaseAttachmentViewPdf({ [attachmentCarouselPagerContext, isUsedInCarousel, onScaleChangedProp], ); + const onPress = useCallback( + (e) => { + if (onPressProp !== undefined) { + onPressProp(e); + } + if (attachmentCarouselPagerContext !== null && attachmentCarouselPagerContext.onTap !== null) { + attachmentCarouselPagerContext.onTap(e); + } + }, + [attachmentCarouselPagerContext], + ); + return ( Date: Wed, 17 Jan 2024 21:21:06 +0100 Subject: [PATCH 063/107] fix: callback deps --- .../AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js index 6d2e8ec6242a..0f6a4a54b1af 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js @@ -54,7 +54,7 @@ function BaseAttachmentViewPdf({ attachmentCarouselPagerContext.onTap(e); } }, - [attachmentCarouselPagerContext], + [attachmentCarouselPagerContext, onPressProp], ); return ( From 05ade3ceb3bed7ad1238d4342a30f2ef39064f9c Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 18 Jan 2024 10:56:04 +0100 Subject: [PATCH 064/107] fix: pdf; don't allow tap when zoomed in --- .../AttachmentViewPdf/BaseAttachmentViewPdf.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js index 0f6a4a54b1af..e9ad8f5e529b 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js @@ -1,4 +1,4 @@ -import React, {memo, useCallback, useContext, useEffect} from 'react'; +import React, {memo, useCallback, useContext, useEffect, useState} from 'react'; import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; import PDFView from '@components/PDFView'; import {attachmentViewPdfDefaultProps, attachmentViewPdfPropTypes} from './propTypes'; @@ -16,6 +16,7 @@ function BaseAttachmentViewPdf({ style, }) { const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); + const [scale, setScale] = useState(); useEffect(() => { if (!attachmentCarouselPagerContext) { @@ -26,12 +27,13 @@ function BaseAttachmentViewPdf({ }, []); const onScaleChanged = useCallback( - (scale) => { - onScaleChangedProp(scale); + (newScale) => { + onScaleChangedProp(newScale); + setScale(newScale); // When a pdf is shown in a carousel, we want to disable the pager scroll when the pdf is zoomed in if (isUsedInCarousel && attachmentCarouselPagerContext) { - const isPdfZooming = scale === 1; + const isPdfZooming = newScale === 1; attachmentCarouselPagerContext.onScaleChanged(1); @@ -50,11 +52,11 @@ function BaseAttachmentViewPdf({ if (onPressProp !== undefined) { onPressProp(e); } - if (attachmentCarouselPagerContext !== null && attachmentCarouselPagerContext.onTap !== null) { + if (attachmentCarouselPagerContext !== null && attachmentCarouselPagerContext.onTap !== null && scale === 1) { attachmentCarouselPagerContext.onTap(e); } }, - [attachmentCarouselPagerContext, onPressProp], + [attachmentCarouselPagerContext, onPressProp, scale], ); return ( From fcee0f2e5c7a880687a2e4c0853e0eb52d97adaa Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 18 Jan 2024 11:03:34 +0100 Subject: [PATCH 065/107] fix: callback prop --- src/components/MultiGestureCanvas/index.tsx | 11 ++++++----- src/components/MultiGestureCanvas/types.ts | 2 +- src/components/MultiGestureCanvas/useTapGestures.ts | 4 +--- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index 3bbc201f255f..55a683a1278c 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -123,11 +123,7 @@ function MultiGestureCanvas({ /** * Resets the canvas to the initial state and animates back smoothly */ - const reset = useWorkletCallback((animated: boolean, callbackProp?: () => void) => { - const callback = callbackProp ?? (() => {}); - - pinchScale.value = 1; - + const reset = useWorkletCallback((animated: boolean, callback?: () => void) => { stopAnimation(); pinchScale.value = 1; @@ -140,6 +136,7 @@ function MultiGestureCanvas({ pinchTranslateX.value = withSpring(0, SPRING_CONFIG); pinchTranslateY.value = withSpring(0, SPRING_CONFIG); zoomScale.value = withSpring(1, SPRING_CONFIG, callback); + return; } @@ -151,6 +148,10 @@ function MultiGestureCanvas({ pinchTranslateY.value = 0; zoomScale.value = 1; + if (callback === undefined) { + return; + } + callback(); }); diff --git a/src/components/MultiGestureCanvas/types.ts b/src/components/MultiGestureCanvas/types.ts index ec1975c883e8..c10b1ab677a8 100644 --- a/src/components/MultiGestureCanvas/types.ts +++ b/src/components/MultiGestureCanvas/types.ts @@ -42,7 +42,7 @@ type MultiGestureCanvasVariables = { pinchTranslateX: SharedValue; pinchTranslateY: SharedValue; stopAnimation: () => void; - reset: (animated: boolean, callbackProp: () => void) => void; + reset: (animated: boolean, callback: () => void) => void; onTap: OnTapCallback | undefined; onScaleChanged?: OnScaleChangedCallback; }; diff --git a/src/components/MultiGestureCanvas/useTapGestures.ts b/src/components/MultiGestureCanvas/useTapGestures.ts index 137c2287e8cc..6eba3849d572 100644 --- a/src/components/MultiGestureCanvas/useTapGestures.ts +++ b/src/components/MultiGestureCanvas/useTapGestures.ts @@ -36,7 +36,7 @@ const useTapGestures = ({ const doubleTapScale = useMemo(() => Math.max(DOUBLE_TAP_SCALE, maxContentScale / minContentScale), [maxContentScale, minContentScale]); const zoomToCoordinates = useWorkletCallback( - (focalX: number, focalY: number, callbackProp: () => void) => { + (focalX: number, focalY: number, callback: () => void) => { 'worklet'; stopAnimation(); @@ -100,8 +100,6 @@ const useTapGestures = ({ offsetAfterZooming.y = 0; } - const callback = callbackProp || (() => {}); - offsetX.value = withSpring(offsetAfterZooming.x, SPRING_CONFIG); offsetY.value = withSpring(offsetAfterZooming.y, SPRING_CONFIG); zoomScale.value = withSpring(doubleTapScale, SPRING_CONFIG, callback); From 395e25ce96c1d23d74503bef95acb6c2d91f153f Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 18 Jan 2024 11:13:19 +0100 Subject: [PATCH 066/107] move style --- src/components/MultiGestureCanvas/index.tsx | 8 +------- src/styles/utils/index.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index 55a683a1278c..625c58bdb7ec 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -239,13 +239,7 @@ function MultiGestureCanvas({ return ( ({ From 8164ff5a222a9a9e95f8a84a9e8513833d7f611c Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 18 Jan 2024 11:16:17 +0100 Subject: [PATCH 067/107] memoize style --- src/components/MultiGestureCanvas/index.tsx | 4 +++- src/styles/utils/index.ts | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index 625c58bdb7ec..a53166c292b3 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -236,10 +236,12 @@ function MultiGestureCanvas({ }; }); + const containerStyle = useMemo(() => StyleUtils.getMultiGestureCanvasContainerStyle(canvasSize.width), [StyleUtils, canvasSize.width]); + return ( ({ From 5cba6a865656370f09598f38e310d5292e8df8c6 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 18 Jan 2024 11:17:36 +0100 Subject: [PATCH 068/107] fix: wrong condition --- .../AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js index e9ad8f5e529b..4c85b7d0a456 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js @@ -52,7 +52,7 @@ function BaseAttachmentViewPdf({ if (onPressProp !== undefined) { onPressProp(e); } - if (attachmentCarouselPagerContext !== null && attachmentCarouselPagerContext.onTap !== null && scale === 1) { + if (attachmentCarouselPagerContext !== undefined && attachmentCarouselPagerContext.onTap !== undefined && scale === 1) { attachmentCarouselPagerContext.onTap(e); } }, From e90de14bd3c3e4d730ad0c5ffa68421fbdfffcfb Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 18 Jan 2024 11:36:08 +0100 Subject: [PATCH 069/107] fix: arrows not shown after pdf is unzoomed --- .../Pager/AttachmentCarouselPagerContext.ts | 2 +- .../AttachmentCarousel/Pager/index.tsx | 10 +++++----- .../AttachmentCarousel/index.native.js | 10 +++++++++- .../AttachmentViewPdf/BaseAttachmentViewPdf.js | 15 +++------------ .../AttachmentViewPdf/index.android.js | 2 +- 5 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts index 481c11ee0397..b73f30e7114c 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -6,7 +6,7 @@ import type {SharedValue} from 'react-native-reanimated'; type AttachmentCarouselPagerContextValue = { pagerRef: ForwardedRef; isPagerSwiping: SharedValue; - isPdfZooming: SharedValue; + scale: number; onTap: () => void; onScaleChanged: (scale: number) => void; }; diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index cf95382ccc30..cb6fa58d8f48 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -27,6 +27,7 @@ type PagerItem = { type AttachmentCarouselPagerProps = { items: PagerItem[]; + scale: number; scrollEnabled?: boolean; renderItem: (props: {item: PagerItem; index: number; isActive: boolean}) => React.ReactNode; initialIndex: number; @@ -36,13 +37,12 @@ type AttachmentCarouselPagerProps = { }; function AttachmentCarouselPager( - {items, scrollEnabled = true, renderItem, initialIndex, onTap, onPageSelected, onScaleChanged}: AttachmentCarouselPagerProps, + {items, scale, scrollEnabled = true, renderItem, initialIndex, onTap, onPageSelected, onScaleChanged}: AttachmentCarouselPagerProps, ref: ForwardedRef, ) { const styles = useThemeStyles(); const pagerRef = useRef(null); - const isPdfZooming = useSharedValue(false); const isPagerSwiping = useSharedValue(false); const activePage = useSharedValue(initialIndex); const [activePageState, setActivePageState] = useState(initialIndex); @@ -68,11 +68,11 @@ function AttachmentCarouselPager( () => ({ pagerRef, isPagerSwiping, - isPdfZooming, + scale, onTap, onScaleChanged, }), - [isPagerSwiping, isPdfZooming, onTap, onScaleChanged], + [isPagerSwiping, scale, onTap, onScaleChanged], ); useImperativeHandle( @@ -90,7 +90,7 @@ function AttachmentCarouselPager( { + if (newScale === scale) { + return; + } + + setScale(newScale); + const newIsZoomedOut = newScale === 1; if (isZoomedOut === newIsZoomedOut) { @@ -117,7 +124,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, setIsZoomedOut(newIsZoomedOut); setShouldShowArrows(newIsZoomedOut); }, - [isZoomedOut, setShouldShowArrows], + [isZoomedOut, scale, setShouldShowArrows], ); return ( @@ -147,6 +154,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, { if (!attachmentCarouselPagerContext) { @@ -29,19 +29,10 @@ function BaseAttachmentViewPdf({ const onScaleChanged = useCallback( (newScale) => { onScaleChangedProp(newScale); - setScale(newScale); // When a pdf is shown in a carousel, we want to disable the pager scroll when the pdf is zoomed in if (isUsedInCarousel && attachmentCarouselPagerContext) { - const isPdfZooming = newScale === 1; - - attachmentCarouselPagerContext.onScaleChanged(1); - - if (attachmentCarouselPagerContext.isPdfZooming.value === isPdfZooming) { - return; - } - - attachmentCarouselPagerContext.isPdfZooming.value = isPdfZooming; + attachmentCarouselPagerContext.onScaleChanged(newScale); } }, [attachmentCarouselPagerContext, isUsedInCarousel, onScaleChangedProp], diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js index 9c83bc7fc54d..53e5c1ff9ddd 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js @@ -22,7 +22,7 @@ function AttachmentViewPdf(props) { // frozen, which combined with Reanimated using strict mode since 3.6.0 was resulting in errors. // Without strict mode, it would just silently fail. // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze#description - const isPdfZooming = attachmentCarouselPagerContext !== null ? attachmentCarouselPagerContext.shouldPagerScroll : undefined; + const isPdfZooming = attachmentCarouselPagerContext !== null ? attachmentCarouselPagerContext.scale !== 1 : undefined; const Pan = Gesture.Pan() .manualActivation(true) From 773b4dcdc0992590d78231c75033b4886a22ae19 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 18 Jan 2024 14:13:00 +0100 Subject: [PATCH 070/107] improve style memo --- src/components/MultiGestureCanvas/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index a53166c292b3..d8452becf01b 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -236,12 +236,12 @@ function MultiGestureCanvas({ }; }); - const containerStyle = useMemo(() => StyleUtils.getMultiGestureCanvasContainerStyle(canvasSize.width), [StyleUtils, canvasSize.width]); + const containerStyles = useMemo(() => [styles.flex1, StyleUtils.getMultiGestureCanvasContainerStyle(canvasSize.width)], [StyleUtils, canvasSize.width, styles.flex1]); return ( Date: Fri, 19 Jan 2024 11:37:35 +0100 Subject: [PATCH 071/107] fix: arrows in pdf delayed --- .../Pager/AttachmentCarouselPagerContext.ts | 2 +- .../Attachments/AttachmentCarousel/Pager/index.tsx | 7 +++---- .../Attachments/AttachmentCarousel/index.native.js | 9 ++++----- .../AttachmentViewPdf/BaseAttachmentViewPdf.js | 6 +++--- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts index b73f30e7114c..b2aaea942073 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -6,7 +6,7 @@ import type {SharedValue} from 'react-native-reanimated'; type AttachmentCarouselPagerContextValue = { pagerRef: ForwardedRef; isPagerSwiping: SharedValue; - scale: number; + scrollEnabled: boolean; onTap: () => void; onScaleChanged: (scale: number) => void; }; diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index cb6fa58d8f48..f4b6d9e918da 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -27,7 +27,6 @@ type PagerItem = { type AttachmentCarouselPagerProps = { items: PagerItem[]; - scale: number; scrollEnabled?: boolean; renderItem: (props: {item: PagerItem; index: number; isActive: boolean}) => React.ReactNode; initialIndex: number; @@ -37,7 +36,7 @@ type AttachmentCarouselPagerProps = { }; function AttachmentCarouselPager( - {items, scale, scrollEnabled = true, renderItem, initialIndex, onTap, onPageSelected, onScaleChanged}: AttachmentCarouselPagerProps, + {items, scrollEnabled = true, renderItem, initialIndex, onTap, onPageSelected, onScaleChanged}: AttachmentCarouselPagerProps, ref: ForwardedRef, ) { const styles = useThemeStyles(); @@ -68,11 +67,11 @@ function AttachmentCarouselPager( () => ({ pagerRef, isPagerSwiping, - scale, + scrollEnabled, onTap, onScaleChanged, }), - [isPagerSwiping, scale, onTap, onScaleChanged], + [isPagerSwiping, scrollEnabled, onTap, onScaleChanged], ); useImperativeHandle( diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js index 1bf03457722e..ac402130beae 100644 --- a/src/components/Attachments/AttachmentCarousel/index.native.js +++ b/src/components/Attachments/AttachmentCarousel/index.native.js @@ -23,7 +23,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, const pagerRef = useRef(null); const [page, setPage] = useState(); const [attachments, setAttachments] = useState([]); - const [scale, setScale] = useState(1); + const scale = useRef(1); const [isZoomedOut, setIsZoomedOut] = useState(true); const [shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows] = useCarouselArrows(); const [activeSource, setActiveSource] = useState(source); @@ -109,11 +109,11 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, const handleScaleChange = useCallback( (newScale) => { - if (newScale === scale) { + if (newScale === scale.current) { return; } - setScale(newScale); + scale.current = newScale; const newIsZoomedOut = newScale === 1; @@ -124,7 +124,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, setIsZoomedOut(newIsZoomedOut); setShouldShowArrows(newIsZoomedOut); }, - [isZoomedOut, scale, setShouldShowArrows], + [isZoomedOut, setShouldShowArrows], ); return ( @@ -154,7 +154,6 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, { if (!attachmentCarouselPagerContext) { @@ -43,11 +43,11 @@ function BaseAttachmentViewPdf({ if (onPressProp !== undefined) { onPressProp(e); } - if (attachmentCarouselPagerContext !== undefined && attachmentCarouselPagerContext.onTap !== undefined && scale === 1) { + if (attachmentCarouselPagerContext !== undefined && attachmentCarouselPagerContext.onTap !== undefined && scrollEnabled) { attachmentCarouselPagerContext.onTap(e); } }, - [attachmentCarouselPagerContext, onPressProp, scale], + [attachmentCarouselPagerContext, scrollEnabled, onPressProp], ); return ( From 1049b845ed5f6aa8da24925b3be4842307265132 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Jan 2024 12:55:15 +0100 Subject: [PATCH 072/107] improve Lightbox on Android --- .../{Lightbox.tsx => Lightbox/index.tsx} | 33 +++++++++---------- .../numberOfConcurrentLightboxes/index.ios.ts | 8 +++++ .../numberOfConcurrentLightboxes/index.ts | 8 +++++ .../numberOfConcurrentLightboxes/types.ts | 5 +++ 4 files changed, 36 insertions(+), 18 deletions(-) rename src/components/{Lightbox.tsx => Lightbox/index.tsx} (89%) create mode 100644 src/components/Lightbox/numberOfConcurrentLightboxes/index.ios.ts create mode 100644 src/components/Lightbox/numberOfConcurrentLightboxes/index.ts create mode 100644 src/components/Lightbox/numberOfConcurrentLightboxes/types.ts diff --git a/src/components/Lightbox.tsx b/src/components/Lightbox/index.tsx similarity index 89% rename from src/components/Lightbox.tsx rename to src/components/Lightbox/index.tsx index d814e34933c0..8af6d301c2b7 100644 --- a/src/components/Lightbox.tsx +++ b/src/components/Lightbox/index.tsx @@ -1,16 +1,13 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react'; import type {LayoutChangeEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native'; import {ActivityIndicator, PixelRatio, StyleSheet, View} from 'react-native'; +import Image from '@components/Image'; +import MultiGestureCanvas, {DEFAULT_ZOOM_RANGE} from '@components/MultiGestureCanvas'; +import type {CanvasSize, ContentSize, OnScaleChangedCallback, ZoomRange} from '@components/MultiGestureCanvas/types'; +import {getCanvasFitScale} from '@components/MultiGestureCanvas/utils'; import useStyleUtils from '@hooks/useStyleUtils'; -import Image from './Image'; -import MultiGestureCanvas, {DEFAULT_ZOOM_RANGE} from './MultiGestureCanvas'; -import type {CanvasSize, ContentSize, OnScaleChangedCallback, ZoomRange} from './MultiGestureCanvas/types'; -import {getCanvasFitScale} from './MultiGestureCanvas/utils'; - -// Increase/decrease this number to change the number of concurrent lightboxes -// The more concurrent lighboxes, the worse performance gets (especially on low-end devices) -type LightboxConcurrencyLimit = number | 'UNLIMITED'; -const NUMBER_OF_CONCURRENT_LIGHTBOXES: LightboxConcurrencyLimit = 3; +import NUMBER_OF_CONCURRENT_LIGHTBOXES from './numberOfConcurrentLightboxes'; + const DEFAULT_IMAGE_SIZE = 200; const DEFAULT_IMAGE_DIMENSION: ContentSize = {width: DEFAULT_IMAGE_SIZE, height: DEFAULT_IMAGE_SIZE}; @@ -75,7 +72,6 @@ function Lightbox({ ); const [contentSize, setInternalContentSize] = useState(() => cachedImageDimensions.get(uri)); - const isContentLoaded = contentSize !== undefined; const setContentSize = useCallback( (newDimensions: ContentSize | undefined) => { setInternalContentSize(newDimensions); @@ -131,7 +127,8 @@ function Lightbox({ // because it's only going to be rendered after the fallback image is hidden. const shouldShowLightbox = !hasSiblingCarouselItems || !isFallbackVisible; - const isLoading = isActive && (!isCanvasLoaded || !isContentLoaded); + const isContentLoaded = isLightboxImageLoaded || isFallbackImageLoaded; + const isLoading = isActive && (!isCanvasLoaded || !isContentLoaded || isFallbackVisible); // We delay setting a page to active state by a (few) millisecond(s), // to prevent the image transformer from flashing while still rendering @@ -148,6 +145,7 @@ function Lightbox({ if (isLightboxVisible) { return; } + setLightboxImageLoaded(false); setContentSize(undefined); }, [isLightboxVisible, setContentSize]); @@ -157,12 +155,9 @@ function Lightbox({ } if (isActive) { - if (isContentLoaded && isFallbackVisible) { - // We delay hiding the fallback image while image transformer is still rendering - setTimeout(() => { - setFallbackVisible(false); - setFallbackImageLoaded(false); - }, 100); + if (isLightboxImageLoaded && isFallbackVisible) { + setFallbackVisible(false); + setFallbackImageLoaded(false); } } else { if (isLightboxVisible && isLightboxImageLoaded) { @@ -196,7 +191,9 @@ function Lightbox({ isAuthTokenRequired={isAuthTokenRequired} onError={onError} onLoad={updateContentSize} - onLoadEnd={() => setLightboxImageLoaded(true)} + onLoadEnd={() => { + setLightboxImageLoaded(true); + }} /> diff --git a/src/components/Lightbox/numberOfConcurrentLightboxes/index.ios.ts b/src/components/Lightbox/numberOfConcurrentLightboxes/index.ios.ts new file mode 100644 index 000000000000..1ce0d2cee405 --- /dev/null +++ b/src/components/Lightbox/numberOfConcurrentLightboxes/index.ios.ts @@ -0,0 +1,8 @@ +import type LightboxConcurrencyLimit from './types'; + +// On iOS we can allow multiple lightboxes to be rendered at the same time. +// This enables faster time to interaction when swiping between pages in the carousel. +// When the lightbox is pre-rendered, we don't need to wait for the gestures to initialize. +const NUMBER_OF_CONCURRENT_LIGHTBOXES: LightboxConcurrencyLimit = 3; + +export default NUMBER_OF_CONCURRENT_LIGHTBOXES; diff --git a/src/components/Lightbox/numberOfConcurrentLightboxes/index.ts b/src/components/Lightbox/numberOfConcurrentLightboxes/index.ts new file mode 100644 index 000000000000..f6f55a8913c7 --- /dev/null +++ b/src/components/Lightbox/numberOfConcurrentLightboxes/index.ts @@ -0,0 +1,8 @@ +import type LightboxConcurrencyLimit from './types'; + +// On web, this is not used. +// On Android, we don't want to allow rendering multiple lightboxes, +// because performance is typically slower than on iOS and this caused issues. +const NUMBER_OF_CONCURRENT_LIGHTBOXES: LightboxConcurrencyLimit = 1; + +export default NUMBER_OF_CONCURRENT_LIGHTBOXES; diff --git a/src/components/Lightbox/numberOfConcurrentLightboxes/types.ts b/src/components/Lightbox/numberOfConcurrentLightboxes/types.ts new file mode 100644 index 000000000000..57aaa53cca8c --- /dev/null +++ b/src/components/Lightbox/numberOfConcurrentLightboxes/types.ts @@ -0,0 +1,5 @@ +// Increase/decrease this number to change the number of concurrent lightboxes +// The more concurrent lighboxes, the worse performance gets (especially on low-end devices) +type LightboxConcurrencyLimit = number | 'UNLIMITED'; + +export default LightboxConcurrencyLimit; From 5221cffe76793274cb39a4776a0c8c3c32bb54bd Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Jan 2024 13:00:05 +0100 Subject: [PATCH 073/107] fix: lightbox flashing --- src/components/Lightbox/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/Lightbox/index.tsx b/src/components/Lightbox/index.tsx index 8af6d301c2b7..61942cf7e419 100644 --- a/src/components/Lightbox/index.tsx +++ b/src/components/Lightbox/index.tsx @@ -125,7 +125,7 @@ function Lightbox({ // so that we don't see two overlapping images at the same time. // If there the Lightbox is not used within a carousel, we don't need to hide the Lightbox, // because it's only going to be rendered after the fallback image is hidden. - const shouldShowLightbox = !hasSiblingCarouselItems || !isFallbackVisible; + const shouldShowLightbox = isLightboxImageLoaded && (!hasSiblingCarouselItems || !isFallbackVisible); const isContentLoaded = isLightboxImageLoaded || isFallbackImageLoaded; const isLoading = isActive && (!isCanvasLoaded || !isContentLoaded || isFallbackVisible); @@ -141,6 +141,7 @@ function Lightbox({ } }, [isItemActive]); + // Resets the lightbox when it becomes inactive useEffect(() => { if (isLightboxVisible) { return; @@ -149,6 +150,7 @@ function Lightbox({ setContentSize(undefined); }, [isLightboxVisible, setContentSize]); + // Enables and disables the fallback image when the carousel item is active or not useEffect(() => { if (!hasSiblingCarouselItems) { return; From 2dc798d2407ba3e5368e138a6a172c1053d6bcbf Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Jan 2024 13:25:35 +0100 Subject: [PATCH 074/107] simplify lightbox concurrency --- src/components/Lightbox/index.tsx | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/components/Lightbox/index.tsx b/src/components/Lightbox/index.tsx index 61942cf7e419..eeecc71dfab2 100644 --- a/src/components/Lightbox/index.tsx +++ b/src/components/Lightbox/index.tsx @@ -92,9 +92,19 @@ function Lightbox({ const isItemActive = index === activeIndex; const [isActive, setActive] = useState(isItemActive); - const isInactiveCarouselItem = hasSiblingCarouselItems && !isActive; - const [isFallbackVisible, setFallbackVisible] = useState(isInactiveCarouselItem); + const isLightboxVisible = useMemo(() => { + if (!hasSiblingCarouselItems || NUMBER_OF_CONCURRENT_LIGHTBOXES === 'UNLIMITED') { + return true; + } + + const indexCanvasOffset = Math.floor((NUMBER_OF_CONCURRENT_LIGHTBOXES - 1) / 2) || 0; + const indexOutOfRange = index > activeIndex + indexCanvasOffset || index < activeIndex - indexCanvasOffset; + return !indexOutOfRange; + }, [activeIndex, hasSiblingCarouselItems, index]); + const [isLightboxImageLoaded, setLightboxImageLoaded] = useState(false); + + const [isFallbackVisible, setFallbackVisible] = useState(!isLightboxVisible); const [isFallbackImageLoaded, setFallbackImageLoaded] = useState(false); const fallbackSize = useMemo(() => { if (!hasSiblingCarouselItems || !contentSize || !isCanvasLoaded) { @@ -109,18 +119,6 @@ function Lightbox({ }; }, [hasSiblingCarouselItems, contentSize, isCanvasLoaded, canvasSize]); - const isLightboxInRange = useMemo(() => { - if (NUMBER_OF_CONCURRENT_LIGHTBOXES === 'UNLIMITED') { - return true; - } - - const indexCanvasOffset = Math.floor((NUMBER_OF_CONCURRENT_LIGHTBOXES - 1) / 2) || 0; - const indexOutOfRange = index > activeIndex + indexCanvasOffset || index < activeIndex - indexCanvasOffset; - return !indexOutOfRange; - }, [activeIndex, index]); - const [isLightboxImageLoaded, setLightboxImageLoaded] = useState(false); - const isLightboxVisible = isLightboxInRange && (isActive || isLightboxImageLoaded || isFallbackImageLoaded); - // If the fallback image is currently visible, we want to hide the Lightbox until the fallback gets hidden, // so that we don't see two overlapping images at the same time. // If there the Lightbox is not used within a carousel, we don't need to hide the Lightbox, From 08a6c4131c33d9e18eecc5a40dbf0cc772f7385e Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Jan 2024 13:27:05 +0100 Subject: [PATCH 075/107] add comment --- src/components/Lightbox/index.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/Lightbox/index.tsx b/src/components/Lightbox/index.tsx index eeecc71dfab2..c7515040cb83 100644 --- a/src/components/Lightbox/index.tsx +++ b/src/components/Lightbox/index.tsx @@ -93,6 +93,10 @@ function Lightbox({ const isItemActive = index === activeIndex; const [isActive, setActive] = useState(isItemActive); + // Enables/disables the lightbox based on the number of concurrent lightboxes + // On higher-end devices, we can show render lightboxes at the same time, + // while on lower-end devices we want to only render the active carousel item as a lightbox + // to avoid performance issues. const isLightboxVisible = useMemo(() => { if (!hasSiblingCarouselItems || NUMBER_OF_CONCURRENT_LIGHTBOXES === 'UNLIMITED') { return true; From 0446a6de27f3f1ff38e03ca5c86f5841e204736e Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 22 Jan 2024 11:19:28 +0100 Subject: [PATCH 076/107] improve --- .../AttachmentViewPdf/BaseAttachmentViewPdf.js | 4 ++-- src/components/MultiGestureCanvas/constants.ts | 4 +++- src/components/MultiGestureCanvas/types.ts | 6 +++--- src/components/MultiGestureCanvas/useTapGestures.ts | 4 +--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js index cb3db626b423..1333f641aee9 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js @@ -16,7 +16,7 @@ function BaseAttachmentViewPdf({ style, }) { const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); - const scrollEnabled = attachmentCarouselPagerContext === undefined ? 1 : attachmentCarouselPagerContext.scrollEnabled; + const scrollEnabled = attachmentCarouselPagerContext === null ? 1 : attachmentCarouselPagerContext.scrollEnabled; useEffect(() => { if (!attachmentCarouselPagerContext) { @@ -43,7 +43,7 @@ function BaseAttachmentViewPdf({ if (onPressProp !== undefined) { onPressProp(e); } - if (attachmentCarouselPagerContext !== undefined && attachmentCarouselPagerContext.onTap !== undefined && scrollEnabled) { + if (attachmentCarouselPagerContext !== null && scrollEnabled) { attachmentCarouselPagerContext.onTap(e); } }, diff --git a/src/components/MultiGestureCanvas/constants.ts b/src/components/MultiGestureCanvas/constants.ts index 7dba3e568ea4..58ad6997bbeb 100644 --- a/src/components/MultiGestureCanvas/constants.ts +++ b/src/components/MultiGestureCanvas/constants.ts @@ -1,6 +1,8 @@ import type {WithSpringConfig} from 'react-native-reanimated'; import type {ZoomRange} from './types'; +const DOUBLE_TAP_SCALE = 3; + // The spring config is used to determine the physics of the spring animation // Details and a playground for testing different configs can be found at // https://docs.swmansion.com/react-native-reanimated/docs/animations/withSpring @@ -23,4 +25,4 @@ const ZOOM_RANGE_BOUNCE_FACTORS: Required = { max: 1.5, }; -export {SPRING_CONFIG, DEFAULT_ZOOM_RANGE, ZOOM_RANGE_BOUNCE_FACTORS}; +export {DOUBLE_TAP_SCALE, SPRING_CONFIG, DEFAULT_ZOOM_RANGE, ZOOM_RANGE_BOUNCE_FACTORS}; diff --git a/src/components/MultiGestureCanvas/types.ts b/src/components/MultiGestureCanvas/types.ts index c10b1ab677a8..bbd8f69e6947 100644 --- a/src/components/MultiGestureCanvas/types.ts +++ b/src/components/MultiGestureCanvas/types.ts @@ -22,7 +22,7 @@ type ZoomRange = { type OnScaleChangedCallback = (zoomScale: number) => void; /** Triggered when the canvas is tapped (single tap) */ -type OnTapCallback = (() => void) | undefined; +type OnTapCallback = () => void; /** Types used of variables used within the MultiGestureCanvas component and it's hooks */ type MultiGestureCanvasVariables = { @@ -43,8 +43,8 @@ type MultiGestureCanvasVariables = { pinchTranslateY: SharedValue; stopAnimation: () => void; reset: (animated: boolean, callback: () => void) => void; - onTap: OnTapCallback | undefined; - onScaleChanged?: OnScaleChangedCallback; + onTap: OnTapCallback; + onScaleChanged: OnScaleChangedCallback | undefined; }; export type {CanvasSize, ContentSize, ZoomRange, OnScaleChangedCallback, MultiGestureCanvasVariables}; diff --git a/src/components/MultiGestureCanvas/useTapGestures.ts b/src/components/MultiGestureCanvas/useTapGestures.ts index 6eba3849d572..ce67f11a91c8 100644 --- a/src/components/MultiGestureCanvas/useTapGestures.ts +++ b/src/components/MultiGestureCanvas/useTapGestures.ts @@ -3,12 +3,10 @@ import {useMemo} from 'react'; import type {TapGesture} from 'react-native-gesture-handler'; import {Gesture} from 'react-native-gesture-handler'; import {runOnJS, useWorkletCallback, withSpring} from 'react-native-reanimated'; -import {SPRING_CONFIG} from './constants'; +import {DOUBLE_TAP_SCALE, SPRING_CONFIG} from './constants'; import type {MultiGestureCanvasVariables} from './types'; import * as MultiGestureCanvasUtils from './utils'; -const DOUBLE_TAP_SCALE = 3; - type UseTapGesturesProps = Pick< MultiGestureCanvasVariables, 'canvasSize' | 'contentSize' | 'minContentScale' | 'maxContentScale' | 'offsetX' | 'offsetY' | 'pinchScale' | 'zoomScale' | 'reset' | 'stopAnimation' | 'onScaleChanged' | 'onTap' From acadc33aaef5bbe0a0603e4adcdf0d5c3a8dd072 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 22 Jan 2024 11:27:51 +0100 Subject: [PATCH 077/107] improve types --- .../AttachmentCarousel/Pager/usePageScrollHandler.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts b/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts index bcc616883d72..6f0ab4a51dc7 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts @@ -2,11 +2,17 @@ import type {PagerViewProps} from 'react-native-pager-view'; import {useEvent, useHandler} from 'react-native-reanimated'; type PageScrollHandler = NonNullable; -type OnPageScrollEventData = Parameters[0]['nativeEvent']; -type OnPageScrollREAHandler = Parameters>>[0][string]; + +type PageScrollEventData = Parameters[0]['nativeEvent']; +type PageScrollContext = Record; + +// 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>[0]; +type AnimatedPageScrollHandler = AnimatedHandlers[string]; type Handlers = { - onPageScroll?: OnPageScrollREAHandler; + onPageScroll?: AnimatedPageScrollHandler; }; type Deps = Parameters[1]; From 6d21d627cad1101e5b604c6604b3d8f432c16cae Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 23 Jan 2024 10:28:57 +0100 Subject: [PATCH 078/107] fix: pdf scroll --- .../Pager/AttachmentCarouselPagerContext.ts | 5 +++-- .../AttachmentCarousel/Pager/index.tsx | 22 ++++++++++++------- .../AttachmentCarousel/index.native.js | 2 +- .../BaseAttachmentViewPdf.js | 6 ++--- .../AttachmentViewPdf/index.android.js | 10 +++++---- src/components/MultiGestureCanvas/index.tsx | 4 ++-- 6 files changed, 29 insertions(+), 20 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts index b2aaea942073..94f503002b4a 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -5,8 +5,9 @@ import type {SharedValue} from 'react-native-reanimated'; type AttachmentCarouselPagerContextValue = { pagerRef: ForwardedRef; - isPagerSwiping: SharedValue; - scrollEnabled: boolean; + isPagerScrolling: SharedValue; + isScrollEnabled: boolean; + setScrollEnabled: (shouldPagerScroll: boolean) => void; onTap: () => void; onScaleChanged: (scale: number) => void; }; diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index f4b6d9e918da..dded43516947 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -27,7 +27,7 @@ type PagerItem = { type AttachmentCarouselPagerProps = { items: PagerItem[]; - scrollEnabled?: boolean; + isZoomedOut?: boolean; renderItem: (props: {item: PagerItem; index: number; isActive: boolean}) => React.ReactNode; initialIndex: number; onTap: () => void; @@ -36,13 +36,18 @@ type AttachmentCarouselPagerProps = { }; function AttachmentCarouselPager( - {items, scrollEnabled = true, renderItem, initialIndex, onTap, onPageSelected, onScaleChanged}: AttachmentCarouselPagerProps, + {items, isZoomedOut = true, renderItem, initialIndex, onTap, onPageSelected, onScaleChanged}: AttachmentCarouselPagerProps, ref: ForwardedRef, ) { const styles = useThemeStyles(); const pagerRef = useRef(null); - const isPagerSwiping = useSharedValue(false); + const isPagerScrolling = useSharedValue(false); + const [isScrollEnabled, setScrollEnabled] = useState(isZoomedOut); + useEffect(() => { + setScrollEnabled(isZoomedOut); + }, [isZoomedOut]); + const activePage = useSharedValue(initialIndex); const [activePageState, setActivePageState] = useState(initialIndex); @@ -52,7 +57,7 @@ function AttachmentCarouselPager( 'worklet'; activePage.value = e.position; - isPagerSwiping.value = e.offset !== 0; + isPagerScrolling.value = e.offset !== 0; }, }, [], @@ -66,12 +71,13 @@ function AttachmentCarouselPager( const contextValue = useMemo( () => ({ pagerRef, - isPagerSwiping, - scrollEnabled, + isPagerScrolling, + isScrollEnabled, + setScrollEnabled, onTap, onScaleChanged, }), - [isPagerSwiping, scrollEnabled, onTap, onScaleChanged], + [isPagerScrolling, isScrollEnabled, onTap, onScaleChanged], ); useImperativeHandle( @@ -87,9 +93,9 @@ function AttachmentCarouselPager( return ( { diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js index 1333f641aee9..99f726bc6032 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js @@ -16,7 +16,7 @@ function BaseAttachmentViewPdf({ style, }) { const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); - const scrollEnabled = attachmentCarouselPagerContext === null ? 1 : attachmentCarouselPagerContext.scrollEnabled; + const isScrollEnabled = attachmentCarouselPagerContext === null ? true : attachmentCarouselPagerContext.isScrollEnabled; useEffect(() => { if (!attachmentCarouselPagerContext) { @@ -43,11 +43,11 @@ function BaseAttachmentViewPdf({ if (onPressProp !== undefined) { onPressProp(e); } - if (attachmentCarouselPagerContext !== null && scrollEnabled) { + if (attachmentCarouselPagerContext !== null && isScrollEnabled) { attachmentCarouselPagerContext.onTap(e); } }, - [attachmentCarouselPagerContext, scrollEnabled, onPressProp], + [attachmentCarouselPagerContext, isScrollEnabled, onPressProp], ); return ( diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js index 53e5c1ff9ddd..629a3dd19ecf 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js @@ -22,19 +22,21 @@ function AttachmentViewPdf(props) { // frozen, which combined with Reanimated using strict mode since 3.6.0 was resulting in errors. // Without strict mode, it would just silently fail. // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze#description - const isPdfZooming = attachmentCarouselPagerContext !== null ? attachmentCarouselPagerContext.scale !== 1 : undefined; + const isScrollEnabled = attachmentCarouselPagerContext !== null ? attachmentCarouselPagerContext.isScrollEnabled : undefined; const Pan = Gesture.Pan() .manualActivation(true) .onTouchesMove((evt) => { - if (offsetX.value !== 0 && offsetY.value !== 0 && isPdfZooming) { + if (offsetX.value !== 0 && offsetY.value !== 0 && attachmentCarouselPagerContext !== null && isScrollEnabled) { + const {setScrollEnabled} = attachmentCarouselPagerContext; + // if the value of X is greater than Y and the pdf is not zoomed in, // enable the pager scroll so that the user // can swipe to the next attachment otherwise disable it. if (Math.abs(evt.allTouches[0].absoluteX - offsetX.value) > Math.abs(evt.allTouches[0].absoluteY - offsetY.value) && scaleRef.value === 1) { - isPdfZooming.value = true; + setScrollEnabled(true); } else { - isPdfZooming.value = false; + setScrollEnabled(false); } } offsetX.value = evt.allTouches[0].absoluteX; diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index d8452becf01b..a7eca6baa18f 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -56,7 +56,7 @@ function MultiGestureCanvas({ const { onTap, onScaleChanged: onScaleChangedContext, - isPagerSwiping, + isPagerScrolling: isPagerSwiping, pagerRef, } = useMemo( () => @@ -64,7 +64,7 @@ function MultiGestureCanvas({ onTap: () => {}, onScaleChanged: () => {}, pagerRef: undefined, - isPagerSwiping: isSwipingInPagerFallback, + isPagerScrolling: isSwipingInPagerFallback, }, [attachmentCarouselPagerContext, isSwipingInPagerFallback], ); From c8770bb4ea3b9e72acb91ece7c525acd87053a6c Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 23 Jan 2024 10:29:43 +0100 Subject: [PATCH 079/107] fix: worklet error --- .../AttachmentView/AttachmentViewPdf/index.android.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js index 629a3dd19ecf..19719c133f1c 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js @@ -1,7 +1,7 @@ import React, {memo, useCallback, useContext} from 'react'; import {StyleSheet, View} from 'react-native'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; -import Animated, {useSharedValue} from 'react-native-reanimated'; +import Animated, {runOnJS, useSharedValue} from 'react-native-reanimated'; import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; import useThemeStyles from '@hooks/useThemeStyles'; import BaseAttachmentViewPdf from './BaseAttachmentViewPdf'; @@ -34,9 +34,9 @@ function AttachmentViewPdf(props) { // enable the pager scroll so that the user // can swipe to the next attachment otherwise disable it. if (Math.abs(evt.allTouches[0].absoluteX - offsetX.value) > Math.abs(evt.allTouches[0].absoluteY - offsetY.value) && scaleRef.value === 1) { - setScrollEnabled(true); + runOnJS(setScrollEnabled)(true); } else { - setScrollEnabled(false); + runOnJS(setScrollEnabled)(false); } } offsetX.value = evt.allTouches[0].absoluteX; From f3b7de586f11d03ab09585ae1e5f083f8107f210 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 23 Jan 2024 12:47:08 +0100 Subject: [PATCH 080/107] improve pdf --- .../Pager/AttachmentCarouselPagerContext.ts | 3 +- .../AttachmentCarousel/Pager/index.tsx | 16 ++-- .../AttachmentViewImage/index.js | 2 - .../BaseAttachmentViewPdf.js | 5 +- .../AttachmentViewPdf/index.android.js | 91 ++++++++++++------- .../AttachmentView/AttachmentViewPdf/index.js | 3 +- .../Attachments/AttachmentView/index.js | 3 - 7 files changed, 73 insertions(+), 50 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts index 94f503002b4a..270e0b04909c 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -6,8 +6,7 @@ import type {SharedValue} from 'react-native-reanimated'; type AttachmentCarouselPagerContextValue = { pagerRef: ForwardedRef; isPagerScrolling: SharedValue; - isScrollEnabled: boolean; - setScrollEnabled: (shouldPagerScroll: boolean) => void; + isScrollEnabled: SharedValue; onTap: () => void; onScaleChanged: (scale: number) => void; }; diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index dded43516947..2f79f5b0abb2 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -5,7 +5,7 @@ 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, {useSharedValue} from 'react-native-reanimated'; +import Animated, {useAnimatedProps, useSharedValue} from 'react-native-reanimated'; import useThemeStyles from '@hooks/useThemeStyles'; import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext'; import usePageScrollHandler from './usePageScrollHandler'; @@ -43,10 +43,10 @@ function AttachmentCarouselPager( const pagerRef = useRef(null); const isPagerScrolling = useSharedValue(false); - const [isScrollEnabled, setScrollEnabled] = useState(isZoomedOut); + const isScrollEnabled = useSharedValue(isZoomedOut); useEffect(() => { - setScrollEnabled(isZoomedOut); - }, [isZoomedOut]); + isScrollEnabled.value = isZoomedOut; + }, [isScrollEnabled, isZoomedOut]); const activePage = useSharedValue(initialIndex); const [activePageState, setActivePageState] = useState(initialIndex); @@ -58,6 +58,7 @@ function AttachmentCarouselPager( activePage.value = e.position; isPagerScrolling.value = e.offset !== 0; + isScrollEnabled.value = true; }, }, [], @@ -73,13 +74,16 @@ function AttachmentCarouselPager( pagerRef, isPagerScrolling, isScrollEnabled, - setScrollEnabled, onTap, onScaleChanged, }), [isPagerScrolling, isScrollEnabled, onTap, onScaleChanged], ); + const animatedProps = useAnimatedProps(() => ({ + scrollEnabled: isScrollEnabled.value, + })); + useImperativeHandle( ref, () => ({ @@ -93,7 +97,6 @@ function AttachmentCarouselPager( return ( {items.map((item, index) => ( { if (!attachmentCarouselPagerContext) { @@ -43,7 +43,8 @@ function BaseAttachmentViewPdf({ if (onPressProp !== undefined) { onPressProp(e); } - if (attachmentCarouselPagerContext !== null && isScrollEnabled) { + + if (attachmentCarouselPagerContext !== null && isScrollEnabled.value) { attachmentCarouselPagerContext.onTap(e); } }, diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js index 19719c133f1c..b85242f5f571 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js @@ -1,19 +1,25 @@ -import React, {memo, useCallback, useContext} from 'react'; +import React, {memo, useContext, useMemo} from 'react'; import {StyleSheet, View} from 'react-native'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; -import Animated, {runOnJS, useSharedValue} from 'react-native-reanimated'; +import Animated, {useSharedValue} from 'react-native-reanimated'; import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; import useThemeStyles from '@hooks/useThemeStyles'; import BaseAttachmentViewPdf from './BaseAttachmentViewPdf'; import {attachmentViewPdfDefaultProps, attachmentViewPdfPropTypes} from './propTypes'; +// If the user pans less than this threshold, we'll not enable/disable the pager scroll, since the thouch will most probably be a tap. +// If the user moves their finger more than this threshold in the X direction, we'll enable the pager scroll. Otherwise if in the Y direction, we'll disable it. +const SCROLL_THRESHOLD = 10; + +function roundToDecimal(value, decimalPlaces = 0) { + const valueWithExponent = Math.round(`${value}e${decimalPlaces}`); + return Number(`${valueWithExponent}e${-decimalPlaces}`); +} + function AttachmentViewPdf(props) { const styles = useThemeStyles(); - const {onScaleChanged, ...restProps} = props; const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); - const scaleRef = useSharedValue(1); - const offsetX = useSharedValue(0); - const offsetY = useSharedValue(0); + const scale = useSharedValue(1); // Reanimated freezes all objects captured in the closure of a worklet. // Since Reanimated 3, entire objects are captured instead of just the relevant properties. @@ -22,32 +28,54 @@ function AttachmentViewPdf(props) { // frozen, which combined with Reanimated using strict mode since 3.6.0 was resulting in errors. // Without strict mode, it would just silently fail. // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze#description - const isScrollEnabled = attachmentCarouselPagerContext !== null ? attachmentCarouselPagerContext.isScrollEnabled : undefined; + const isScrollEnabled = attachmentCarouselPagerContext === null ? undefined : attachmentCarouselPagerContext.isScrollEnabled; + + const offsetX = useSharedValue(0); + const offsetY = useSharedValue(0); + const isPanRunning = useSharedValue(false); const Pan = Gesture.Pan() .manualActivation(true) .onTouchesMove((evt) => { - if (offsetX.value !== 0 && offsetY.value !== 0 && attachmentCarouselPagerContext !== null && isScrollEnabled) { - const {setScrollEnabled} = attachmentCarouselPagerContext; + if (offsetX.value !== 0 && offsetY.value !== 0 && isScrollEnabled) { + const translateX = Math.abs(evt.allTouches[0].absoluteX - offsetX.value); + const translateY = Math.abs(evt.allTouches[0].absoluteY - offsetY.value); + + const allowEnablingScroll = !isPanRunning.value || isScrollEnabled.value; // if the value of X is greater than Y and the pdf is not zoomed in, // enable the pager scroll so that the user // can swipe to the next attachment otherwise disable it. - if (Math.abs(evt.allTouches[0].absoluteX - offsetX.value) > Math.abs(evt.allTouches[0].absoluteY - offsetY.value) && scaleRef.value === 1) { - runOnJS(setScrollEnabled)(true); - } else { - runOnJS(setScrollEnabled)(false); + if (translateX > translateY && translateX > SCROLL_THRESHOLD && scale.value === 1 && allowEnablingScroll) { + isScrollEnabled.value = true; + } else if (translateY > SCROLL_THRESHOLD) { + isScrollEnabled.value = false; } } + + isPanRunning.value = true; offsetX.value = evt.allTouches[0].absoluteX; offsetY.value = evt.allTouches[0].absoluteY; + }) + .onTouchesUp(() => { + isPanRunning.value = false; + isScrollEnabled.value = true; }); - const updateScale = useCallback( - (scale) => { - scaleRef.value = scale; - }, - [scaleRef], + const Content = useMemo( + () => ( + { + // The react-native-pdf library's onScaleChanged event will sometimes give us scale values of e.g. 0.99... instead of 1, + // even though we're not pinching/zooming + // Rounding the scale value to 2 decimal place fixes this issue, since pinching will still be possible but very small pinches are ignored. + scale.value = roundToDecimal(newScale, 2); + }} + /> + ), + [props, scale], ); return ( @@ -55,21 +83,18 @@ function AttachmentViewPdf(props) { collapsable={false} style={styles.flex1} > - - - { - updateScale(scale); - onScaleChanged(); - }} - /> - - + {attachmentCarouselPagerContext === null ? ( + Content + ) : ( + + + {Content} + + + )} ); } diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js index c3d1423b17c9..d6a402613c34 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js @@ -2,7 +2,7 @@ import React, {memo} from 'react'; import PDFView from '@components/PDFView'; import {attachmentViewPdfDefaultProps, attachmentViewPdfPropTypes} from './propTypes'; -function AttachmentViewPdf({file, encryptedSourceUrl, isFocused, onPress, onScaleChanged, onToggleKeyboard, onLoadComplete, errorLabelStyles, style}) { +function AttachmentViewPdf({file, encryptedSourceUrl, isFocused, onPress, onToggleKeyboard, onLoadComplete, errorLabelStyles, style}) { return ( diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js index b0060afdb813..3b90385f9e9b 100755 --- a/src/components/Attachments/AttachmentView/index.js +++ b/src/components/Attachments/AttachmentView/index.js @@ -67,7 +67,6 @@ function AttachmentView({ shouldShowLoadingSpinnerIcon, shouldShowDownloadIcon, containerStyles, - onScaleChanged, onToggleKeyboard, translate, isFocused, @@ -141,7 +140,6 @@ function AttachmentView({ carouselItemIndex={carouselItemIndex} carouselActiveItemIndex={carouselActiveItemIndex} onPress={onPress} - onScaleChanged={onScaleChanged} onToggleKeyboard={onToggleKeyboard} onLoadComplete={() => !loadComplete && setLoadComplete(true)} errorLabelStyles={isUsedInAttachmentModal ? [styles.textLabel, styles.textLarge] : [styles.cursorAuto]} @@ -174,7 +172,6 @@ function AttachmentView({ carouselActiveItemIndex={carouselActiveItemIndex} isImage={isImage} onPress={onPress} - onScaleChanged={onScaleChanged} onError={() => { setImageError(true); }} From cc5bb3328fc69b6f0d77f89f4c9748b27bb72d61 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 23 Jan 2024 13:08:34 +0100 Subject: [PATCH 081/107] remove unused prop --- .../AttachmentView/AttachmentViewPdf/index.android.js | 2 +- src/components/ImageView/index.native.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js index b85242f5f571..0ab8c2b07f56 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js @@ -68,7 +68,7 @@ function AttachmentViewPdf(props) { // eslint-disable-next-line react/jsx-props-no-spreading {...props} onScaleChanged={(newScale) => { - // The react-native-pdf library's onScaleChanged event will sometimes give us scale values of e.g. 0.99... instead of 1, + // The react-native-pdf's onScaleChanged event will sometimes give us scale values of e.g. 0.99... instead of 1, // even though we're not pinching/zooming // Rounding the scale value to 2 decimal place fixes this issue, since pinching will still be possible but very small pinches are ignored. scale.value = roundToDecimal(newScale, 2); diff --git a/src/components/ImageView/index.native.js b/src/components/ImageView/index.native.js index ba10162ec1e2..8af91f6eb604 100644 --- a/src/components/ImageView/index.native.js +++ b/src/components/ImageView/index.native.js @@ -28,7 +28,7 @@ const defaultProps = { style: {}, }; -function ImageView({isAuthTokenRequired, url, onScaleChanged, style, zoomRange, onError, isUsedInCarousel, isSingleCarouselItem, carouselItemIndex, carouselActiveItemIndex}) { +function ImageView({isAuthTokenRequired, url, style, zoomRange, onError, isUsedInCarousel, isSingleCarouselItem, carouselItemIndex, carouselActiveItemIndex}) { const hasSiblingCarouselItems = isUsedInCarousel && !isSingleCarouselItem; return ( @@ -36,7 +36,6 @@ function ImageView({isAuthTokenRequired, url, onScaleChanged, style, zoomRange, uri={url} zoomRange={zoomRange} isAuthTokenRequired={isAuthTokenRequired} - onScaleChanged={onScaleChanged} onError={onError} index={carouselItemIndex} activeIndex={carouselActiveItemIndex} From 4b8f1831fed3352a74ec604e3f2ae619433706d9 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 23 Jan 2024 13:21:29 +0100 Subject: [PATCH 082/107] fix: console errors --- .../Attachments/AttachmentView/AttachmentViewImage/index.js | 4 ++-- .../AttachmentView/AttachmentViewImage/propTypes.js | 2 ++ src/components/Attachments/AttachmentView/index.js | 6 +++++- src/components/Attachments/AttachmentView/propTypes.js | 3 --- src/components/ImageView/propTypes.js | 4 ---- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js index 1a6b8b360097..14c60458b044 100755 --- a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js @@ -13,7 +13,7 @@ const propTypes = { }; function AttachmentViewImage({ - source, + url, file, isAuthTokenRequired, isUsedInCarousel, @@ -31,7 +31,7 @@ function AttachmentViewImage({ const children = ( Date: Tue, 23 Jan 2024 17:52:36 +0100 Subject: [PATCH 083/107] remove redundant isScrollEnabled change --- src/components/Attachments/AttachmentCarousel/Pager/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index 2f79f5b0abb2..59612897ca97 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -58,7 +58,6 @@ function AttachmentCarouselPager( activePage.value = e.position; isPagerScrolling.value = e.offset !== 0; - isScrollEnabled.value = true; }, }, [], From 5f17f2bcf461cb91b9348ce0447a12030f60cc4c Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 23 Jan 2024 17:52:45 +0100 Subject: [PATCH 084/107] remove round function --- .../AttachmentView/AttachmentViewPdf/index.android.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js index 0ab8c2b07f56..ff4e8ec1e961 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js @@ -11,11 +11,6 @@ import {attachmentViewPdfDefaultProps, attachmentViewPdfPropTypes} from './propT // If the user moves their finger more than this threshold in the X direction, we'll enable the pager scroll. Otherwise if in the Y direction, we'll disable it. const SCROLL_THRESHOLD = 10; -function roundToDecimal(value, decimalPlaces = 0) { - const valueWithExponent = Math.round(`${value}e${decimalPlaces}`); - return Number(`${valueWithExponent}e${-decimalPlaces}`); -} - function AttachmentViewPdf(props) { const styles = useThemeStyles(); const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); @@ -71,7 +66,7 @@ function AttachmentViewPdf(props) { // The react-native-pdf's onScaleChanged event will sometimes give us scale values of e.g. 0.99... instead of 1, // even though we're not pinching/zooming // Rounding the scale value to 2 decimal place fixes this issue, since pinching will still be possible but very small pinches are ignored. - scale.value = roundToDecimal(newScale, 2); + scale.value = Math.round(newScale * 1e2) / 1e2; }} /> ), From 80bf0ad236532a1757cc12e920fc3b7930efe83b Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 23 Jan 2024 20:50:56 +0100 Subject: [PATCH 085/107] move logic to AttachmenCarouselPager --- .../AttachmentCarousel/Pager/index.tsx | 44 +++++++++++++++---- .../AttachmentCarousel/index.native.js | 32 +------------- 2 files changed, 37 insertions(+), 39 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index 59612897ca97..0f6010737bfe 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -1,5 +1,5 @@ import type {ForwardedRef} from 'react'; -import React, {useEffect, useImperativeHandle, useMemo, useRef, useState} 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'; @@ -30,23 +30,21 @@ type AttachmentCarouselPagerProps = { isZoomedOut?: boolean; renderItem: (props: {item: PagerItem; index: number; isActive: boolean}) => React.ReactNode; initialIndex: number; - onTap: () => void; onPageSelected: () => void; - onScaleChanged: (scale: number) => void; + shouldShowArrows: boolean; + setShouldShowArrows: (shouldShowArrows: boolean) => void; }; function AttachmentCarouselPager( - {items, isZoomedOut = true, renderItem, initialIndex, onTap, onPageSelected, onScaleChanged}: AttachmentCarouselPagerProps, + {items, isZoomedOut = true, renderItem, initialIndex, onPageSelected, shouldShowArrows, setShouldShowArrows}: AttachmentCarouselPagerProps, ref: ForwardedRef, ) { const styles = useThemeStyles(); const pagerRef = useRef(null); + const scale = useSharedValue(1); const isPagerScrolling = useSharedValue(false); const isScrollEnabled = useSharedValue(isZoomedOut); - useEffect(() => { - isScrollEnabled.value = isZoomedOut; - }, [isScrollEnabled, isZoomedOut]); const activePage = useSharedValue(initialIndex); const [activePageState, setActivePageState] = useState(initialIndex); @@ -68,15 +66,43 @@ function AttachmentCarouselPager( activePage.value = initialIndex; }, [activePage, initialIndex]); + const handleScaleChange = useCallback( + (newScale: number) => { + if (newScale === scale.value) { + return; + } + + scale.value = newScale; + + const newIsZoomedOut = newScale === 1; + + if (isZoomedOut === newIsZoomedOut) { + return; + } + + isScrollEnabled.value = newIsZoomedOut; + setShouldShowArrows(newIsZoomedOut); + }, + [isScrollEnabled, isZoomedOut, scale, setShouldShowArrows], + ); + + const onTap = useCallback(() => { + if (!isScrollEnabled.value) { + return; + } + + setShouldShowArrows(!shouldShowArrows); + }, [isScrollEnabled.value, setShouldShowArrows, shouldShowArrows]); + const contextValue = useMemo( () => ({ pagerRef, isPagerScrolling, isScrollEnabled, onTap, - onScaleChanged, + onScaleChanged: handleScaleChange, }), - [isPagerScrolling, isScrollEnabled, onTap, onScaleChanged], + [isPagerScrolling, isScrollEnabled, onTap, handleScaleChange], ); const animatedProps = useAnimatedProps(() => ({ diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js index bf4f12e28583..5c72633f12e4 100644 --- a/src/components/Attachments/AttachmentCarousel/index.native.js +++ b/src/components/Attachments/AttachmentCarousel/index.native.js @@ -23,8 +23,6 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, const pagerRef = useRef(null); const [page, setPage] = useState(); const [attachments, setAttachments] = useState([]); - const scale = useRef(1); - const [isZoomedOut, setIsZoomedOut] = useState(true); const [shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows] = useCarouselArrows(); const [activeSource, setActiveSource] = useState(source); @@ -107,26 +105,6 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, [activeSource, attachments.length, page], ); - const handleScaleChange = useCallback( - (newScale) => { - if (newScale === scale.current) { - return; - } - - scale.current = newScale; - - const newIsZoomedOut = newScale === 1; - - if (isZoomedOut === newIsZoomedOut) { - return; - } - - setIsZoomedOut(newIsZoomedOut); - setShouldShowArrows(newIsZoomedOut); - }, - [isZoomedOut, setShouldShowArrows], - ); - return ( {page == null ? ( @@ -154,17 +132,11 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, { - if (!isZoomedOut) { - return; - } - setShouldShowArrows(!shouldShowArrows); - }} + shouldShowArrows={shouldShowArrows} + setShouldShowArrows={setShouldShowArrows} onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)} - onScaleChanged={handleScaleChange} ref={pagerRef} /> From a83a16e39fdac9d768fdc26c92fe42eddb6ba136 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 23 Jan 2024 20:51:45 +0100 Subject: [PATCH 086/107] remove unusedProp --- .../BaseAttachmentViewPdf.js | 22 +++++-------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js index f320eb82950d..eb772e2542f4 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js @@ -3,18 +3,7 @@ import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCa import PDFView from '@components/PDFView'; import {attachmentViewPdfDefaultProps, attachmentViewPdfPropTypes} from './propTypes'; -function BaseAttachmentViewPdf({ - file, - encryptedSourceUrl, - isFocused, - isUsedInCarousel, - onPress: onPressProp, - onScaleChanged: onScaleChangedProp, - onToggleKeyboard, - onLoadComplete, - errorLabelStyles, - style, -}) { +function BaseAttachmentViewPdf({file, encryptedSourceUrl, isFocused, isUsedInCarousel, onPress: onPressProp, onToggleKeyboard, onLoadComplete, errorLabelStyles, style}) { const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); const isScrollEnabled = attachmentCarouselPagerContext === null ? undefined : attachmentCarouselPagerContext.isScrollEnabled; @@ -28,14 +17,13 @@ function BaseAttachmentViewPdf({ const onScaleChanged = useCallback( (newScale) => { - onScaleChangedProp(newScale); - // When a pdf is shown in a carousel, we want to disable the pager scroll when the pdf is zoomed in - if (isUsedInCarousel && attachmentCarouselPagerContext) { - attachmentCarouselPagerContext.onScaleChanged(newScale); + if (!isUsedInCarousel || !attachmentCarouselPagerContext) { + return; } + attachmentCarouselPagerContext.onScaleChanged(newScale); }, - [attachmentCarouselPagerContext, isUsedInCarousel, onScaleChangedProp], + [attachmentCarouselPagerContext, isUsedInCarousel], ); const onPress = useCallback( From aff821cf36503ac5f96ebc705efb2212340dfe07 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 23 Jan 2024 20:53:12 +0100 Subject: [PATCH 087/107] fix: onScaleChanged prop --- .../BaseAttachmentViewPdf.js | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js index eb772e2542f4..b689cbe27c5e 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js @@ -3,7 +3,18 @@ import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCa import PDFView from '@components/PDFView'; import {attachmentViewPdfDefaultProps, attachmentViewPdfPropTypes} from './propTypes'; -function BaseAttachmentViewPdf({file, encryptedSourceUrl, isFocused, isUsedInCarousel, onPress: onPressProp, onToggleKeyboard, onLoadComplete, errorLabelStyles, style}) { +function BaseAttachmentViewPdf({ + file, + encryptedSourceUrl, + isFocused, + isUsedInCarousel, + onPress: onPressProp, + onScaleChanged: onScaleChangedProp, + onToggleKeyboard, + onLoadComplete, + errorLabelStyles, + style, +}) { const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); const isScrollEnabled = attachmentCarouselPagerContext === null ? undefined : attachmentCarouselPagerContext.isScrollEnabled; @@ -17,13 +28,16 @@ function BaseAttachmentViewPdf({file, encryptedSourceUrl, isFocused, isUsedInCar const onScaleChanged = useCallback( (newScale) => { + if (onScaleChangedProp !== undefined) { + onScaleChangedProp(newScale); + } + // When a pdf is shown in a carousel, we want to disable the pager scroll when the pdf is zoomed in - if (!isUsedInCarousel || !attachmentCarouselPagerContext) { - return; + if (isUsedInCarousel && attachmentCarouselPagerContext) { + attachmentCarouselPagerContext.onScaleChanged(newScale); } - attachmentCarouselPagerContext.onScaleChanged(newScale); }, - [attachmentCarouselPagerContext, isUsedInCarousel], + [attachmentCarouselPagerContext, isUsedInCarousel, onScaleChangedProp], ); const onPress = useCallback( From 1ea4d707cad92056278a39b1d1f80c0226945d7f Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 23 Jan 2024 20:55:48 +0100 Subject: [PATCH 088/107] fix: prop types --- .../AttachmentViewPdf/BaseAttachmentViewPdf.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js index b689cbe27c5e..34d2cb89aa42 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js @@ -1,8 +1,22 @@ +import PropTypes from 'prop-types'; import React, {memo, useCallback, useContext, useEffect} from 'react'; import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; import PDFView from '@components/PDFView'; import {attachmentViewPdfDefaultProps, attachmentViewPdfPropTypes} from './propTypes'; +const baseAttachmentViewPdfPropTypes = { + ...attachmentViewPdfPropTypes, + + /** Triggered when the PDF's onScaleChanged event is triggered */ + onScaleChanged: PropTypes.func, +}; + +const baseAttachmentViewPdfDefaultProps = { + ...attachmentViewPdfDefaultProps, + + onScaleChanged: undefined, +}; + function BaseAttachmentViewPdf({ file, encryptedSourceUrl, @@ -68,8 +82,8 @@ function BaseAttachmentViewPdf({ ); } -BaseAttachmentViewPdf.propTypes = attachmentViewPdfPropTypes; -BaseAttachmentViewPdf.defaultProps = attachmentViewPdfDefaultProps; +BaseAttachmentViewPdf.propTypes = baseAttachmentViewPdfPropTypes; +BaseAttachmentViewPdf.defaultProps = baseAttachmentViewPdfDefaultProps; BaseAttachmentViewPdf.displayName = 'BaseAttachmentViewPdf'; export default memo(BaseAttachmentViewPdf); From 8c6cb24717b00e534e2c56e050b70ffcf52e70a5 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 23 Jan 2024 22:50:22 +0100 Subject: [PATCH 089/107] fix: pager --- .../AttachmentCarousel/Pager/index.tsx | 31 ++++++++----------- .../AttachmentCarousel/index.native.js | 19 ++++++++++-- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index 0f6010737bfe..9f2f33ea60fe 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -27,24 +27,19 @@ type PagerItem = { type AttachmentCarouselPagerProps = { items: PagerItem[]; - isZoomedOut?: boolean; renderItem: (props: {item: PagerItem; index: number; isActive: boolean}) => React.ReactNode; initialIndex: number; onPageSelected: () => void; - shouldShowArrows: boolean; - setShouldShowArrows: (shouldShowArrows: boolean) => void; + onRequestToggleArrows: (showArrows?: boolean) => void; }; -function AttachmentCarouselPager( - {items, isZoomedOut = true, renderItem, initialIndex, onPageSelected, shouldShowArrows, setShouldShowArrows}: AttachmentCarouselPagerProps, - ref: ForwardedRef, -) { +function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelected, onRequestToggleArrows}: AttachmentCarouselPagerProps, ref: ForwardedRef) { const styles = useThemeStyles(); const pagerRef = useRef(null); - const scale = useSharedValue(1); + const scale = useRef(1); const isPagerScrolling = useSharedValue(false); - const isScrollEnabled = useSharedValue(isZoomedOut); + const isScrollEnabled = useSharedValue(true); const activePage = useSharedValue(initialIndex); const [activePageState, setActivePageState] = useState(initialIndex); @@ -68,22 +63,22 @@ function AttachmentCarouselPager( const handleScaleChange = useCallback( (newScale: number) => { - if (newScale === scale.value) { + if (newScale === scale.current) { return; } - scale.value = newScale; + scale.current = newScale; - const newIsZoomedOut = newScale === 1; + const newIsScrollEnabled = newScale === 1; - if (isZoomedOut === newIsZoomedOut) { + if (isScrollEnabled.value === newIsScrollEnabled) { return; } - isScrollEnabled.value = newIsZoomedOut; - setShouldShowArrows(newIsZoomedOut); + isScrollEnabled.value = newIsScrollEnabled; + onRequestToggleArrows(newIsScrollEnabled); }, - [isScrollEnabled, isZoomedOut, scale, setShouldShowArrows], + [isScrollEnabled, onRequestToggleArrows], ); const onTap = useCallback(() => { @@ -91,8 +86,8 @@ function AttachmentCarouselPager( return; } - setShouldShowArrows(!shouldShowArrows); - }, [isScrollEnabled.value, setShouldShowArrows, shouldShowArrows]); + onRequestToggleArrows(); + }, [isScrollEnabled.value, onRequestToggleArrows]); const contextValue = useMemo( () => ({ diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js index 5c72633f12e4..1cf1811df21c 100644 --- a/src/components/Attachments/AttachmentCarousel/index.native.js +++ b/src/components/Attachments/AttachmentCarousel/index.native.js @@ -87,6 +87,22 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, [autoHideArrows, page, updatePage], ); + /** + * Toggles the arrows visibility + * @param {Boolean} showArrows if showArrows is passed, it will set the visibility to the passed value + */ + const toggleArrows = useCallback( + (showArrows) => { + if (showArrows) { + setShouldShowArrows(showArrows); + return; + } + + setShouldShowArrows(!shouldShowArrows); + }, + [setShouldShowArrows, shouldShowArrows], + ); + /** * Defines how a single attachment should be rendered * @param {{ reportActionID: String, isAuthTokenRequired: Boolean, source: String, file: { name: String }, hasBeenFlagged: Boolean }} item @@ -134,8 +150,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, items={attachments} renderItem={renderItem} initialIndex={page} - shouldShowArrows={shouldShowArrows} - setShouldShowArrows={setShouldShowArrows} + onRequestToggleArrows={toggleArrows} onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)} ref={pagerRef} /> From d8d4a299d96858bb14968de006aca6b6fba516a1 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 23 Jan 2024 23:14:34 +0100 Subject: [PATCH 090/107] fix: wrong condition --- src/components/Attachments/AttachmentCarousel/index.native.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js index 1cf1811df21c..964aa4f4cd15 100644 --- a/src/components/Attachments/AttachmentCarousel/index.native.js +++ b/src/components/Attachments/AttachmentCarousel/index.native.js @@ -93,7 +93,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, */ const toggleArrows = useCallback( (showArrows) => { - if (showArrows) { + if (showArrows !== undefined) { setShouldShowArrows(showArrows); return; } From bb91b85750433631fda6a2a5bc34630835d6bbd6 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 23 Jan 2024 23:17:23 +0100 Subject: [PATCH 091/107] improve readability --- .../Attachments/AttachmentCarousel/index.native.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js index 964aa4f4cd15..134da0eaf669 100644 --- a/src/components/Attachments/AttachmentCarousel/index.native.js +++ b/src/components/Attachments/AttachmentCarousel/index.native.js @@ -93,12 +93,12 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, */ const toggleArrows = useCallback( (showArrows) => { - if (showArrows !== undefined) { - setShouldShowArrows(showArrows); + if (showArrows === undefined) { + setShouldShowArrows(!shouldShowArrows); return; } - setShouldShowArrows(!shouldShowArrows); + setShouldShowArrows(showArrows); }, [setShouldShowArrows, shouldShowArrows], ); From f54a4b9f2ea9dff179e7d32c1a939363f1edb06e Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 24 Jan 2024 09:01:46 +0100 Subject: [PATCH 092/107] fix: state setter --- src/components/Attachments/AttachmentCarousel/index.native.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js index 134da0eaf669..fa24ccd0ef53 100644 --- a/src/components/Attachments/AttachmentCarousel/index.native.js +++ b/src/components/Attachments/AttachmentCarousel/index.native.js @@ -94,13 +94,13 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, const toggleArrows = useCallback( (showArrows) => { if (showArrows === undefined) { - setShouldShowArrows(!shouldShowArrows); + setShouldShowArrows((prevShouldShowArrows) => !prevShouldShowArrows); return; } setShouldShowArrows(showArrows); }, - [setShouldShowArrows, shouldShowArrows], + [setShouldShowArrows], ); /** From 2a2f00788f8906c27b73c8e1eeb3f7551fdefef6 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 24 Jan 2024 21:26:34 +0100 Subject: [PATCH 093/107] fix: loading issues --- src/components/Lightbox/index.tsx | 34 +++++++++++++++---------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/components/Lightbox/index.tsx b/src/components/Lightbox/index.tsx index c7515040cb83..85776a18c2ee 100644 --- a/src/components/Lightbox/index.tsx +++ b/src/components/Lightbox/index.tsx @@ -61,7 +61,7 @@ function Lightbox({ const StyleUtils = useStyleUtils(); const [canvasSize, setCanvasSize] = useState(); - const isCanvasLoaded = canvasSize !== undefined; + const isCanvasLoading = canvasSize === undefined; const updateCanvasSize = useCallback( ({ nativeEvent: { @@ -111,7 +111,7 @@ function Lightbox({ const [isFallbackVisible, setFallbackVisible] = useState(!isLightboxVisible); const [isFallbackImageLoaded, setFallbackImageLoaded] = useState(false); const fallbackSize = useMemo(() => { - if (!hasSiblingCarouselItems || !contentSize || !isCanvasLoaded) { + if (!hasSiblingCarouselItems || !contentSize || isCanvasLoading) { return DEFAULT_IMAGE_DIMENSION; } @@ -121,7 +121,7 @@ function Lightbox({ width: PixelRatio.roundToNearestPixel(contentSize.width * minScale), height: PixelRatio.roundToNearestPixel(contentSize.height * minScale), }; - }, [hasSiblingCarouselItems, contentSize, isCanvasLoaded, canvasSize]); + }, [hasSiblingCarouselItems, contentSize, isCanvasLoading, canvasSize]); // If the fallback image is currently visible, we want to hide the Lightbox until the fallback gets hidden, // so that we don't see two overlapping images at the same time. @@ -129,8 +129,9 @@ function Lightbox({ // because it's only going to be rendered after the fallback image is hidden. const shouldShowLightbox = isLightboxImageLoaded && (!hasSiblingCarouselItems || !isFallbackVisible); - const isContentLoaded = isLightboxImageLoaded || isFallbackImageLoaded; - const isLoading = isActive && (!isCanvasLoaded || !isContentLoaded || isFallbackVisible); + const isFallbackStillLoading = isFallbackVisible && !isFallbackImageLoaded; + const isLightboxStillLoading = isLightboxVisible && !isLightboxImageLoaded; + const isLoading = isActive && (isCanvasLoading || isFallbackStillLoading || isLightboxStillLoading); // We delay setting a page to active state by a (few) millisecond(s), // to prevent the image transformer from flashing while still rendering @@ -154,31 +155,30 @@ function Lightbox({ // Enables and disables the fallback image when the carousel item is active or not useEffect(() => { + // When there are no other carousel items, we don't need to show the fallback image if (!hasSiblingCarouselItems) { return; } - if (isActive) { - if (isLightboxImageLoaded && isFallbackVisible) { - setFallbackVisible(false); - setFallbackImageLoaded(false); - } - } else { - if (isLightboxVisible && isLightboxImageLoaded) { - return; - } + // When the carousel item is active and the lightbox has finished loading, we want to hide the fallback image + if (isActive && isFallbackVisible && isLightboxVisible && isLightboxImageLoaded) { + setFallbackVisible(false); + setFallbackImageLoaded(false); + return; + } - // Show fallback when the image goes out of focus or when the image is loading + // If the carousel item has become inactive and the lightbox is not continued to be rendered, we want to show the fallback image + if (!isActive && !isLightboxVisible) { setFallbackVisible(true); } - }, [isContentLoaded, hasSiblingCarouselItems, isActive, isFallbackVisible, isLightboxImageLoaded, isLightboxVisible]); + }, [hasSiblingCarouselItems, isActive, isFallbackVisible, isLightboxImageLoaded, isLightboxVisible]); return ( - {isCanvasLoaded && ( + {!isCanvasLoading && ( <> {isLightboxVisible && ( From 33dea648f4dd6c68ca8dff612fbae96289030473 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 24 Jan 2024 21:26:48 +0100 Subject: [PATCH 094/107] remove unused code --- src/components/Lightbox/index.tsx | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/components/Lightbox/index.tsx b/src/components/Lightbox/index.tsx index 85776a18c2ee..8cc9f8afd4e2 100644 --- a/src/components/Lightbox/index.tsx +++ b/src/components/Lightbox/index.tsx @@ -90,9 +90,6 @@ function Lightbox({ [contentSize, setContentSize], ); - const isItemActive = index === activeIndex; - const [isActive, setActive] = useState(isItemActive); - // Enables/disables the lightbox based on the number of concurrent lightboxes // On higher-end devices, we can show render lightboxes at the same time, // while on lower-end devices we want to only render the active carousel item as a lightbox @@ -129,21 +126,11 @@ function Lightbox({ // because it's only going to be rendered after the fallback image is hidden. const shouldShowLightbox = isLightboxImageLoaded && (!hasSiblingCarouselItems || !isFallbackVisible); + const isActive = index === activeIndex; const isFallbackStillLoading = isFallbackVisible && !isFallbackImageLoaded; const isLightboxStillLoading = isLightboxVisible && !isLightboxImageLoaded; const isLoading = isActive && (isCanvasLoading || isFallbackStillLoading || isLightboxStillLoading); - // We delay setting a page to active state by a (few) millisecond(s), - // to prevent the image transformer from flashing while still rendering - // Instead, we show the fallback image while the image transformer is loading the image - useEffect(() => { - if (isItemActive) { - setTimeout(() => setActive(true), 1); - } else { - setActive(false); - } - }, [isItemActive]); - // Resets the lightbox when it becomes inactive useEffect(() => { if (isLightboxVisible) { From a058e3b3cc4047e543f7df49c947e927dd47c918 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 25 Jan 2024 00:19:11 +0100 Subject: [PATCH 095/107] simplify condition --- src/components/Lightbox/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Lightbox/index.tsx b/src/components/Lightbox/index.tsx index 8cc9f8afd4e2..07710d11c367 100644 --- a/src/components/Lightbox/index.tsx +++ b/src/components/Lightbox/index.tsx @@ -120,11 +120,11 @@ function Lightbox({ }; }, [hasSiblingCarouselItems, contentSize, isCanvasLoading, canvasSize]); - // If the fallback image is currently visible, we want to hide the Lightbox until the fallback gets hidden, - // so that we don't see two overlapping images at the same time. + // If the fallback image is currently visible, we want to hide the Lightbox by setting the opacity to 0, + // until the fallback gets hidden so that we don't see two overlapping images at the same time. // If there the Lightbox is not used within a carousel, we don't need to hide the Lightbox, // because it's only going to be rendered after the fallback image is hidden. - const shouldShowLightbox = isLightboxImageLoaded && (!hasSiblingCarouselItems || !isFallbackVisible); + const shouldShowLightbox = isLightboxImageLoaded && !isFallbackVisible; const isActive = index === activeIndex; const isFallbackStillLoading = isFallbackVisible && !isFallbackImageLoaded; From 57d1f7eea89b288d035a7d20be2ebd537df59cb1 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 26 Jan 2024 13:06:55 +0100 Subject: [PATCH 096/107] add comment --- src/components/Attachments/AttachmentCarousel/Pager/index.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index 9f2f33ea60fe..3bda3e303395 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -104,6 +104,10 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte 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( ref, () => ({ From 1594b72b99f09892c74929bc3635a977431bb706 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 26 Jan 2024 13:07:50 +0100 Subject: [PATCH 097/107] remove empty line? --- src/components/Attachments/AttachmentCarousel/Pager/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index 3bda3e303395..271452e1c855 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -70,7 +70,6 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte scale.current = newScale; const newIsScrollEnabled = newScale === 1; - if (isScrollEnabled.value === newIsScrollEnabled) { return; } From d6da8e915a3b18a9b53fa84a975f840e642c6b6a Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 26 Jan 2024 13:09:12 +0100 Subject: [PATCH 098/107] add comment --- .../AttachmentCarousel/Pager/usePageScrollHandler.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts b/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts index 6f0ab4a51dc7..b5d06676abde 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts @@ -16,6 +16,14 @@ type Handlers = { }; type Deps = Parameters[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 handlers + * @param dependencies + * @returns + */ const usePageScrollHandler = (handlers: Handlers, dependencies: Deps): PageScrollHandler => { const {context, doDependenciesDiffer} = useHandler(handlers, dependencies); const subscribeForEvents = ['onPageScroll']; From f2e4057171307044a3257361311ac50d45f08598 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 26 Jan 2024 13:10:50 +0100 Subject: [PATCH 099/107] add comment --- .../Pager/usePageScrollHandler.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts b/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts index b5d06676abde..fe255c6ead78 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts @@ -11,28 +11,24 @@ type PageScrollContext = Record; type AnimatedHandlers = Parameters>[0]; type AnimatedPageScrollHandler = AnimatedHandlers[string]; -type Handlers = { - onPageScroll?: AnimatedPageScrollHandler; -}; type Deps = Parameters[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 handlers - * @param dependencies - * @returns + * @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 = (handlers: Handlers, dependencies: Deps): PageScrollHandler => { - const {context, doDependenciesDiffer} = useHandler(handlers, dependencies); +const usePageScrollHandler = (onPageScroll: AnimatedPageScrollHandler, dependencies: Deps): PageScrollHandler => { + const {context, doDependenciesDiffer} = useHandler({onPageScroll}, dependencies); const subscribeForEvents = ['onPageScroll']; return useEvent( (event) => { 'worklet'; - const {onPageScroll} = handlers; if (onPageScroll && event.eventName.endsWith('onPageScroll')) { onPageScroll(event, context); } From 38175123575aee9b413004ccf3f7d38468340a96 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 26 Jan 2024 13:11:24 +0100 Subject: [PATCH 100/107] simplify usePageScrollHandler --- .../AttachmentCarousel/Pager/index.tsx | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index 271452e1c855..59f6cb0ba5fc 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -44,17 +44,12 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte const activePage = useSharedValue(initialIndex); const [activePageState, setActivePageState] = useState(initialIndex); - const pageScrollHandler = usePageScrollHandler( - { - onPageScroll: (e) => { - 'worklet'; + const pageScrollHandler = usePageScrollHandler((e) => { + 'worklet'; - activePage.value = e.position; - isPagerScrolling.value = e.offset !== 0; - }, - }, - [], - ); + activePage.value = e.position; + isPagerScrolling.value = e.offset !== 0; + }, []); useEffect(() => { setActivePageState(initialIndex); From 6aae70b33bad018425d3f1eb07d4ad79bd91ee7d Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 26 Jan 2024 13:11:44 +0100 Subject: [PATCH 101/107] rename type --- .../AttachmentCarousel/Pager/usePageScrollHandler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts b/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts index fe255c6ead78..ab7f0d99b7f0 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts @@ -11,7 +11,7 @@ type PageScrollContext = Record; type AnimatedHandlers = Parameters>[0]; type AnimatedPageScrollHandler = AnimatedHandlers[string]; -type Deps = Parameters[1]; +type Dependencies = Parameters[1]; /** * This hook is used to create a wrapped handler for the onPageScroll event from react-native-pager-view. @@ -21,7 +21,7 @@ type Deps = Parameters[1]; * @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: Deps): PageScrollHandler => { +const usePageScrollHandler = (onPageScroll: AnimatedPageScrollHandler, dependencies: Dependencies): PageScrollHandler => { const {context, doDependenciesDiffer} = useHandler({onPageScroll}, dependencies); const subscribeForEvents = ['onPageScroll']; From 62c1d4ff8ce178185ea024c6c683961f04c9591e Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 26 Jan 2024 13:13:11 +0100 Subject: [PATCH 102/107] add cp,,emt --- src/components/Attachments/AttachmentCarousel/Pager/index.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index 59f6cb0ba5fc..bef9f11bb77f 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -75,6 +75,10 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte [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 onTap = useCallback(() => { if (!isScrollEnabled.value) { return; From 2d5eb62c9e675e886641ac439dbb41c5476ea305 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 26 Jan 2024 13:14:29 +0100 Subject: [PATCH 103/107] improve --- .../Attachments/AttachmentCarousel/Pager/index.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index bef9f11bb77f..490afb6614ac 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -56,6 +56,11 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte 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) { @@ -79,7 +84,7 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte * 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 onTap = useCallback(() => { + const handleTap = useCallback(() => { if (!isScrollEnabled.value) { return; } @@ -92,10 +97,10 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte pagerRef, isPagerScrolling, isScrollEnabled, - onTap, + onTap: handleTap, onScaleChanged: handleScaleChange, }), - [isPagerScrolling, isScrollEnabled, onTap, handleScaleChange], + [isPagerScrolling, isScrollEnabled, handleTap, handleScaleChange], ); const animatedProps = useAnimatedProps(() => ({ From 4eaad5a32330d534397d477ed4dc99cd4fbb46d9 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 26 Jan 2024 13:15:59 +0100 Subject: [PATCH 104/107] remove empty line --- .../AttachmentView/AttachmentViewPdf/index.android.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js index ff4e8ec1e961..4679e00573a1 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js @@ -35,7 +35,6 @@ function AttachmentViewPdf(props) { if (offsetX.value !== 0 && offsetY.value !== 0 && isScrollEnabled) { const translateX = Math.abs(evt.allTouches[0].absoluteX - offsetX.value); const translateY = Math.abs(evt.allTouches[0].absoluteY - offsetY.value); - const allowEnablingScroll = !isPanRunning.value || isScrollEnabled.value; // if the value of X is greater than Y and the pdf is not zoomed in, From 9a5a83309893ec7d0b72766aaede22dcd6445398 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 26 Jan 2024 13:16:33 +0100 Subject: [PATCH 105/107] rename variable --- .../AttachmentView/AttachmentViewPdf/index.android.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js index 4679e00573a1..07cd8ecf61e7 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js @@ -27,7 +27,7 @@ function AttachmentViewPdf(props) { const offsetX = useSharedValue(0); const offsetY = useSharedValue(0); - const isPanRunning = useSharedValue(false); + const isPanGestureActive = useSharedValue(false); const Pan = Gesture.Pan() .manualActivation(true) @@ -35,7 +35,7 @@ function AttachmentViewPdf(props) { if (offsetX.value !== 0 && offsetY.value !== 0 && isScrollEnabled) { const translateX = Math.abs(evt.allTouches[0].absoluteX - offsetX.value); const translateY = Math.abs(evt.allTouches[0].absoluteY - offsetY.value); - const allowEnablingScroll = !isPanRunning.value || isScrollEnabled.value; + const allowEnablingScroll = !isPanGestureActive.value || isScrollEnabled.value; // if the value of X is greater than Y and the pdf is not zoomed in, // enable the pager scroll so that the user @@ -47,12 +47,12 @@ function AttachmentViewPdf(props) { } } - isPanRunning.value = true; + isPanGestureActive.value = true; offsetX.value = evt.allTouches[0].absoluteX; offsetY.value = evt.allTouches[0].absoluteY; }) .onTouchesUp(() => { - isPanRunning.value = false; + isPanGestureActive.value = false; isScrollEnabled.value = true; }); From 55d36a636e5dd4affdd72f6cce26648d0453483e Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 26 Jan 2024 13:18:22 +0100 Subject: [PATCH 106/107] add comment --- .../AttachmentViewPdf/BaseAttachmentViewPdf.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js index 34d2cb89aa42..4cf7de40bf12 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js @@ -40,6 +40,11 @@ function BaseAttachmentViewPdf({ // eslint-disable-next-line react-hooks/exhaustive-deps -- we just want to call this function when component is mounted }, []); + /** + * When the PDF's onScaleChanged event is triggered, we must call the context's onScaleChanged callback, + * because we want to disable the pager scroll when the pdf is zoomed in, + * as well as call the onScaleChanged prop of the AttachmentViewPdf component if defined. + */ const onScaleChanged = useCallback( (newScale) => { if (onScaleChangedProp !== undefined) { From 9a530615d5b1993ffed9cc92a0e07704165a8d3c Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 26 Jan 2024 13:20:24 +0100 Subject: [PATCH 107/107] add comment --- .../AttachmentViewPdf/BaseAttachmentViewPdf.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js index 4cf7de40bf12..2f16b63aacc6 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js @@ -59,6 +59,12 @@ function BaseAttachmentViewPdf({ [attachmentCarouselPagerContext, isUsedInCarousel, onScaleChangedProp], ); + /** + * This callback is used to pass-through the onPress event from the AttachmentViewPdf's props + * as well trigger the onTap event from the context. + * The onTap event should only be triggered, if the pager is currently scrollable. + * Otherwise it means that the PDF is currently zoomed in, therefore the onTap callback should be ignored + */ const onPress = useCallback( (e) => { if (onPressProp !== undefined) {