Skip to content
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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 );
Comment on lines +105 to +106
Copy link
Contributor Author

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 in BlockDraggable.

};

// Stop dragging blocks if the block draggable is unmounted.
Expand Down Expand Up @@ -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 )
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.
Expand Down Expand Up @@ -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 = () => {
Expand All @@ -174,6 +191,7 @@ const BlockDraggableWrapper = ( { children } ) => {

chip.scale.value = withTiming( 0 );
runOnJS( stopDraggingBlocks )();
stopScrolling();
};

const chipDynamicStyles = useAnimatedStyle( () => {
Expand Down
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;
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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;
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 animationTimer value from 0 to 1 every 1 second and in reverser, from 1 to 0.

This timer is used below in useAnimatedReaction block for calculating the offset to be increased on every frame.

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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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. SCROLL_INACTIVE_DISTANCE_PX constant).

const distancePercentage = dragDistance / windowHeight;
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scroll is updated by calling Reanimated's scrollTo function, which prompts for the absolute scroll offset. In this case, we calculate the offset by adding to the current offset the pixels that have been moved on each frame (i.e. delta * scroll velocity).


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 ];
}