question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Support synchronous onScroll events on the UI thread by using Reanimated

See original GitHub issue

Describe the feature

Currently the onPageScroll, onPageSelected and onPageScrollStateChanged events are dispatched as bubbling events using the batched bridge. This is really slow and doesn’t allow for any smooth (60 FPS) interpolations based on scroll state, such as smooth title changes etc.

I’m suggesting to support synchronous callbacks by using the Reanimated APIs (worklets on the UI thread), which shouldn’t be too hard to implement and allow for perfectly smooth 60 FPS animations all without crossing threads and spamming the bridge (see react-native-gesture-handler, they do the same thing with the onGesture event.)

Motivation

  • Way better Performance
  • No Bridge-spam
  • Allow reanimated worklet interpolations with 60 FPS

Implementation

I wanted to do the same thing for a gyroscope library but haven’t come around to actually implement it. I believe reanimated already does a lot of work for you, but I don’t exactly know how this is going to work. I’ll have to take a look myself, but it looks like they also just have an RCTEventDispatcher set up, which then simply sends those events… 🤔 So I think there are no native code changes required here, but we somehow need a hook similar to the useAnimatedGestureHandler hook.

Issue Analytics

  • State:open
  • Created 2 years ago
  • Reactions:5
  • Comments:7

github_iconTop GitHub Comments

2reactions
mrousavycommented, Aug 27, 2021

This PR has been merged, allowing the low-level useHandler hooks to be used.

I don’t have an app anymore that uses this Pager View library, but I will start working on this feature once reanimated ships a release anyway just because I think the challenge is interesting 😅

0reactions
izakfilmaltercommented, Sep 23, 2022

I was able to do the following to get it working with reanimated. This is a modified version of react-native-tab-view:

import type {
  EventEmitterProps,
  Listener,
  NavigationState,
  PagerProps,
  Route,
} from 'Components/react-native-tab-view/types'
import {
  DependencyList,
  ReactElement,
  ReactNode,
  useCallback,
  useEffect,
  useRef,
} from 'react'
import { Keyboard, StyleSheet } from 'react-native'
import ViewPager from 'react-native-pager-view'
import {
  PagerViewOnPageScrollEventData,
  PageScrollStateChangedEvent,
} from 'react-native-pager-view/src/types'
import Animated, {
  add,
  call,
  useEvent,
  useHandler,
  useSharedValue,
} from 'react-native-reanimated'

const AnimatedViewPager = Animated.createAnimatedComponent(ViewPager)

export function usePagerScrollHandler(
  handlers: {
    onPageScroll: (event: PagerViewOnPageScrollEventData, context: {}) => void
  },
  dependencies?: DependencyList,
) {
  const { context, doDependenciesDiffer } = useHandler(handlers, dependencies)
  const subscribeForEvents = ['onPageScroll']

  return useEvent<PagerViewOnPageScrollEventData>(
    (event) => {
      'worklet'
      const { onPageScroll } = handlers

      // console.log(event)

      if (onPageScroll) {
        onPageScroll(event, context)
      }
    },
    subscribeForEvents,
    doDependenciesDiffer,
  )
}

export function usePageScrollStateChangedHandler(
  handlers: {
    idle?: (event: PageScrollStateChangedEvent, context: {}) => void
    dragging?: (event: PageScrollStateChangedEvent, context: {}) => void
    settling?: (event: PageScrollStateChangedEvent, context: {}) => void
  },
  dependencies?: DependencyList,
) {
  const { context, doDependenciesDiffer } = useHandler(handlers, dependencies)
  const subscribeForEvents = ['onPageScroll']

  return useEvent<PageScrollStateChangedEvent>(
    (event) => {
      'worklet'
      const { idle, dragging, settling } = handlers
      const { pageScrollState } = event

      if (idle && pageScrollState === 'idle') {
        idle(event, context)
      }

      if (dragging && pageScrollState === 'dragging') {
        dragging(event, context)
      }

      if (settling && pageScrollState === 'settling') {
        settling(event, context)
      }
    },
    subscribeForEvents,
    doDependenciesDiffer,
  )
}

