[Animated] Question: Triggering a separate animation after a threshold #2072

Closed
dubert opened this Issue Jul 21, 2015 · 23 comments

Comments

Projects
None yet
6 participants
@dubert
Contributor

dubert commented Jul 21, 2015

I posted a few times in the IRC, and I can't seem to find an answer. I've watched @sahrens Fluid UX video a bunch of times, poured over the new Animation API, looked through @brentvatne 's tinder demo and I can't figure out how to do this particular animation.

I want to create something very similar to this: http://cl.ly/c0H3

Basically, as you slide the row past a certain threshold, it triggers a new timed animation that isn't based off of x in ValueXY.

At first I tried I tried interpolating the colors, but that's based off the X value, so the transition can be too fast or slow based on how quickly the user slides the row. I also tried adding a listener to the panning gesture, and then trying to trigger an animation after it passes the threshold, but I can't animate colors, I can only interpolate colors.

Any help or ideas?

@sahrens

This comment has been minimized.

Show comment
Hide comment
@sahrens

sahrens Jul 21, 2015

Contributor

Should be doable I think, and it sounds like you're going in the right direction.

Adding a listener to the pan x is the right starting point. Then in that listener, if the value crosses your threshold, start the color animation.

The trick is actually animating the colors. You'll want a dedicated Animated.Value just for color, and it will be kind of like an enum. Then interpolate it like so:

var BLACK = 0;
var RED = 1;
var BLUE = 2;

backgroundColor: this.state.color.interpolate({
  inputRange: [BLACK, RED, BLUE],
  outputRange: ['rgb(0, 0, 0)', 'rgb(255, 0, 0)', 'rgb(0, 0, 255)']
})

Then you can do

Animated.timing(this.state.color, {toValue: RED}).start();

This has its limitations of course - if you want to be able to go straight from RED to BLUE and BLACK to BLUE without going through RED, it's going to get more complicated, but I think you can get it to work by changing the outputRange of the interpolation function via setState in your listener. If you need that and can't figure it out, let me know.

Contributor

sahrens commented Jul 21, 2015

Should be doable I think, and it sounds like you're going in the right direction.

Adding a listener to the pan x is the right starting point. Then in that listener, if the value crosses your threshold, start the color animation.

The trick is actually animating the colors. You'll want a dedicated Animated.Value just for color, and it will be kind of like an enum. Then interpolate it like so:

var BLACK = 0;
var RED = 1;
var BLUE = 2;

backgroundColor: this.state.color.interpolate({
  inputRange: [BLACK, RED, BLUE],
  outputRange: ['rgb(0, 0, 0)', 'rgb(255, 0, 0)', 'rgb(0, 0, 255)']
})

Then you can do

Animated.timing(this.state.color, {toValue: RED}).start();

This has its limitations of course - if you want to be able to go straight from RED to BLUE and BLACK to BLUE without going through RED, it's going to get more complicated, but I think you can get it to work by changing the outputRange of the interpolation function via setState in your listener. If you need that and can't figure it out, let me know.

@sahrens

This comment has been minimized.

Show comment
Hide comment
@sahrens

sahrens Jul 21, 2015

Contributor

Apologies for formatting and any bugs - I'm on my phone.

Contributor

sahrens commented Jul 21, 2015

Apologies for formatting and any bugs - I'm on my phone.

@brentvatne

This comment has been minimized.

Show comment
Hide comment
@brentvatne

brentvatne Jul 21, 2015

Collaborator

Very cool example! I think @sahrens' suggestion will work well! I cleaned up the formatting for him on the code because I'm on my computer :)

Collaborator

brentvatne commented Jul 21, 2015

Very cool example! I think @sahrens' suggestion will work well! I cleaned up the formatting for him on the code because I'm on my computer :)

@sahrens

This comment has been minimized.

Show comment
Hide comment
@sahrens

sahrens Jul 21, 2015

Contributor

Wow, didn't realize you could edit other people comments. Thanks, Brent!

Contributor

sahrens commented Jul 21, 2015

Wow, didn't realize you could edit other people comments. Thanks, Brent!

