Making an Input Slider in React Native with the Animated and Panresponder APIs
Tags: reactnative, react, animated, panresponder
Published: 2020-12-03 18:55:26

---
title: Making an Input Slider in React Native with the Animated and Panresponder APIs
published: true
description: Tutorial to build an animated slider/range input in React Native using the Animated and PanResponder APIs
tags: #reactnative, #react, #animated #panresponder
cover_image: https://www.appstud.com/wp-content/uploads/2018/03/React-Native-Titre.png
---

This is the second of a two-part series on the React Native PanResponder API. See the first part [here](https://dev.to/rmunschie92/building-a-movable-animated-component-in-react-native-4o96).

Now that we have a draggable component, let’s update our logic to give it the desired “slider” input behavior. The first step here is quite simple, we simply want to disable the `y` value from being updated when we move the Animated View component. To do this we’ll go into our `onPanResponderMove` handler and update the `y ` value to `0` in `this.state.animate.setValue()`:

```javascript
// The user is moving their finger
onPanResponderMove: (e, gesture) => {
  // Set value of state.animate x to the delta for x and y to 0
  // to prevent vertical movement
  this.state.animate.setValue({ x: gesture.dx, y: 0 });
},
```

With these changes made, refresh your simulator and try to move the component around the screen. Now you should be able to move it from along the screen’s x-axis, but any vertical movement should be disabled.

Next, let’s include styles for a basic slider. Back in `Container.js`, start by wrapping the instance of `<Movable />` in a `View` component. Let’s give our new `View` some basic styles to make it actually look something like an HTML range-type input; for now just a set width and height, a border, a border radius, and justify the `View` content along the component’s center.

Your Container component should now look like this:

```javascript
export class Container extends Component {
  render() {
    return (
    	<ScrollView contentContainerStyle={styles.container} canCancelContentTouches={false}>    				 
          <View style={styles.slider}>
      	    <Movable />
      	  </View>   
	</ScrollView>  
    ); 
	}
} 
    
const styles = StyleSheet.create({
  container: {
    height: vh(100),
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center'
  },
  slider: {
    height: vh(1.5),
    width: vw(85),
    borderWidth: 1,
    borderRadius: 25,
    justifyContent: 'center'
  }
});
```

Things should be _looking_ pretty good by now, but there’s an issue - nothing stops the user from simply dragging `<Movable />` right outside the horizontal boundaries of the <View> component acting as the slider itself.

![Slider with no min/max offset]
(https://res.cloudinary.com/munsch-creative/image/upload/v1607021251/slider-no-min_max-offset_zhyudw.gif)

Now we’ll have to establish logic to keep `<Movable />` within the bounds of its parent component. Since we set the width of the slider component as 85% of the viewport width, we can bet that is going to be the maximum value we can drag `<Movable />` to along the x-axis. To get access to the maximum offset we want, pass `maxOffset` as a prop to `<Movable />`; in this case we’ll pass `vw(85)`.

Before we move on, the value of `props.maxOffset` will need to take the width of `<Movable />` into account, otherwise our positioning will be off by the value of the component's radius. We'll store the radius value in a class property, `this.componentRadius`. Simply set the value to half of the component's width.

We'll also set another class property, `this.initialPosition`, the negation of `this.componentRadius`. We'll use this value in a bit. 

```javascript
constructor(props) {
	super(props);

	// Get radius from StyleSheet rule
	this.componentRadius = styles.movable.width * .5;
	// Set initial position to negative value of component's radius
	this.initialPosition = this.componentRadius * -1;
	// Set property maxOffset to prop value minus component's radius
	this.maxOffset = this.props.maxOffset - this.componentRadius;
  
  [...]
```

With the value of `props.maxOffset` in our Animated component, we'll have to incorporate the logic to actually limit movement beyond the bounds of the slider parent component. The logic for this is pretty straightforward, we need to get the `x` offset of the component while it's being moved, and if it's less than  `0` or is greater than `this.maxOffset`, we don't want to allow the "movement", that is, the `setValue()` called on the instance of `this.state.animate`.

The tricky part of implementing this is actually getting the `x` offset the user has dragged to. I calculated this by getting the sum of the change in the x offset and the most recent position of `<Movable />`. Let’s start by adding new value in state, `latestPosition`. This will keep track of the latest position of the component along the x-axis. We’ll need this value to determine if the component is still within bounds of its parent when it is being moved. We'll intialize it as the value of `this.intialPosition`, to account for the width of the component.

```javascript
// Initialize state
this.state = {
  // Create instance of Animated.XY, which interpolates X and Y values
  animate: new Animated.ValueXY(),
  latestPosition: this.initialPosition
};
```

We'll also need to update the `x` value that we pass in `this.state.animate.setValue()` for the same reason - we need to consider the width of the component skewing the positioning of `<Movable />` within the slider. We'll simply subtract the value of `this.componentRadius` from the existing value of `0`.

```javascript
// Initialize value of x and y coordinates
this.state.animate.setValue({ 
	// Subtract half of the width of the component to account for positioning
	x: 0 - this.componentRadius,
	y: 0, 
});
```

With our new state value and class properties in place, let's go back to `onPanResponderMove()` and update the logic to perform what was described above to prevent movement out of bounds:

```javascript
// The user is moving their finger
onPanResponderMove: (e, gesture) => {
  // Get the final value that user has dragged to.
  let finalOffset = gesture.dx + this.state.latestPosition;

  // If finalOffset is within bounds of the slider, update state.drag to appropriate position
  if (finalOffset >= 0 && finalOffset <= this.maxOffset) {
    this.state.animate.setValue({ x: gesture.dx, y: 0 });
  }
},
```

Now try saving, refreshing and moving the component around again. You'll see that the behavior is not there yet. We'll have to update the value of `this.state.latestPosition` at the end of the touch event. In order to do so, we'll have to update `onPanResponderRelease()`. 

At this point, your code should simply have a call to `this.state.animate.flattenOffset()` in `onPanResponderRelease()`. Since we're about to make the logic progressively more involved, I'd recommend moving the business logic into a handler function and passing it as a callback. Don't forget to update the handler in `PanResponder.create()` to execute our callback and pass the gesture object as an argument.

```javascript
// Fired at the end of the touch
onPanResponderRelease: (e, gesture) => {
	this.panResponderReleaseHandler(gesture)
}
      
[...]
      
/** 
 * Event handler for when panResponder touch event ends. 
 * @param {Object} gesture - The gestureState object passed as a param to each panResponder callback.
 * @return {null} Updates local state.
*/
panResponderReleaseHandler = (gesture) => {
  // Merges the offset value into the base value and resets the offset to     
  // zero
	this.state.animate.flattenOffset();	
}
```

At the top of the function we’ll declare the `finalOffset` variable the same way we did in `onPanResponderMove()`. We'll also keep the call to `flattenOffset()`.

```javascript
// Get the final x value that user has dragged to
let finalOffset = gesture.dx + this.state.latestPosition;

// Merges the offset value into the base value and resets the offset to
// zero
this.state.animate.flattenOffset();
```

Try `console.log()`-ing the value of `finalOffset` in the same handler function. If you refresh the simulator and drag the component roughly halfway to the other end of the slider, you should get a positive value (I got about `150` on the simulated iPhone 11 Pro Max with after passing `vw(85)` to `props.maxOffset` .

Before we go any further, we’re also going to need two more variables: one to eventually set as the `x` value in `this.state.animate.setValue()` , and a second to set as the updated value of `this.state.latestPosition`. Let's call these `updatedOffsetX` and `newPosition`.

```javascript
// Initialize value we'll use to update this.state.animate.x
let updatedOffsetX;
// Initialize value we'll use to update this.state.latestPosition
let newPosition;
```

With the variables we'll need declared, let's think about what we want to achieve with keeping the <Movable /> component within it's parent. We want to prevent the component from being dragged "out of bounds". To prevent this, we need to quantify what the numeric boundaries of the slider are. 

For the left/minimum side of the slider, the value would simply be `0`. If the value of `finalOffset` is less than or equal to `0`, we know that the user has reached the left edge of the slider. 

For the right/maximum side of the slider, we can use the value of `this.maxOffset` to determine if the user has dragged out of bounds. If the value of `finalOffset` is greater than or equal to `this.maxOffset`, we know we'll have to forcefully end the `PanResponder` event.

Taking these "boundary" values into consideration, we know the `finalOffset` of an "in bounds" drag would fall within the range of `0` and `this.maxOffset`, so a conditional would look like:

```javascript
// If drag is "in bounds"
if (finalOffset >= 0 && finalOffset <= this.maxOffset) {
  // Handle logic for an in bounds drag here
}
```

Naturally, we'd expand this logic with a simple `else` block to determine the logic we'd use for an "out of bounds" drag. We'll do that next:

```javascript
// If drag is "in bounds"
if (finalOffset >= 0 && finalOffset <= this.maxOffset) {
  // Handle logic for an in bounds drag here
}
// If drag is "out of bounds"
else {
  // Handle logic here
}
```

With this logic in place we're fundamentally designating two types of drags of our component: one that is within the bounds of the slider container and one that is out of bounds. 

Within each of these types of drags we have three more scenarios: 

- The user drags and it ends up to the left of its beginning position.
- The user drags the component and it ends up to the right of its beginning position.
- The user drags it and it ends up in the exact same position. 

Now let's determine the logic for each of these scenarios. We'll start with the user dragging to the left. We should be able to tell if the user has moved to the left if the value of `gesture.dx` is negative. If this is the case, we'll set `updatedOffsetX` to the negation of `this.state.latestPosition - newPosition`. This will give us the value of how far the user dragged to the left from the position of the component prior to it being moved.

```javascript
// If drag is in bounds
if (finalOffset >= 0 && finalOffset <= this.maxOffset) {
  // Set newPosition to that of finalOffset
  newPosition = finalOffset;

  // If moved to the left
  if (gesture.dx < 0) {
    // Set udatedOffsetX to negation of state.latestPosition - newPosition
    updatedOffsetX = (this.state.latestPosition - newPosition) * -1
  }
}

```

Now, below the handler's main `if/else` block:

- Pass `updatedOffsetX` to the `x` value in `this.state.animate.setValue()`
- Call `this.setState()`, updating the value of `this.state.latestPosition` to the the value of `newPosition`. 
- Move the call to `flattenOffset()` to the bottom of the function.

Your handler should now look something like this:

```javascript
panResponderReleaseHandler = (gesture) => {
    // Get the final value that user has dragged to.
    let finalOffset = gesture.dx + this.state.latestPosition;

    // Initialize value we'll use to update this.state.animate.x
    let updatedOffsetX;
    // Initialize value we'll use to update this.state.latestPosition
    let newPosition;

    // If drag is in bounds
    if (finalOffset >= 0 && finalOffset <= this.maxOffset) {
      // Set newPosition to that of finalOffset
      newPosition = finalOffset;

      // If moved to the left
      if (gesture.dx < 0) {
        // Set udatedOffsetX to negation of state.latestPosition - newPosition
        updatedOffsetX = (this.state.latestPosition - newPosition) * -1
      }
    }
    // If drag is "out of bounds"
    else {
      // Handle logic here
    }

    // Update x value of this.state.animate
    this.state.animate.setValue({ x: updatedOffsetX, y: 0 });

    // Update latestPosition
    this.setState({ latestPosition: newPosition });

    // Merges the offset value into the base value and resets the offset to zero
    this.state.animate.flattenOffset();
  }
```

With this current form of `panResponderReleaseHandler()`, we'll simply add more conditional statements to handle our other cases. 

The conditional for the component being moved to the right simply checks if `gesture.dx` is positive; if it is we'll set `updatedOffsetX` to the value of `newPosition - this.state.latestPosition`. This gives us the distance that the user has moved the component to the right from the starting point of the touch event. 

```java
// If moved to the left
if (gesture.dx < 0) {
  // Set udatedOffsetX to negation of state.latestPosition - newPosition
  updatedOffsetX = (this.state.latestPosition - newPosition) * -1
}
// If moved to the right
else if (gesture.dx > 0) {
  // Set updatedOffsetX to newPosition - this.state.latestPosition
  updatedOffsetX = newPosition - this.state.latestPosition;
}
```

Finally, we need to add an `else` block to handle the rare event that the user retuns to exactly the same spot along the slider. If that's the case we simply set `updatedOffsetX` to `0`.

```javascript
// If drag is in bounds
if (finalOffset >= 0 && finalOffset <= this.maxOffset) {
  // Set newPosition to that of finalOffset
  newPosition = finalOffset;

  // If moved to the left
  if (gesture.dx < 0) {
    // Set udatedOffsetX to negation of state.latestPosition - newPosition
    updatedOffsetX = (this.state.latestPosition - newPosition) * -1
  }
  // If moved to the right
  else if (gesture.dx > 0) {
    // Set updatedOffsetX to newPosition - this.state.latestPosition
    updatedOffsetX = newPosition - this.state.latestPosition;
  }
  // If user returns to original position prior to this panResponder touch
  else {
    // Set updatedOffsetX to 0
    updatedOffsetX = 0;
  }
}
```

Now, test your progress. You should have everything working as long as you keep `<Movable />` in bounds (you'll get an error if you drag out of bounds). If things look a little wacky after a couple of touch events, make sure you removed the call to `flattenOffset()` before the conditional logic in `panResponderReleaseHandler()`.

Now we're at the home stretch! The following logic handles out of bounds drags. Let's take it step-by-step.

In the `else` block of our function's main conditional, we'll take a similar approach to the one we did with "in bound" movements. The main difference you'll see here is that we have no `else if` logic because we'll have the same logic for drags to the right and back to the original position. 

In the first conditional, we'll target drags to the right boundary by checking if the value of `gesture.dx` is greater than `0`.

The logic here looks similar to how we handle drags to the right for in bound movements, but we set `newPosition` to `this.maxOffset` and `updatedOffsetX` to the difference of `this.maxOffset` and `this.state.latestPosition` instead of  `newPosition` and `this.state.latestPosition`. 

```javascript
// If drag is out of bounds
else {
  // If gesture.dx is positive
  if (gesture.dx > 0) {
    // Set newPosition to maxOffset
    newPosition = this.maxOffset;
    // Set value to update offset x with to maxOffset - latestPosition
    updatedOffsetX = this.maxOffset - this.state.latestPosition;
  }
  // If gesture.dx is the same or negative 
  else {

  }
}
```

Now we just need to handle drags to the left edge and back to the starting position of the touch event. In the `else` block, start by setting `newPosition` to `0`. Then we need to check if the user was already at `0`, if they were set the value `updatedOffsetX` to `0`, otherwise set it to the negation of `this.state.latestPosition`. 

```javascript
// If gesture.dx is the same or negative 
else {
  // Set newPosition to 0
  newPosition = 0;

  // If already at zero
  if (this.state.latestPosition <= 0) {
    // Set updatedOffsetX to 0
    updatedOffsetX = 0;
  }
  // Set value to update offset x with to negation of latestPosition
  else {
    updatedOffsetX = this.state.latestPosition * -1;
  }
}
```

Now take a look at how everything is working. If you've followed along correctly, you shouldn't be getting any more errors when dragging `<Movable />` out of bounds in either direction. There should just be one issue with the slider at this point: if you return to the left edge of the slider, it should look like the component doesn't _quite_ go to the edge of the slider like it does on the right edge. This is because we need to take the radius of the component into consideration like we did when we inititialized `this.initialPosition` in the `constructor`. 

![Slider with no component radius compensation]
(https://res.cloudinary.com/munsch-creative/image/upload/v1607021459/slider-no-radius-compensation_i8wxsv.gif)

We can compensate for the radius by subtracting `this.componentRadius` from the value of `updatedOffsetX` in the `else` statement _if the user isn't already at the left edge of the component_. Instead of using `this.state.latestPosition` to determine this, let's add a very explicit boolean value in local state, `atMinValue`. Initialize it as false in the `constructor`.

```javascript
// Initialize state
this.state = {
  // Create instance of Animated.XY, which interpolates X and Y values
  animate: new Animated.ValueXY(),
  latestPosition: this.initialPosition,
  atMinValue: false
};
```

Back in `else` block of the out of bounds conditional in `panResponderReleaseHandler()`,  we want to subtract the value of `this.componentRadius` from `updatedOffsetX` if we're not already at the minimum value. We'll also always set `this.state.atMinValue` to `true` at this point. 

```javascript
 // Set value to update offset x with to negative value of latestPosition
else {
  updatedOffsetX = (this.state.latestPosition * -1);

  // If not already atMinValue
  if (!this.state.atMinValue) {
    // Subtract component radius from updatedOffsetX
    updatedOffsetX -= this.componentRadius;
  }

  this.setState({ atMinValue: true });
}
```

With this solution in place, you should now be having the adverse issue with `<Movable />` not quite looking like it's all the way at the value of `this.maxOffset` on a drag all the way to the right. In the conditional for a positive move, we'll add the opposite logic to add the value of the component's radius to `updateOffsetX`.

```javascript
// If gesture.dx is positive
if (gesture.dx > 0) {
  // Set newPosition to maxOffset
  newPosition = this.maxOffset;
  // Set value to update offset x with to maxOffset - latestPosition
  updatedOffsetX = this.maxOffset - this.state.latestPosition;

  // If coming from minValue/0
  if (this.state.atMinValue) {
    // Add component radius to updatedOffsetX
    updatedOffsetX += this.componentRadius;
    // Update state.atMinValue
    this.setState({ atMinValue: false });
  }
}
```

![Finished Slider](https://res.cloudinary.com/munsch-creative/image/upload/v1607021571/slider-finished-product_xzksd9.gif)

With this logic in place, our slider should be finished. Of course you'll likely want to add some additional logic to "snap" to a pressed location, to set a minimum and maximum value with new props, or to tailor the `PanResponder` handlers to your own unique needs, but this should give you a good foundation to build off of to fit your needs.

Thanks for reading and for following along! :) Happy coding.