7 MIN READ
   //   Mar 2, 2020

Understanding PanResponder with react-native-dragging-list

Kuldeep Singh

A simpler way to drag and drop list items.

Nowadays, the internet has solutions to almost all sorts of problems but Alas! This is not the case when it comes to issues related to PanResponder and Animated component/library of React Native. There are very few blogs or even StackOverflow questions addressing these issues and hence this blog.
If you too find yourself in the middle of this, stay put, this blog will help you since this package I build makes good use of these components and a detailed explanation is always appreciated!

Overview

This is a React Native package for dragging and dropping list items provided all list items are of the same height. It mainly makes use of PanResponder and Animated components/library of React Native along with other components like FlatList, etc.
So, let’s begin with an overview of PanResponder and Animated component/library of React Native.

PanResponder

This is a React Native component which enables apps to recognise touch and work upon them. It is created using the PanResponder.create({}) function and includes several predefined methods like - 

  • onPanResponderGrant: This function accepts 2 parameters - event and gestureState.
    Event is a touch event which contains various parameters like the position of the touch, target, timestamp, etc.
    gestureState is an object which contains parameters like the latest coordinates of the recent movement, accumulated distance, velocity of the gesture, etc. We can extract out the values of x and y coordinates of a particular item from gestureState.
    This function is triggered at the start and can be used to initialise variables with a default value that we want such as the x and y position of the list item.

  • onPanResponderMove: This function is triggered every time a movement is recognised on the app such as scrolling, dragging irrespective of the above function. This function enables us to track the distance travelled by a particular list item along the x and y-axis which can be stored and worked upon.

  • onPanResponderRelease: This function is triggered when the user has released all the touches. So, here we can implement the functionality that we want to execute at the end of the gesture.

You can read more about PanResponder here.

Animated

This React Native library is designed to enable animations in our app easily and effectively. It comes with loads of predefined methods even with start/stop functionality to control time-based animation execution.

Animated provides three types of animations. It essentially determines how the initial value animates to the final value. Following are the methods through which these animations can be implemented

  1. Animated.decay()

  2. Animated.spring()

  3. Animated.timing()

Using Native Driver: Sometimes, using Animated library may lead to a drop in JS frames. We can avoid this by specifying useNativeDriver:true in our animation configuration. It enables the native code to animate on the UI thread and blocks the JS thread once the animation has started without affecting the animation. So the animation will not get affected even if the JS frames drop.

You can read more about this library in detail here.

You can also check out this blog for a better understanding of PanResponder and Animated components and their working.

Getting Started with react-native-dragging-list

Step 1: Import PanResponder, Animated, View, etc. from React Native.

import { PanResponder, Animated, View, Text, FlatList } from ‘react-native’;

Step 2: Now we need to define some variables that we will be using later in our code:

point = new Animated.ValueXY()
scale = new Animated.Value(1)
currentY = 0;
scrollOffset = 0;
flatlistTopOffset = 0;
rowHeight = 0;
currentIndex = -1;
active = false;
  • point: It is an animated value to keep track of the list item position on the screen. It stores the position in the form of x & y coordinates of the list item.

  • scale: It is also an animated value and is set to 1 by default. This will be used to zoom in the list item whenever it is touched and dragged.

  • currentY: This variable will store the current/initial position of the list item as soon as it is touched. And since the list item will be dragged vertically, it only stores the y value of it.

  • scrollOffset: This variable stores the distance by which a particular list item is dragged w.r.t its initial position. It will help reposition other items with respect to the item which is dragged.

  • flatlistTopOffset: This variable indicates the distance of the list from the top of the screen.

  • rowHeight: As the name suggests, this variable stores each item’s height which, in our example, is the same for all the items.

  • currentIndex: This is the current index of the list item which is touched. This helps set draggingIndex of the item which is dragged. Its default value is set to -1.

  • active: This variable stores the boolean value to activate/deactivate PanResponder. By default, it is set to false and will be set to active inside the onPanResponderGrant method.

After this, we need to create our PanResponder inside the constructor and define methods according to our requirement in a similar way as implemented in the links provided above.

The Development Process ft. PanResponder

onPanResponderGrant

In this method, we are storing the currentIndex of the list item that is being dragged and executing an animated event through which we are mapping the y values of the list item as shown below

onPanResponderGrant: (evt, gestureState) => {

  this.currentIndex = this.yToIndex(gestureState.y0)

  this.currentY = gestureState.y0;

  Animated.event([{ y: this.point.y }])({ y: gestureState.y0 -     this.rowHeight / 2 })

  this.active = true;

  this.setState({ draggingIndex: this.currentIndex, dragging: true }, () => {
  this.animateList();

  })

  Animated.timing(
    this.scale,
    { toValue: 1.2, duration: 0 }
  ).start();
  return true;
}

Animated.timing is one of the three methods of the Animated library which we discussed earlier. It is used to zoom in a particular list item as soon as touch is detected on it by scaling it to a value of 1.2 from the original 1 with a duration of 0.

animateList is a method which returns whenever the active is set to false.

yToIndex is a separate function and gestureState.y0 is passed as a parameter to it and is accessed as y inside the function. This function calculates the currentIndex by adding the scrollOffset to the current y ( i.e. gestureState.y0 ) position of the item and subtracting the flatlistTopOffset ( if any ) and then dividing the whole by the rowHeight.