@sahrens

This comment has been minimized.

Show comment
Hide comment
@sahrens

sahrens Jul 21, 2015

Contributor

Oh, and please post what you come up with - it is a cool interaction indeed.

Contributor

sahrens commented Jul 21, 2015

Oh, and please post what you come up with - it is a cool interaction indeed.

@brentvatne

This comment has been minimized.

Show comment
Hide comment
@brentvatne

brentvatne Jul 22, 2015

Collaborator

@dubert - any luck with this? 😄

Collaborator

brentvatne commented Jul 22, 2015

@dubert - any luck with this? 😄

@browniefed

This comment has been minimized.

Show comment
Hide comment
@browniefed

browniefed Jul 22, 2015

Contributor

Hmm, I figured I'd attempt it (I don't quite fully grasp Animated and PanRespnder so take it easy on me), except PanResponderMove wasn't firing and causing the color interpolation to happen consistently.
Code: https://rnplay.org/apps/32dWag/
http://i.imgur.com/cqxo4qq.gif

I dunno

Contributor

browniefed commented Jul 22, 2015

Hmm, I figured I'd attempt it (I don't quite fully grasp Animated and PanRespnder so take it easy on me), except PanResponderMove wasn't firing and causing the color interpolation to happen consistently.
Code: https://rnplay.org/apps/32dWag/
http://i.imgur.com/cqxo4qq.gif

I dunno

@dubert

This comment has been minimized.

Show comment
Hide comment
@dubert

dubert Jul 22, 2015

Contributor

Woohoo! I've got it working using @sahrens suggestion! Thank you very much.

done

At the top:

var GREY = 0
var GREEN = 1
var RED = 2

var Triggered = {
  GREY: false,
  GREEN: false,
  RED: false,
}

The listener function:

_animateRow(value) {

  var threshold = deviceScreen.width / 2

  if (value.x < 0) {

    if (value.x > -50 && !Triggered.GREY) {
      Triggered.GREY = true
      Triggered.GREEN = false
      Triggered.RED = false

      Animated.timing(this.state.color, {
        toValue: GREY,
        duration: 180,
      }).start()
    }
    if (value.x < -50 && value.x > -threshold && !Triggered.GREEN) {
      Triggered.GREY = false
      Triggered.GREEN = true
      Triggered.RED = false

      Animated.timing(this.state.color, {
        toValue: GREEN,
        duration: 180,
      }).start()
    }
    if (value.x < -threshold && !Triggered.RED) {
      Triggered.GREY = false
      Triggered.GREEN = false
      Triggered.RED = true

      Animated.timing(this.state.color, {
        toValue: RED,
        duration: 180,
      }).start()
    }
  }
}

In render:

var bgColor = this.state.color.interpolate({
  inputRange: [
    GREY,
    BLUE,
    PURPLE,
  ],
  outputRange: [
    'rgb(180, 180, 180)',
    'rgb(63, 236, 35)',
    'rgb(233, 19, 19)',
  ],
})

I'm using Triggered to limit the animation to only executing once. (Because it was on the listener, it was being repeatedly called.) Please let me know if there is a better way to do this.

Contributor

dubert commented Jul 22, 2015

Woohoo! I've got it working using @sahrens suggestion! Thank you very much.

done

At the top:

var GREY = 0
var GREEN = 1
var RED = 2

var Triggered = {
  GREY: false,
  GREEN: false,
  RED: false,
}

The listener function:

_animateRow(value) {

  var threshold = deviceScreen.width / 2

  if (value.x < 0) {

    if (value.x > -50 && !Triggered.GREY) {
      Triggered.GREY = true
      Triggered.GREEN = false
      Triggered.RED = false

      Animated.timing(this.state.color, {
        toValue: GREY,
        duration: 180,
      }).start()
    }
    if (value.x < -50 && value.x > -threshold && !Triggered.GREEN) {
      Triggered.GREY = false
      Triggered.GREEN = true
      Triggered.RED = false

      Animated.timing(this.state.color, {
        toValue: GREEN,
        duration: 180,
      }).start()
    }
    if (value.x < -threshold && !Triggered.RED) {
      Triggered.GREY = false
      Triggered.GREEN = false
      Triggered.RED = true

      Animated.timing(this.state.color, {
        toValue: RED,
        duration: 180,
      }).start()
    }
  }
}

