Zach
Zach

Reputation: 620

Panresponder + interpolate

I have a Component Parent which has an Animated.value "this.progress" which takes values in [1, 3].

The Parent renders a Component Child and pass it the progress as a props:

// File Parent.js
import React from "react";
import { Animated } from "react-native";
import Child from "./Child.js"

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.progress = new Animated.Value(1)
  }

  render() {
    return (
      <Child
        progress = {this.progress}
      />
    );
  }
}

The Child position depends on the progress:

If an animation changes the value of the progress, the Child must move accoringly (e.g., if progress is animated from 1 to 2, the user must see the Child move from [1, 3] to [2, 2] in a smooth way).

To that end I use, two interpolations:

// In the constructor of Child.js
this.interpolateX = this.props.progress.interpolate({
  inputRange: [1, 2, 3],
  outputRange: [1, 2, 1],
});

this.interpolateY = this.props.progress.interpolate({
  inputRange: [1, 2, 3],
  outputRange: [3, 2, 1],
});

and I use them to fix the position of the Child using "transform":

// Render method of Child.js
render() {
return (
  <Animated.View
    style={[
      {
        transform: [
          {
            translateX: this.interpolateX,
          },
          {
            translateY: this.interpolateY,
          },
        ],
      },
      { position: 'absolute' },
      { left: 0 },
      { top: 0 },
    ]}
  />
);
}

It works well, but now I also want to make the Child movable using the user finger. To that end I defined a panResponder. The problem is that the panResponder also uses a transform. I now have 2 transforms:

  1. one used to interpolate the Child's position from this.props.progress ;
  2. one used by the panResponder to move the Child with the user finger.

I don't know how to combine these two transform. Here is what I tried:

// File Child.js
import React from "react";
import { Animated, PanResponder } from "react-native";

class Child extends React.Component {
  constructor(props) {
    super(props);

    this.interpolateX = this.props.progress.interpolate({
      inputRange: [1, 2, 3],
      outputRange: [1, 2, 1],
    });

    this.interpolateY = this.props.progress.interpolate({
      inputRange: [1, 2, 3],
      outputRange: [3, 2, 1],
    });

    this.offset = new Animated.ValueXY({ x: 0, y: 0 });

    this._val = { x: 0, y: 0 };

    this.offset.addListener((value) => (this._val = value));

    // Initiate the panResponder
    this.panResponder = PanResponder.create({
      // Ask to be the responder
      onStartShouldSetPanResponder: () => true,

      // Called when the gesture starts
      onPanResponderGrant: () => {
        this.offset.setOffset({
          x: this._val.x,
          y: this._val.y,
        });

        this.offset.setValue({ x: 0, y: 0 });
      },

      // Called when a move is made
      onPanResponderMove: Animated.event([
        null,
        { dx: this.offset.x, dy: this.offset.y },
      ]),

      onPanResponderRelease: (evt, gesturestate) => {
        console.log(
          "released with offsetX: " +
            this.offset.x._value +
            "/" +
            this.offset.y._value
        );
      },
    });
  }

  render() {
    const panStyle = {
      transform: this.offset.getTranslateTransform(),
    };

    return (
      <Animated.View
        {...this.panResponder.panHandlers}
        style={[
          panStyle,
          {
            transform: [
              {
                translateX: this.interpolateX,
              },
              {
                translateY: this.interpolateY,
              },
            ],
          },
          { position: "absolute" },
          { left: 0 },
          { top: 0 },
        ]}
      />
    );
  }
}

export default Child;

It does not work. panStyle seems to be ignored. I guess you cannot have two transforms together.

I also tried to "add" the two expressions in the transforms without success (maybe its feasible).

My last idea was to have a boolean "isMoving" equal to true when the user moves the Child. If isMoving is false, I use the first transform, otherwise, I use the second one. It seems promising but I did not succeed in making it work.

Could you tell me how I could do that?

Upvotes: 0

Views: 1097

Answers (1)

Zach
Zach

Reputation: 620

A boolean variable isMoving which indicates which transform to use was the good idea. The trick was:

  • to put isMoving in the state;
  • to set the offset of my offset variable to the current value of the interpolate.

Here is the result:

// File Child.js
import React from "react";
import { Animated, PanResponder } from "react-native";

class Child extends React.Component {
  constructor(props) {
    super(props);

    this.interpolateX = this.props.progress.interpolate({
      inputRange: [1, 2, 3],
      outputRange: [1, 2, 1],
    });

    this.interpolateY = this.props.progress.interpolate({
      inputRange: [1, 2, 3],
      outputRange: [3, 2, 1],
    });

    this.offset = new Animated.ValueXY({ x: 0, y: 0 });

    this.state = {
      isMoving: false,
    };

    // Initiate the panResponder
    this.panResponder = PanResponder.create({
      // Ask to be the responder
      onStartShouldSetPanResponder: () => true,

      // Called when the gesture starts
      onPanResponderGrant: () => {
        this.setState({
          isMoving: true,
        });

        this.offset.setOffset({
          x: this.interpolateX.__getValue(),
          y: this.interpolateY.__getValue(),
        });

        this.offset.setValue({ x: 0, y: 0 });
      },

      // Called when a move is made
      onPanResponderMove: Animated.event([
        null,
        { dx: this.offset.x, dy: this.offset.y },
      ]),

      onPanResponderRelease: (evt, gesturestate) => {
        this.setState({
          isMoving: false,
        });
      },
    });
  }

  render() {
    const panStyle = {
      transform: this.offset.getTranslateTransform(),
    };

    if (this.state.isMoving)
      return (
        <Animated.View
          {...this.panResponder.panHandlers}
          style={[panStyle, { position: "absolute" }, { left: 0 }, { top: 0 }]}
        />
      );
    else
      return (
        <Animated.View
          {...this.panResponder.panHandlers}
          style={[
            {
              transform: [
                {
                  translateX: this.interpolateX,
                },
                {
                  translateY: this.interpolateY,
                },
              ],
            },
            { position: "absolute" },
            { left: 0 },
            { top: 0 },
          ]}
        />
      );
  }
}

export default Child;

Upvotes: 1

Related Questions