Animating react-native — take 2

Back to first principles

I described my first attempt at animating a react native app in this blog post. To make sense of this post you’ll need to refer back to that one. There I used the react-native Animated API, packaging it up for use in a Rum mixin. It’s fine. It worked. And it was easy to do when animating just one value–in my case the height of a bar in a bar chart:

Animating a bar height change

But things suddenly became difficult when I wanted to add a label. I should be able to animate the text in the label too.

Animating a labelled bar height change

The Animated API now made things needlessly complex:

  • Components no longer composed cleanly. I have to remember which of them needs to be animated — when to use ‘view’ and when to use ‘animated-view’. When to use ‘text’ and when ‘animated-text’.
  • Passing animated values from parent to subcomponent is not straight forward.
  • And it wasn’t buying me any ‘native’ goodness — the tweening functions were still operating inside the javascript world.

So I started thinking that maybe I should avoid the Animated API, and instead go back to basic reactive principles. Why not animate the application state values, and let Rum and the rum/reactive mixin take care of the rest?

Animating values

When the height changes in our application state, instead of setting it with (swap! state assoc :height new-value) I’ll animate the change with (animate-to-new-value! state easeOutQuad 15 300 :height new-value) instead.

easeOutQuad is a simple function returning a quadratically eased progress value between 0 and 1, when given the time elapsed from the start of the animation, and the total animation duration.

(defn easeOutQuad
[elapsed-t duration]
(let [dt (/ elapsed-t duration)]
(* dt (- 2 dt))))

And here’s what animate-to-new-value!looks like. It will probably get prettier, but it’s really not too bad. It’s fairly easy to see how to extend this function if I need things like chained or staggered animations in the future.

(defn animate-to-new-value! [state easing interval duration 
key new-value]
(let [anim-key (keyword (str (name key) "-anim"))
initial-value (key @state)]
(letfn [(tick [_]
(let [a-map (anim-key @state)
t (- (.now js/Date) (::t0 a-map))]
(if (< t duration)
(let [progress (easing t duration)
new-val (+ (::initial-value a-map)
(* progress (::delta a-map)))]
(swap! state assoc key new-val))
(do
(js/clearInterval (::ticker a-map))
(swap! state dissoc anim-key)
(swap! state assoc key new-value)
))))]
(swap! state assoc anim-key
{::t0 (.now js/Date.)
::ticker (js/setInterval tick interval)
::delta (- new-value initial-value)
::initial-value initial-value}))))

And that’s it!

Leave the rest to Rum. The chart uses the rum/reactive mixin to watch for changes to the :height value. Sub components like the bars and the labels are passed that value, and use the rum/static mixin so they update when the parameter changes. No need for animated-view, animated-image or animated-text at all.

Performance

I’m not expecting this solution to perform quite as well as single value animation through the Animated API, but the difference so far is not noticeable. Still, I’ll keep both solutions around till things settle down.

Testing from the REPL

(defn log-val [_ _ _ new-state]
(prn (:height new-state)))

(def state (atom {:height 0}))

(add-watch state :state-watch log-val)

(animate-to-new-value! state easeOutQuad 50 1000 :height 20)

(remove-watch state :state-watch)
=>
{:height 0,
:height-anim {:unspun.screens.rum-bars/t0 1483208967936,
:unspun.screens.rum-bars/ticker 3247,
:unspun.screens.rum-bars/delta 20,
:unspun.screens.rum-bars/initial-value 0}}
2.5155
4.6875
8.569280000000001
10.642879999999998
11.986220000000001
13.90592
15.46848
17.46528
18.3755
18.942
19.50702
19.896320000000003
19.896320000000003
20

Conclusion

I’m happier with this approach so far as it means that components don’t need any special animation handling at all. Not ruling out the animated api entirely since I’m sure there are good reasons it’s there, and one day I’ll bump into them.