In render:

var bgColor = this.state.color.interpolate({
  inputRange: [
    GREY,
    BLUE,
    PURPLE,
  ],
  outputRange: [
    'rgb(180, 180, 180)',
    'rgb(63, 236, 35)',
    'rgb(233, 19, 19)',
  ],
})

I'm using Triggered to limit the animation to only executing once. (Because it was on the listener, it was being repeatedly called.) Please let me know if there is a better way to do this.

@browniefed

This comment has been minimized.

Show comment
Hide comment
@browniefed

browniefed Jul 22, 2015

Contributor

@dubert that's hot! Nice work, I am definitely stealing this concept.

Contributor

browniefed commented Jul 22, 2015

@dubert that's hot! Nice work, I am definitely stealing this concept.

@sahrens

This comment has been minimized.

Show comment
Hide comment
@sahrens

sahrens Jul 22, 2015

Contributor

That's great! It's mostly taste, but the only change I would make personally would be to have a single target value and set that to GREY, GREEN, or RED, rather than the Triggered Object, and just have a single chunk of code to run the animation, something like this:

var target = null;
if (value.x > -50 && this._target != GREY) {
  target = GREY;
} else if (value.x < -50 && value.x > -threshold && this._target != GREEN) {
  target = GREEN;
} else if (value.x < -threshold && this._target != RED) {
  target = RED;
}
if (target !== null) {
  this._target = target:
  Animated.timing(this.state.color, {
    toValue: target,
    duration: 180,
  }).start();
}
Contributor

sahrens commented Jul 22, 2015

That's great! It's mostly taste, but the only change I would make personally would be to have a single target value and set that to GREY, GREEN, or RED, rather than the Triggered Object, and just have a single chunk of code to run the animation, something like this:

var target = null;
if (value.x > -50 && this._target != GREY) {
  target = GREY;
} else if (value.x < -50 && value.x > -threshold && this._target != GREEN) {
  target = GREEN;
} else if (value.x < -threshold && this._target != RED) {
  target = RED;
}
if (target !== null) {
  this._target = target:
  Animated.timing(this.state.color, {
    toValue: target,
    duration: 180,
  }).start();
}
@sahrens

This comment has been minimized.

Show comment
Hide comment
@sahrens

sahrens Jul 22, 2015

Contributor

I'd also pick prettier colors ;)

Closing since you got it working :)

Contributor

sahrens commented Jul 22, 2015

I'd also pick prettier colors ;)

Closing since you got it working :)

@sahrens sahrens closed this Jul 22, 2015

@sahrens

This comment has been minimized.

Show comment
Hide comment
@sahrens

sahrens Jul 22, 2015

Contributor

One other thought - you might want to do this with a horizontal scroll view rather than a pan responder on a translated view of you're not already. That way I think the nested scroll views will negotiate their responder locks correctly. Let me know if you have issues with that.

Contributor

sahrens commented Jul 22, 2015

One other thought - you might want to do this with a horizontal scroll view rather than a pan responder on a translated view of you're not already. That way I think the nested scroll views will negotiate their responder locks correctly. Let me know if you have issues with that.

@dubert

This comment has been minimized.

Show comment
Hide comment
@dubert

dubert Jul 22, 2015

Contributor

That's really smart @sahrens , thanks. I will definitely use the object to reduce the code.

And you anticipated my next issue! I was already thinking about a directional measurement on the Pan Responder to disabled vertical scrolling but that's much much better.

And the colors were placeholders :) I'll pick good ones

Contributor

dubert commented Jul 22, 2015

That's really smart @sahrens , thanks. I will definitely use the object to reduce the code.

And you anticipated my next issue! I was already thinking about a directional measurement on the Pan Responder to disabled vertical scrolling but that's much much better.

And the colors were placeholders :) I'll pick good ones

@browniefed

This comment has been minimized.

