While using the “hype” Clubhouse app, I noticed an unusual scrolling when choosing user interests. In this article, I decided to tell you how to implement a similar scrolling for labels in React Native without using third-party modules and libraries.
We will make several rows of horizontal scroll-components, on top of which will be PanResponder. It will intercept all scroll gestures and control the behavior of scroll components. The Flatlist will act as a scroll component, since it is better optimized for displaying a large list of data.
We will also provide additional functionality for our component:
- Don’t block/intercept gestures if our component is already in another scroll component;
- Dynamic change of elements for a row and the number of separate rows for scrolling;
- Required and optional component props;
- If the required props are not set, the component must not be rendered.
- If the number of items to scroll is not enough, we render them without Flatlist, just in View with flexDirection row.
First, let’s create a parent component in which we will render our component with two required parameters — data and renderItem (the names are identical for Flatlist). The rows parameter is responsible for the number of horizontal scrolling rows.
One list item (label) is a touchable-wrapper over the picture and text of varying length. I got the test data using mockaroo.
Prop data is an array with label data. We need to divide it into subarrays with an unequal number, so that there are fewer elements in the first rows, and more in the last ones. Schematically it can be represented as follows:
1 Array *****2 Array *********3 Array *************4 Array ******************
The first row of scrolls will contain the least number of items and will scroll slower than the last row with the most items. Separation arrays into subarrays with different number of elements implements function splitArrayToUnequal. We also memoize the result of this function and add to our component prop reverse (the first row will contain the largest number of elements) and random (random order of the rows).
Now that we have the data, we can display it in the Flatlist:
- Set the layout of Flatlist children horizontally in the row, hide the scroll indicator;
- In the scrollListRefs object, we will store the reference of each scroll for further control;
- By the onScroll callback, we save the horizontal position of the scroll;
- Using the onContentSizeChange callback, we determine the full length of the row in pixels.
Now each of the rows can be scrolled separately:
PanResponder is a React Native wrapper for responder handlers provided by the gesture responder system. Let’s wrap all Flatlists in a gesture handler (panResponder.panHandlers) and handle them. Methods used:
- In onMoveShouldSetPanResponder we “filter” the user’s gesture — if he makes a swipe, return true and subsequent callbacks will be called; if only tap on the screen — return false;
- Responders onPanResponderTerminationRequest and onShouldBlockNativeResponder allow us not to intercept gestures if our component is already in another scroll component or panResponder;
- onPanResponderMove is called if the gesture handler is activated and motion (swipe) is detected. This callback is called frequently — for every pixel of “movement”.
The tempPanResponderMoveX variable stores the last horizontal coordinates of the user’s touch. The calculatedOffset calculates a new offset for each Flatlist, based on the length (scrollRowFullWidth) of the row, which is then added to or subtracted from the current scroll position. If gesureState.dx> 0, the user swipes to the left, and vice versa: if swipes to the right — add a new offset to the scroll position. Scroll to the new coordinates is run by scrollToOffset with parameter animated false, because onPanResponderMove method is called frequently, and animation will not keep up with the upgrade scroll position.
Now all elements are scrolled at a speed proportional to the length of the row. With default settings, the bottom scroll contains the largest number of elements and scrolls faster than the top one. When the user finishes the swipe and removes their finger from the screen, the elements simply stop moving. Let’s add animation to complete the swipe. To do this, we use the onPanResponderRelease method, which is called just at the moment the gesture ends:
- The logic for defining a new offset is about the same as for onPanResponderMove. It depends on the length in pixels of a particular Flatlist;
- If the user completes the movement quickly (with gesture velocity greater than 1.3), increase the offset further by the value of gestureState.vx;
With the animation of the end of the swipe, the picture is much better. All that remains is to add some finishing touches to our component:
- defaultProps, which defines the default number of rows, the minimum number of items to scroll and the minimum number of items in one row;
- The code in useEffect is needed to dynamically update the state if props rows and data have changed;
- If the required props data and renderItem are not set, return null;
- If the number of elements (labels) is not greater than the minimum value for our component — render the elements without Flatlist.
Thanks to Flatlist, which does not render all elements at once, it is possible to display a large number of elements. Below I have represented the output of 1000 elements with 13 rows of scrolling:
By writing a small component in React Native, you can make sure that handling gestures with PanResponder is simple enough and no additional libraries are required. Flatlist is an efficient way to create simple and straightforward lists.
The complete component code can be found on my github page: https://github.com/RadiksMan/test-label-scroller
This article in Russian can be read here — https://dou.ua/forums/topic/33138