type Props<T extends Route> = PagerProps & {
  onIndexChange: (index: number) => void
  navigationState: NavigationState<T>
  children: (
    props: EventEmitterProps & {
      // Animated value which represents the state of current index
      // It can include fractional digits as it represents the intermediate value
      position: ReturnType<typeof add>
      // Function to actually render the content of the pager
      // The parent component takes care of rendering
      render: (children: ReactNode) => ReactNode
      // Callback to call when switching the tab
      // The tab switch animation is performed even if the index in state is unchanged
      jumpTo: (key: string) => void
    },
  ) => ReactElement
}

export function Pager<T extends Route>({
  keyboardDismissMode = 'auto',
  swipeEnabled = true,
  navigationState,
  onIndexChange,
  onSwipeStart,
  onSwipeEnd,
  children,
  style,
  ...rest
}: Props<T>) {
  const { index } = navigationState

  const listenersRef = useRef<Array<Listener>>([])

  const pagerRef = useRef<ViewPager | null>()
  const indexRef = useRef<number>(index)
  const navigationStateRef = useRef(navigationState)

  const position = useSharedValue(index)
  const offset = useSharedValue(0)

  useEffect(() => {
    navigationStateRef.current = navigationState
  })

  const jumpTo = useCallback((key: string) => {
    const index = navigationStateRef.current.routes.findIndex(
      (route: { key: string }) => route.key === key,
    )

    pagerRef.current?.setPage(index)
  }, [])

  useEffect(() => {
    if (keyboardDismissMode === 'auto') {
      Keyboard.dismiss()
    }

    if (indexRef.current !== index) {
      pagerRef.current?.setPage(index)
    }
  }, [keyboardDismissMode, index])

  const newOnPageScrollStateChanged = usePageScrollStateChangedHandler({
    idle: () => {
      'worklet'
      onSwipeEnd?.()
      return
    },
    dragging: () => {
      call([offset.value], ([x]) => {
        const next = index + (x > 0 ? Math.ceil(x) : Math.floor(x))

        if (next !== index) {
          listenersRef.current.forEach((listener) => listener(next))
        }
      })

      onSwipeStart?.()
      return
    },
  })

  const addEnterListener = useCallback((listener: Listener) => {
    listenersRef.current.push(listener)

    return () => {
      const index = listenersRef.current.indexOf(listener)

      if (index > -1) {
        listenersRef.current.splice(index, 1)
      }
    }
  }, [])

  const handler = usePagerScrollHandler({
    onPageScroll: (event) => {
      'worklet'
      position.value = event.position
      offset.value = event.offset
    },
  })

  return children({
    position: add(position.value, offset.value),
    addEnterListener,
    jumpTo,
    render: (children) => (
      <AnimatedViewPager
        {...rest}
        ref={(x) => (pagerRef.current = x as ViewPager | null)}
        style={[styles.container, style]}
        initialPage={index}
        keyboardDismissMode={
          keyboardDismissMode === 'auto' ? 'on-drag' : keyboardDismissMode
        }
        onPageScroll={handler}
        onPageSelected={(e) => {
          const index = e.nativeEvent.position
          indexRef.current = index
          onIndexChange(index)
        }}
        onPageScrollStateChanged={newOnPageScrollStateChanged}
        scrollEnabled={swipeEnabled}
      >
        {children}
      </AnimatedViewPager>
    ),
  })
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
})
Read more comments on GitHub >

github_iconTop Results From Across the Web

Shared Values | React Native Reanimated
Above, the scroll handler is a worklet and runs the scroll event logic on the UI thread. Updates made in that worklet are...
Read more >
Getting started with Reanimated 2 for React native Part-1
From the example given above from the official docs we can see that ,the scroll handler is a worklet and runs the scroll...
Read more >
React Native Gesture Handler + Reanimated Flat List Scroll
I'm having a problem with react-native-gesture handler animation in ... on the flatList the behavior of PanGesture not trigger scroll event.
Read more >
Swipe to delete Animation in React Native with Reanimated 2
In this tutorial we'll learn how to create a Dismiss Animation in React Native.We'll use these packages: - react-native- reanimated v2: ...
Read more >
Animations
The native driver also works with Animated.event . This is especially useful for animations that follow the scroll position as without the ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found