Show comment
Hide comment
@browniefed

browniefed Jul 22, 2015

Contributor

@dubert when you get that figured out can you post the code snippet? I'd be interested in seeing how that all works together.

Contributor

browniefed commented Jul 22, 2015

@dubert when you get that figured out can you post the code snippet? I'd be interested in seeing how that all works together.

@dubert

This comment has been minimized.

Show comment
Hide comment
@dubert

dubert Jul 22, 2015

Contributor

Will do

Contributor

dubert commented Jul 22, 2015

Will do

@browniefed

This comment has been minimized.

Show comment
Hide comment
@browniefed

browniefed Jul 27, 2015

Contributor

For anyone that stumbles upon this I finally got this all figured out.
Demo/Code here : https://rnplay.org/apps/LDI42g

My only issue, is that the

  1. With the horizontal ScrollView you have to define the height (at least from what I could figure out).
  2. The View in the horizontal ScrollView does not stretch completely regardless of what flex properties you add on it.
  3. The PanResponder vs ContentOffset X values are opposites, so you have to multiply by -1.
  4. The content offset is not as large as the PanResponder so I had to lower the RED threshold.

http://i.imgur.com/q9ZbOEh.png

I know this is more suited for StackOverflow however do you happen to have any insight on the matter of the size, I know I have the dimensions of the device, just want to confirm that I'm not running into a layout bug that needs to be fixed

Contributor

browniefed commented Jul 27, 2015

For anyone that stumbles upon this I finally got this all figured out.
Demo/Code here : https://rnplay.org/apps/LDI42g

My only issue, is that the

  1. With the horizontal ScrollView you have to define the height (at least from what I could figure out).
  2. The View in the horizontal ScrollView does not stretch completely regardless of what flex properties you add on it.
  3. The PanResponder vs ContentOffset X values are opposites, so you have to multiply by -1.
  4. The content offset is not as large as the PanResponder so I had to lower the RED threshold.

http://i.imgur.com/q9ZbOEh.png

I know this is more suited for StackOverflow however do you happen to have any insight on the matter of the size, I know I have the dimensions of the device, just want to confirm that I'm not running into a layout bug that needs to be fixed

@nicholasstephan

This comment has been minimized.

Show comment
Hide comment
@nicholasstephan

nicholasstephan Sep 16, 2015

@sahrens You say: if you want to be able to go straight from RED to BLUE and BLACK to BLUE without going through RED, it's going to get more complicated. That's exactly what I need, complicated or not. Any chance you can point me in the right direction? Thanks.

@sahrens You say: if you want to be able to go straight from RED to BLUE and BLACK to BLUE without going through RED, it's going to get more complicated. That's exactly what I need, complicated or not. Any chance you can point me in the right direction? Thanks.

@dubert

This comment has been minimized.

Show comment
Hide comment
@dubert

dubert Sep 17, 2015

Contributor

@nicholasstephan do you need to do that on the row swipe? What Animated value are you tracking? You might need to set up separate animations.

Contributor

dubert commented Sep 17, 2015

@nicholasstephan do you need to do that on the row swipe? What Animated value are you tracking? You might need to set up separate animations.

@jorelllinsangan

This comment has been minimized.

Show comment
Hide comment
@jorelllinsangan

jorelllinsangan Nov 22, 2016

@dubert I know this is a an old thread but any chance you can share how you made it look like you're pulling in the colors from the side as opposed to @browniefed's way that changes the actual row background color?

@dubert I know this is a an old thread but any chance you can share how you made it look like you're pulling in the colors from the side as opposed to @browniefed's way that changes the actual row background color?

@browniefed

This comment has been minimized.

Show comment
Hide comment
@browniefed

browniefed Nov 22, 2016

Contributor

@jorelllinsangan You would need to have the View that has your content with a solid background color and translateX the content view, then the animation of the color is on a separate wrapping View. That's likely your best option.

Contributor

browniefed commented Nov 22, 2016

@jorelllinsangan You would need to have the View that has your content with a solid background color and translateX the content view, then the animation of the color is on a separate wrapping View. That's likely your best option.

