React-Native Animated from Clojurescript

This is a brief note on what is required if you want to access the Animated API in react-native from a clojurescript app.

I’m starting from a project created by Tienson Qin’s 
exponent-cljs-template. If you would like to do the same, make sure you have java8 and leiningen installed and then say: 
lein new exponent your-project +rum . You can choose +om or +reagent if you prefer those react wrappers, but I shall be using +rum.

The skeleton project defines Rum components for each react-native component. Take a look at https://github.com/tiensonqin/cljs-exponent/blob/master/src/cljs_exponent/components.cljc for the full list. You will find that animated-view, animated-text, and animated-image are defined there already. However, you won’t find symbols for parts of the API that are not components.

For those you will need something like this, though I expect you will want to remove some of the repetition. The point is that we can navigate to any required javascript symbols via the top levelreact-native constant.

[Edit: The use of aget to refer to an object is no longer recommended practice. Instead, use goog.object.get from the Google Closure library.]

(def react-native (js/require "react-native"))
(def animated (aget react-native "Animated"))
(def animated-value (aget react-native "Animated" "Value"))
(def animated-timing (aget react-native "Animated" "timing"))
(def animated-spring (aget react-native "Animated" "spring"))
(def ease (aget react-native "Easing" "ease"))
(def ease-out (aget react-native "Easing" "out"))

The Mixin

Now we can create a Rum mixin that can be used to animate a component.

(defn animate-function [key f initial-value]
(letfn [(upd [state]
(let [[_ value] (:rum/args state)]
(.start (animated-timing (key state)
#js {:toValue (f value)
:duration 200
:easing (ease-out ease)}))
state))]
{:init (fn [state props]
(assoc state key
(new animated-value (f initial-value))))
:did-mount upd
:will-update upd}))

animate-function returns a mixin which manages an animated value in the component state map at the key key.

On :initwe create a new animated value starting at (a function f of) the passed in initial value. On :did-mount and :will-update we read in a new value from the component argument list and start animating the old value to the new value with an easing function. I found it useful to animate a function of the value passed in as an argument rather than the value itself. The alternative is to use the rather clunky arithmetic functions provided by the Animated API. Set f to the identity to animate the value itself. react-native will dispose of the animated value when the component unloads.

Here’s how the mixin is used to create atop-barand a bottom-bar in a vertical flex space. The bar heights are maintained in the ratio
 1 — value : value where value is between 0 and 1.

Using the Mixin

(defcs top-bar < rum/static
(animate-function ::height #(- 1 %) 0.5)
[state palette value]
(animated-view {:style {:flex (::height state)
:backgroundColor (:primary palette)}}))

(defcs bottom-bar < rum/static
(animate-function ::height identity 0.5)
[state palette value]
(animated-view {:style {:flex (::height state)
:backgroundColor (:light-primary palette)}}))

It is most important that the value to be animated — here the height — is used directly by animated-view.

A problem

An ordinary view cannot interpret an animated value correctly even if that view is a child of an animated view.

Imagine adding a label to the bar which formats the value in text. For a slow animation, the text should animate in sync with the bar height. Now we are forced to introduce an animated-text component for the label, and we need to pass that component the animated value somehow. Due to the special handling that the parent animated component gives to animated values, this is not at all straight forward.

Conclusion

The Animated api is reasonably straight forward to use in single components, but sharing an animated value between nested components is hard. Breaking component composition is potentially a serious failing of the api.

Read part 2

In the next part ‘Animating react-native — take 2’, I find a way to get round these issues.