-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[RNMobile] Add useScrollWhenDragging
hook
#39705
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,6 +24,7 @@ import { usePreferredColorSchemeStyle } from '@wordpress/compose'; | |
/** | ||
* Internal dependencies | ||
*/ | ||
import useScrollWhenDragging from './use-scroll-when-dragging'; | ||
import DraggableChip from './draggable-chip'; | ||
import { store as blockEditorStore } from '../../store'; | ||
import { useBlockListContext } from '../block-list/block-list-context'; | ||
|
@@ -90,10 +91,19 @@ const BlockDraggableWrapper = ( { children } ) => { | |
const isDragging = useSharedValue( false ); | ||
const scrollAnimation = useSharedValue( 0 ); | ||
|
||
const [ | ||
startScrolling, | ||
scrollOnDragOver, | ||
stopScrolling, | ||
draggingScrollHandler, | ||
] = useScrollWhenDragging(); | ||
|
||
const scrollHandler = ( event ) => { | ||
'worklet'; | ||
const { contentOffset } = event; | ||
scroll.offsetY.value = contentOffset.y; | ||
|
||
draggingScrollHandler( event ); | ||
}; | ||
|
||
// Stop dragging blocks if the block draggable is unmounted. | ||
|
@@ -124,9 +134,13 @@ const BlockDraggableWrapper = ( { children } ) => { | |
0, | ||
blockLayout.y - EXTRA_OFFSET_WHEN_CLOSE_TO_TOP_EDGE | ||
); | ||
scrollAnimation.value = withTiming( scrollOffsetTarget, { | ||
duration: SCROLL_ANIMATION_DURATION, | ||
} ); | ||
scrollAnimation.value = withTiming( | ||
scrollOffsetTarget, | ||
{ duration: SCROLL_ANIMATION_DURATION }, | ||
() => startScrolling( position.y ) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In case we need to animate the scroll because the block is out of scroll view, we'll only notify the hook about starting scrolling once the animation finishes. |
||
); | ||
} else { | ||
runOnUI( startScrolling )( position.y ); | ||
} | ||
} else { | ||
// We stop dragging if no block is found. | ||
|
@@ -166,6 +180,9 @@ const BlockDraggableWrapper = ( { children } ) => { | |
const dragPosition = { x, y }; | ||
chip.x.value = dragPosition.x; | ||
chip.y.value = dragPosition.y; | ||
|
||
// Update scrolling velocity | ||
scrollOnDragOver( dragPosition.y ); | ||
}; | ||
|
||
const stopDragging = () => { | ||
|
@@ -174,6 +191,7 @@ const BlockDraggableWrapper = ( { children } ) => { | |
|
||
chip.scale.value = withTiming( 0 ); | ||
runOnJS( stopDraggingBlocks )(); | ||
stopScrolling(); | ||
}; | ||
|
||
const chipDynamicStyles = useAnimatedStyle( () => { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { useWindowDimensions } from 'react-native'; | ||
import { | ||
useSharedValue, | ||
useAnimatedRef, | ||
scrollTo, | ||
useAnimatedReaction, | ||
withTiming, | ||
withRepeat, | ||
cancelAnimation, | ||
Easing, | ||
} from 'react-native-reanimated'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { useBlockListContext } from '../block-list/block-list-context'; | ||
|
||
const SCROLL_INACTIVE_DISTANCE_PX = 50; | ||
const SCROLL_INTERVAL_MS = 1000; | ||
const VELOCITY_MULTIPLIER = 5000; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If needed, we could tweak this value to increase the acceleration of the scroll velocity. |
||
|
||
/** | ||
* React hook that scrolls the scroll container when a block is being dragged. | ||
* | ||
* @return {Function[]} `startScrolling`, `scrollOnDragOver`, `stopScrolling` | ||
* functions to be called in `onDragStart`, `onDragOver` | ||
* and `onDragEnd` events respectively. Additionally, | ||
* `scrollHandler` function is returned which should be | ||
* called in the `onScroll` event of the block list. | ||
*/ | ||
export default function useScrollWhenDragging() { | ||
const { scrollRef } = useBlockListContext(); | ||
const animatedScrollRef = useAnimatedRef(); | ||
animatedScrollRef( scrollRef ); | ||
|
||
const { height: windowHeight } = useWindowDimensions(); | ||
|
||
const velocityY = useSharedValue( 0 ); | ||
const offsetY = useSharedValue( 0 ); | ||
const dragStartY = useSharedValue( 0 ); | ||
const animationTimer = useSharedValue( 0 ); | ||
const isAnimationTimerActive = useSharedValue( false ); | ||
const isScrollActive = useSharedValue( false ); | ||
|
||
const scroll = { | ||
offsetY: useSharedValue( 0 ), | ||
maxOffsetY: useSharedValue( 0 ), | ||
}; | ||
const scrollHandler = ( event ) => { | ||
'worklet'; | ||
const { contentSize, contentOffset, layoutMeasurement } = event; | ||
scroll.offsetY.value = contentOffset.y; | ||
scroll.maxOffsetY.value = contentSize.height - layoutMeasurement.height; | ||
}; | ||
|
||
const stopScrolling = () => { | ||
'worklet'; | ||
cancelAnimation( animationTimer ); | ||
|
||
isAnimationTimerActive.value = false; | ||
isScrollActive.value = false; | ||
velocityY.value = 0; | ||
}; | ||
|
||
const startScrolling = ( y ) => { | ||
'worklet'; | ||
stopScrolling(); | ||
offsetY.value = scroll.offsetY.value; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since the scroll animation requires providing the absolute offset, we have to get the initial offset at the time the dragging starts. This way we can make the offset calculations using the initial offset. |
||
dragStartY.value = y; | ||
|
||
animationTimer.value = 0; | ||
animationTimer.value = withRepeat( | ||
withTiming( 1, { | ||
duration: SCROLL_INTERVAL_MS, | ||
easing: Easing.linear, | ||
} ), | ||
-1, | ||
true | ||
); | ||
Comment on lines
+74
to
+82
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The scroll animation needs to be running indefinitely until the drag gesture ends. For this reason, and in order of animating the scroll with Reanimated, we create a timer that repeats forever and animates This timer is used below in |
||
isAnimationTimerActive.value = true; | ||
}; | ||
|
||
const scrollOnDragOver = ( y ) => { | ||
'worklet'; | ||
const dragDistance = Math.max( | ||
Math.abs( y - dragStartY.value ) - SCROLL_INACTIVE_DISTANCE_PX, | ||
0 | ||
); | ||
Comment on lines
+88
to
+91
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The scroll is only animated if we drag the block beyond a threshold (i.e. |
||
const distancePercentage = dragDistance / windowHeight; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This percentage is used for increasing the scroll velocity depending on the distance between the drag starting position and the current drag position. |
||
|
||
if ( ! isScrollActive.value ) { | ||
isScrollActive.value = dragDistance > 0; | ||
} else if ( y > dragStartY.value ) { | ||
// User is dragging downwards. | ||
velocityY.value = VELOCITY_MULTIPLIER * distancePercentage; | ||
} else if ( y < dragStartY.value ) { | ||
// User is dragging upwards. | ||
velocityY.value = -VELOCITY_MULTIPLIER * distancePercentage; | ||
} else { | ||
velocityY.value = 0; | ||
} | ||
}; | ||
|
||
useAnimatedReaction( | ||
() => animationTimer.value, | ||
( value, previous ) => { | ||
if ( velocityY.value === 0 ) { | ||
return; | ||
} | ||
|
||
const delta = Math.abs( value - previous ); | ||
let newOffset = offsetY.value + delta * velocityY.value; | ||
Comment on lines
+114
to
+115
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The scroll is updated by calling Reanimated's |
||
|
||
if ( scroll.maxOffsetY.value !== 0 ) { | ||
newOffset = Math.max( | ||
0, | ||
Math.min( scroll.maxOffsetY.value, newOffset ) | ||
); | ||
} else { | ||
// Scroll values are empty until receiving the first scroll event. | ||
// In that case, the max offset is unknown and we can't clamp the | ||
// new offset value. | ||
newOffset = Math.max( 0, newOffset ); | ||
} | ||
|
||
offsetY.value = newOffset; | ||
scrollTo( animatedScrollRef, 0, offsetY.value, false ); | ||
} | ||
); | ||
|
||
return [ startScrolling, scrollOnDragOver, stopScrolling, scrollHandler ]; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
useScrollWhenDragging
hook also needs to listen to scroll events, hence we connect its scroll handler with the one defined inBlockDraggable
.