@jorelllinsangan

This comment has been minimized.

Show comment
Hide comment
@jorelllinsangan

jorelllinsangan Nov 23, 2016

I ended up doing it where I have 3 views wrapped in another view with {flexDirection: "row"}. I did a translateX on the parent view to render the center first. Then made it spring back onPanResponderRelease.

My next issue now is what @dubert was saying above. I need to figure out how to lock the ScrollView when dragging my view.

I ended up doing it where I have 3 views wrapped in another view with {flexDirection: "row"}. I did a translateX on the parent view to render the center first. Then made it spring back onPanResponderRelease.

My next issue now is what @dubert was saying above. I need to figure out how to lock the ScrollView when dragging my view.

@browniefed

This comment has been minimized.

Show comment
Hide comment
@browniefed

browniefed Nov 23, 2016

Contributor

@jorelllinsangan this is why using a Horizontal scrollview per row will help w/ auto locking.
If you don't feel like doing that you can store something like scrollable on state, and when you have a PanResponder trigger in for a row swipe you can change the ScrollView scrollable prop to false and it should work.

I wrote up a very old blog post about this, wouldn't use any of that code but you can see it in action http://browniefed.com/blog/react-native-animated-listview-row-swipe/

Also check out https://github.com/jemise111/react-native-swipe-list-view

Contributor

browniefed commented Nov 23, 2016

@jorelllinsangan this is why using a Horizontal scrollview per row will help w/ auto locking.
If you don't feel like doing that you can store something like scrollable on state, and when you have a PanResponder trigger in for a row swipe you can change the ScrollView scrollable prop to false and it should work.

I wrote up a very old blog post about this, wouldn't use any of that code but you can see it in action http://browniefed.com/blog/react-native-animated-listview-row-swipe/

Also check out https://github.com/jemise111/react-native-swipe-list-view

@jorelllinsangan

This comment has been minimized.

Show comment
Hide comment
@jorelllinsangan

jorelllinsangan Nov 25, 2016

@browniefed: I actually originally followed your old blogpost to get a feel for Animations. That's why I ended up asking above how to do it like how dubert did his.

I've tried doing the setting it to false but I'm not sure how to distinguish between a horizontal row swipe and a vertical swipe to scroll. I'm not sure how onPanResponderMove works. Currently, I just have Animated.event([null, {dx: this.pan.x}]) assigned to it. I was thinking of trying to detect using gesture properties in onPanResponderMove by assigning it a function, doing swipe categorizing, then doing the Animated.event(...) but that makes it so that I can't drag my view anymore.

I've used jemise111's library but it just doesn't handle row swipes the way I need it to work. I need it to work similarly to what dubert originally posted when he opened this thread.

I also noticed that when Im dragging horizontally then I accidentally scroll vertically, my animation stops calling panResponderHandlers and it just freezes. I saw your blog post about setting scrollable to false in panResponderGrant. The problem with that is that as soon as I touch the row, it will prevent me from scrolling vertically. Apparently its a known issue: #1046

jorelllinsangan commented Nov 25, 2016

@browniefed: I actually originally followed your old blogpost to get a feel for Animations. That's why I ended up asking above how to do it like how dubert did his.

I've tried doing the setting it to false but I'm not sure how to distinguish between a horizontal row swipe and a vertical swipe to scroll. I'm not sure how onPanResponderMove works. Currently, I just have Animated.event([null, {dx: this.pan.x}]) assigned to it. I was thinking of trying to detect using gesture properties in onPanResponderMove by assigning it a function, doing swipe categorizing, then doing the Animated.event(...) but that makes it so that I can't drag my view anymore.

I've used jemise111's library but it just doesn't handle row swipes the way I need it to work. I need it to work similarly to what dubert originally posted when he opened this thread.

I also noticed that when Im dragging horizontally then I accidentally scroll vertically, my animation stops calling panResponderHandlers and it just freezes. I saw your blog post about setting scrollable to false in panResponderGrant. The problem with that is that as soon as I touch the row, it will prevent me from scrolling vertically. Apparently its a known issue: #1046

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment