PanResponder snaps Animated.View back to original

2020-08-22 17:04发布

问题:

I have created a PanResponder to move a Animated.View vertically. When I move it from it's original position it works fine, but once I go to move it for a second time it snaps back to its original position and then moves relative to the touch.

I am unpacking the responder straight into the Animated.View, could this be causing this behaviour somehow?

Here is how I define my PanResponder:

this.state = {                            
  drag: new Animated.ValueXY()            
}                                         

this._responder = PanResponder.create({                                              
    onStartShouldSetPanResponder: () => true,                             
    onPanResponderMove: Animated.event([null, {                           
      dy: this.state.drag.y                                             
    }]),                                                                  
    onPanResponderRelease: (e, coords) => {
      ...
    }     
})

And applying the responder to my Animated.View:

<Animated.View {...this._responder.panHandlers} style={this.state.drag.getLayout()}>
  // children go here
</Animated.View>                 

Thanks

回答1:

First, let's look at why this is happening:

  • Your onPanResponderMove callback reads the gesture's dy (delta Y), which gives you the amount of pixels moved vertically since the beginning of the gesture. This means that every time you start a new gesture, the delta starts from 0.

  • AnimatedXY#getLayout() on the other hand simply maps the y value to style property top. This means that when y is set to 0 in the beginning of the touch, the element will bounce back to its initial, non-offset position.

In order to preserve the offset from the previous drag, you can use setOffset to preserve the previous offset position, and then setValue to reset the initial delta to 0. This can be done when the gesture starts, e.g. on onPanResponderGrant:

this._responder = PanResponder.create({
  onStartShouldSetPanResponder: () => true,
  onPanResponderGrant: (evt, gestureState) => {
    this.state.drag.setOffset(this.state.drag.__getValue());
    this.state.drag.setValue({ x: 0, y: 0 });
  },
  onPanResponderMove: Animated.event([
    null,
    { dy: this.state. drag.y }
  ])
});

As you can see, we are using a "private" method __getValue() here. Generally, syncronously reading the value of an Animated.value is not recommended, because the animation may be offloaded onto the native thread and the value may not be up to date. Here, at the beginning of the gesture, it should be safe though.

As a side note, since you are only moving the element on the Y axis, you don't necessarily need to use the two-dimensional Animated.ValueXY, since a basic Animated.Value will suffice. If you refactor your code to use Value, you can just call drag.extractOffset() to reset the offset. It does the same thing, but does not appear to be available on AnimatedXY.



回答2:

You just need to adjust the offset in onPanResponderGrant, like

   onPanResponderGrant: (e, gestureState) => {
          this.state.drag.setOffset({x: this.state.drag.x._value, y: this.state.drag.y._value});
          this.state.drag.setValue({x: 0, y: 0})

      },

But also make sure you flatten the offset when the panResponder is released, or else it will have some glitch (the position will be reset according to previous offset) when you drag for the 3rd or 4th time.

  onPanResponderRelease: (e, {vx, vy}) => {
        this.state.drag.flattenOffset()      
   }