chenglou / react-motion

A spring that solves your animation problems.
MIT License
21.68k stars 1.16k forks source link

Ordered list animation #263

Open lucasfeliciano opened 8 years ago

lucasfeliciano commented 8 years ago

I'm struggling in a problem for a couple of days now.

I'm using redux with react-native and in my store I have a ordered array which in the UI is a list of cards.

Imagine the same scenario as this example: http://chenglou.me/react-motion/demos/demo8-draggable-list/

But the user can choose by what he is ordering this list, so when the user change the sorting order I dispatch an action to my store which will return the new state with the ordered array and my view should animate the cards to the new order to reflect the state.

How do you guys are approaching this scenario?

threepointone commented 8 years ago

can you explain the usecase in a little more detail? also, is ordering via drag and drop (as in the example), or do you have a flag that chooses the sorting order? (like todomvc's 'done', 'all', etc).

lucasfeliciano commented 8 years ago

@threepointone that is the point. It is not drag and drop.

I have a list of offers sorted by price, so I dispatch an action to sort this offers by shipping days so my store receive the new sorted array then I need to animate the old list to reflect the new order

threepointone commented 8 years ago

here's a quick example of redux+react-motion that shows a list of characters, and animates on clicking 'shuffle'/'sort'. I used a single <Motion/> per item for simplicity/ this usecase, though you might want to consider <TransitionMotion/> if you need unmounting support.

import React, {Component} from 'react';
import {render} from 'react-dom';
import {createStore, combineReducers} from 'redux';
import {connect, Provider} from 'react-redux';

import {Motion, spring} from 'react-motion';

// a helper to shuffle the array
function shuffle (src){
  var o = [...src];
  for(var j, x, i = o.length; i; j = parseInt(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x);
  return o;
};

// redux store
const store = createStore(combineReducers({
  app (state = ['a', 'b', 'c', 'd', 'e', 'f'], action){
    switch(action.type){
      case 'sort':
        return [...state].sort();
      case 'shuffle':
        return shuffle(state);
      default:
        return state;
    }
  }
}));

const App = connect(state => state)(class extends Component{
  render(){
    let {dispatch, app} = this.props;
    return <div>
      <button onClick={() => dispatch({type: 'shuffle'})}>shuffle</button>
      <button onClick={() => dispatch({type: 'sort'})}>sort</button>
      {app.map((el, i) =>
        <Motion key={el} style={{top: spring(i*40)}}>{val => // array index correlates to 'top'
          <div style={{...val, position: 'absolute'}}>{el}</div>
        }</Motion> )}
    </div>
  }
})

render(<Provider store={store}><App/></Provider>, document.getElementById('app'));
lucasfeliciano commented 8 years ago

That's a nice snippet.

But I still have to tackle one problem, since react-native do not have a zIndex parameter, the layers depend on the render order.

So when I'm sorting my list, the itens which are going to the top must be in front of the others. ( am i clear ? )

Lets suppose: I have this array

[2,3,4,5,1]

and then I sorted it, so in my animation the component 1 must go to the top in front of the other ones instead of behind it.

The solution that @threepointone suggested is working but I just need to find a hack for the zIndex thing.

threepointone commented 8 years ago

hmm well, in the above example, we use array index to specify both top and render tree precedence; you'd instead use some other key you'd store on the array element to associate with top (you could create these keys this in the reducer maybe?), and then in render(), sort these children to whatever order you'd prefer (with the 'new' elements later in the render tree, etc).

Freddy03h commented 8 years ago

The point is to render a new dom tree for having the final view of the animation. And then, animate effectively with transform. It's called the FLIP technique : https://aerotwist.com/blog/flip-your-animations/ It's useful when you want to do a layout animation, by ui design, but you can't because of poor performance. So instead of doing a browser layout for each frame of the animation, the technique is to only do the layout once, and visually recreate the old view using transform, and then animate the transform to the new view, that was actually already rendered. @joshwcomeau wrote an article with React on the subject : https://medium.com/developers-writing/animating-the-unanimatable-1346a5aab3cd

TransitionMotion is a good candidate to integrate those kinds of behavior. It already had dom change, and then animation append with willEnter and willLeave. It's browser layout, then animation. It's already a FLIP technique, by the way, if you only use transform for the animation. I think the thing that missing is to animate the other items when rendering an new item. It also can be useful to had style to those items without animation and before the animation (for visually recreate old view).

So, when adding an new item on the list, the new dom trigger a browser layout, the willEnter create the animation for the new item, and the 'missing part' create animation for others items. For example, new item is animated using transform scale and items after the new on the list, will be animated to translate to their new position. I think that created that missing part also resolve the principe of ordering a list based on their new location on the dom.