const value = Math.floor((this.scrollOffset + y - this.flatlistTopOffset) / this.rowHeight);

The values of scrollOffset, flatlistTopOffset are extracted from the event triggered via onScroll and onLayout properties of the <FlatList /> component defined in the return method.

onScroll = { e => {

 this.scrollOffset = e.nativeEvent.contentOffset.y

}}

onLayout = { e => {

 this.flatlistTopOffset = e.nativeEvent.layout.y

}}

Similarly, rowHeight is accessed from the onLayout property of the list item defined in the renderItem method.

onLayout = { e => {
this.rowHeight = e.nativeEvent.layout.height
}}

Apart from this, we set the PanResponder as active by providing this.active = true; and updating our draggingIndex with the currentIndex in state and setting dragging: true since touch is recognised and now the item is about to be dragged.

onPanResponderMove

As the name suggests, here we will implement the functionality that we need to execute while the movement is happening on the screen, whether it is scrolling or dragging of an item.

onPanResponderMove: (evt, gestureState) => {

  this.currentY = gestureState.moveY;

  Animated.event([{ y: this.point.y }])({ y: gestureState.moveY })

  const newIndex = this.yToIndex(this.currentY);

  if (this.currentIndex !== newIndex) {

    this.setState({

      data: immutableMove(this.state.data, this.currentIndex, newIndex),

      draggingIndex: newIndex

    });

    this.currentIndex = newIndex;

  }

  this.animateList()

  return true;

}

This method is quite similar to the onPanResponderGrant method. Here, we are mapping the currentY to the distance a list item has travelled w.r.t its drag. We are also calculating the newIndex by passing currentY to the yToIndex method. One thing to notice here is that we are updating our data array through the immutableMove function and this method enables other items to change position w.r.t the item which is being dragged around. Since this update is performed in this method, the state will be updated on every movement detected on the screen and hence we will see the items adjusting themselves whether we drag down or up.

onPanResponderRelease

This method is called as soon as the touch is released from the screen. Here, we can define the functionality like resetting all the variables to their default value as they were in the beginning.

onPanResponderRelease: (evt, gestureState) => {

  this.reset()

  this.point.flattenOffset();

  Animated.timing(

    this.scale,

    { toValue: 1, duration: 0 }

  ).start();

  return true;

}

As explained, the scaling is set to its default value again so that the list item zoomed out to its original size. Reset method is also called here in which

this.active = false;

this.setState({ draggingIndex: -1, dragging: false })

I think this method is self-explanatory since it is called at the end when the touch is released.

This was the crux of the implementation of react-native-dragging-list as well as React Native PanResponder. 

What remains now is defining the render and return method of this package that we are building. 

This package uses <FlatList> to display the list and since this component uses renderItem method to map the items, we need to define it inside our render method 

const renderItem = ({ item, index }) => (

  <View 

    onLayout = { this.setRowHeight }

  >

    <RenderItem item = { item } />

    <View { ...this._panResponder.panHandlers}>

      <DraggingHandle />

    </View>

  </View>

)

Here, <RenderItem /> and <DraggingHandle /> are sent as props by users who will be using this package in their application. The latter component is wrapped inside a <View> to which we have attached our PanResponder. This component enables the dragging of an item.

<View { ...this._panResponder.panHandlers}>

In return method, the conventional <FlatList> will be rendered along with an <Animated.View> component. The latter component will accept the renderItem method and it will be visible only if dragging is set to true because we need to drag a particular item only if the touch is enabled and dragging is set to true in our onPanResponderGrant method.

{dragging && (

  <Animated.View 

    style = {{ 

      zIndex: 2, 

      top: this.point.getLayout().top, 

      width: '100%', 

      position: 'absolute',

      transform: [{ scale }]

    }}

  >

    { renderItem({ item: data[draggingIndex], index: -1 }, true)}

  </Animated.View>

)}


The zoom in and out effect is accomplished by the transform property added to the styling of the component.

The <FlatList> will accept the props like scrollEnabled, onLayout, onScroll in addition to the obvious props like data, renderItem, etc.

Be cautious!

Some things need to be kept in mind while using PanResponder and this package in your application: 

  1. The working of PanResponder can hinder the normal scrolling of the FlatList since onPanResponderMove is called even when you try to scroll normally. So, you ought to find a workaround to this problem.
    When I faced this issue while building this package, I defined a separate component { <DraggingHandle /> } to handle the PanResponder and a variable { dragging } to keep track whether an item is being dragged or not.

  2. Make sure you understand the working and implementation of PanResponder and other React Native libraries like Animated before you start using them for development.

That’s all!

And this is all PanResponder and react-native-dragging-list is all about.

I hope you were engaged throughout and were able to understand the working of PanResponder and the development process of react-native-dragging-list. This will surely help you in building effective and animation enabled applications using PanResponder and Animated library provided by React Native.

I had a great time building this package. There were a lot of challenges in understanding the functionality of PanResponder and a lot of research had to be done. But learning something from scratch is always fun. It was, no doubt, a great learning experience and fun to implement.

Important Links: 

  1. Since whole code can’t be written here, this is the link to the GitHub repository where you can check out the entire code for this package.

  2. The following videos helped me out in building this package. You can get an idea of implementation from these but copying and pasting are not recommended since these too have their shortcomings.
    Part 1, Part 2

Keep learning!!