Skip to content

Comments

Feature: Expose isDragging state via Context#493

Open
romulosalmeida wants to merge 1 commit intoPedroBern:mainfrom
romulosalmeida:feat/isDragging-state
Open

Feature: Expose isDragging state via Context#493
romulosalmeida wants to merge 1 commit intoPedroBern:mainfrom
romulosalmeida:feat/isDragging-state

Conversation

@romulosalmeida
Copy link

@romulosalmeida romulosalmeida commented Jan 27, 2026

Motivation

Currently, there's no way for external components to know when the user is actively dragging a scrollable tab. This limits the ability to coordinate animations and gestures between custom animation (headers in my case) and the scroll content.

What's New

Added a new isDragging shared value to the Context, allowing any component within Tabs.Container to reactively track the drag state of the focused tab.

Changes

Container.tsx:
Added isDragging shared value initialization
Exposed isDragging in the Context Provider

hooks.tsx:
Added isDragging to useTabsContext() destructuring in useScrollHandlerY
Added setDragging helper function
Update isDragging to true on onBeginDrag
Update isDragging to false on onEndDrag

types.ts
Added isDragging: Animated.SharedValue to ContextType

Use Case

This feature enables developers to react to drag state changes from any component within the Tabs context. Example use cases:

Cancel header animations on drag: When implementing custom scrollable headers with momentum/decay animations, cancel them when the user starts dragging the scroll view to avoid conflicts
UI feedback: Show visual indicators during active scrolling
Gesture coordination: Prevent or allow gestures based on scroll drag state

Example:

import React, { useEffect } from 'react';
import { cancelAnimation, useAnimatedReaction, useSharedValue, withDecay } from 'react-native-reanimated';
import { useScroller, useTabsContext, useScrollHandlerY } from 'react-native-collapsible-tab-view/src/hooks';
import { useCurrentTabScrollY } from 'react-native-collapsible-tab-view';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import { View } from 'react-native';

const ScrollableHeader = ({
    children,
    style,
    minDistance = 5,
    minVelocity = 50,
    deceleration = 0.9995,
}) => {
    const { refMap, focusedTab, isDragging } = useTabsContext();
    const scrollTo = useScroller();
    const scrollY = useCurrentTabScrollY();
    const initialScrollY = useSharedValue(0);
    const isGestureActive = useSharedValue(false);
    const targetScrollY = useSharedValue(0);
    
    useAnimatedReaction(
        () => isDragging.value,
        (isDragging) => {
            'worklet';

            if (isDragging)
                cancelAnimation(targetScrollY)
            
        }, [focusedTab]
    );


    useAnimatedReaction(
        () => targetScrollY.value,
        (targetY) => {
            'worklet';
            if (!isGestureActive.value ) {
                const currentTab = focusedTab.value;
                const ref = refMap[currentTab];
                if (ref) {
                    scrollTo(ref, 0, Math.max(0, targetY), false, 'momentumScroll');
                }
            }
        },
        [refMap, focusedTab, scrollTo],
    );

    const headerPanGesture = Gesture.Pan()
        .minDistance(minDistance)
        .onStart(() => {
            'worklet';
            cancelAnimation(targetScrollY);
            initialScrollY.value = scrollY.value;
            targetScrollY.value = scrollY.value;
            isGestureActive.value = true;
        })
        .onUpdate((e) => {
            'worklet';

            
            if (Math.abs(e.translationY) > Math.abs(e.translationX) || Math.abs(e.translationY) > 10) {
                const currentTab = focusedTab.value;
                const ref = refMap[currentTab];

                if (ref) {
                    const delta = -e.translationY;
                    const newTargetScrollY = Math.max(0, initialScrollY.value + delta);
                    targetScrollY.value = newTargetScrollY;
                    scrollTo(ref, 0, newTargetScrollY, false, 'headerGesture');
                }
            }
        })
        .onEnd((e) => {
            'worklet';
            isGestureActive.value = false;

            if (Math.abs(e.velocityY) > minVelocity) {
                const velocity = -e.velocityY;

                targetScrollY.value = withDecay(
                    {
                        velocity: velocity,
                        deceleration: deceleration,
                        clamp: [0, Infinity],
                    },
                    (finished) => {
                        'worklet';
                        if (finished) {
                            const finalY = Math.max(0, targetScrollY.value);
                            targetScrollY.value = finalY;
                        }
                    },
                );
            } else {
                targetScrollY.value = Math.max(0, scrollY.value);
            }
        })
        .onFinalize(() => {
            'worklet';
            isGestureActive.value = false;
        });

    return (
        <GestureDetector gesture={headerPanGesture}>
            <View style={style}>{children}</View>
        </GestureDetector>
    );
};

export default ScrollableHeader;

Testing

  • isDragging.value updates to true on onBeginDrag
  • isDragging.value updates to false on onEndDrag
  • External components can access and react to isDragging via useTabsContext()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant