nicklockwood / layout

A declarative UI framework for iOS
MIT License
2.23k stars 97 forks source link

Declaring animations inside xml file #91

Open mariob opened 6 years ago

mariob commented 6 years ago

Would something like this be possible to implement?

<UIViewPropertyAnimator
    duration="0.25"
    delay="// 0.2"
    options="curveEaseIn, allowUserInteraction">

    <animate "#someButton.alpha" value="{canBuy ? 1 : 0}" />
    <animate "#someLabel.alpha" value="{canBuy ? 0 : 1}" />

</UIViewPropertyAnimator>

In the example above, the animation will only be triggered if canBuy (which is a state variable) is changed. I guess that Expression has some kind of callback for state changes, right? When a canBuy state change occurs, the animator's start method would be called :)

Would be awesome to be able to describe animations in the same file where you actually have your views. Just imagen to be able to iterate or add animation w/o compiling (OTA ๐Ÿ˜›).

Just realised that there could possible be conflicts if #someButton.alpha has it's own expression. So maybe something like this instead:

<UIViewPropertyAnimator
    duration="0.25"
    options="curveEaseIn, allowUserInteraction">

    <animations properties="#button.alpha, #someLabel.alpha" onStateChange="canBuy" />

</UIViewPropertyAnimator>

<UIButton id="someButton" alpha="{canBuy ? 1 : 0}"/>
<UILabel id="someLabel" alpha="{canBuy ? 0 : 1}"/>

Hmm, maybe onStateChange isn't necessary. It might be possible to detect when an value may change for a property and add an animation block around the code that updates the value instead of just doing a plain update.

Does is look feasible?

nicklockwood commented 6 years ago

@mariob yeah, this looks like a good approach. We'd need to add special-case support for <UIViewPropertyAnimator/> though, because at the moment Layout only works with views and view controllers.

nicklockwood commented 6 years ago

@mariob my original thinking was that animation would be done by including t in your expression, and the framework would just linearly change the value of t from 0 to 1, with a timer, so you could apply whatever animation and easing properties you want inside your expression.

It might make more sense to use property animator to control the animation value, but maybe t or something similar could still be standardized, so you'd invoke an animation by just stating the properties you want to animate, and the duration / easing.

mariob commented 6 years ago

So if I get it right, it should look something like this?

<UIViewPropertyAnimator
    duration="0.25"
    options="curveEaseIn, allowUserInteraction">
    <animations property="#someButton.alpha" value="canBuy ? 1 : 0" />
</UIViewPropertyAnimator>

<UIButton id="someButton" alpha="t"/>

<UIViewPropertyAnimator
    duration="0.25"
    options="curveEaseIn, allowUserInteraction">
    <animations property="#someKLabel.alpha" value="canBuy ? 0 : 1" />
</UIViewPropertyAnimator>

<UILabel id="someLabel" alpha="t"/>

Or did you have something else in mind?

Will the button and label get its start value from the animation statement?

nicklockwood commented 6 years ago

@mariob not exactly. My idea was that you would describe the value of an animatable property expression as a function of t. So more like:

<!-- property animator only describes animation timing, not values -->
<UIViewPropertyAnimator
    duration="0.25"
    options="curveEaseIn, allowUserInteraction"
    properties="#someLabel.alpha"
/>

<UILabel id="someLabel" alpha="t"/>

This is very flexible, but there are some awkward aspects. For example, for values like colors, you'd have to write:

<UIView  backgroundColor="rgb(startColor.red * (1 - t) + endColor.red * t, startColor.green * (1 - t) + endColor.green * t, startColor.blue * (1 - t) + endColor.blue * t "/>

Which is obviously terrible. You could make this better by introducing an interpolate() function, so:

<UIView  backgroundColor="interpolate(startColor, endColor, t)"/>

But at that point, do you really need t, or could the entire animation be described by the interpolate() function itself? E.g.

<UIView  backgroundColor="interpolate(startColor, endColor, 0.25, .easeIn)"/>

But then this is also awkward because we don't have labelled function arguments, so maybe we go back to an XML-based solution, e.g.

<animation
    id="bg"
    duration="0.25"
    options="curveEaseIn, allowUserInteraction"
    from="red"
    to="green"
/>

<UIView  backgroundColor="#bg"/>

Again, I think I need to fully understand the use cases for this before we settle on an API. There are 3 kinds of animations I can think of (there may be more that I've missed):

1) Animation triggered as part of an action, which is a case that is quite well-supported now anyway by just wrapping setState() in an animation block (unless you want to animate something that isn't supported by CoreAnimation, which is where UIViewPropertyAnimator is handy, but we don't support that yet).

2) Interactive animation, such as parallax animations when scrolling, which I think are better suited to describing the animation using expressions as a function of contentOffset or panGesture.distance (or whatever), although it would be nice it we could avoid having to repeatedly call update() or setState() manually during the animation.

3) Idle / repeating animations such as a loading spinner. These can be done using a CAKeyframeAnimation, or by repeatedly calling setState() with a timer, but perhaps there's a better way?

So if we are going to have a mechanism for describing animations in XML, which of these scenarios are we catering to, and what does the code for invoking the animation look like at the call site? Is it triggered automatically by setState()? Or would we need a new method like beginAnimation(id: String)?

nicklockwood commented 6 years ago

Another option might be to have the animator declare variables which can then be used in any expression:

<animation
    id="foo"
    duration="0.25"
    easing="easeIn">
    <property name="color" type="UIColor" from="red" to="green"/>
    <property name="fade" type="Double" from="0" to="1"/>
</animation>

<UIView backgroundColor="#foo.color" alpha="#foo.fade"/>

Then you'd invoke it with something like:

layoutNode.animate("foo")
mariob commented 6 years ago

Animation triggered by setState is something that I'd use quite a lot if they could be expressed declaratively in the XML. I like declarative programming :)

I'm fine with just using plain UIView animations. Yes, I know, I introduced the UIVIewPropertyAnimator which might have confused you a bit, but since it's a real type I assumed that it would be simpler to implement than having a fictional animation tag. But I'm probably wrong, special treatment is still required for non view tags.

From my point of view (as a developer using the framework) I'd like to emulate the current way of animating states (which requires code in the view controller), for example:

// Called from view controller
UIView.animate(duration: 0.25) {
    layoutNode.setState(["canBuy": true]
}

UIView.animate(duration: 0.50, ... more options ...) {
    layoutNode.setState(["loggedIn": true]
}
// Corresponding XML
<UIView alpha="canBuy ? 1 : 0" />
<UIView backgroundColor="loggedIn ? #0ff : #f00" />

by having the option to express the same thing in XML. For example (almost as my first example):

<UIView_animate
    duration="0.25"
   properties="#myView.alpha" /> <!-- Can specify more than one property for the same animation -->

<UIView_animate
    duration="0.50"
   ... more options ...
   properties="#anotherView.backgroundColor" />

<UIView id="myView" alpha="canBuy ? 1 : 0" />
<UIView id="anotherView" backgroundColor="loggedIn ? #0ff : #f00" />

The above would wrap myView's alpha and anotherView's backgroundColor evaluation of the expressions inside an animation block. The question is if it's feasible to do?

Somehow catch if a state change for a property is about to happen (for example backgroundColor on anotherView when loggedIn is changed). Check if the change should be executed inside an animation block by checking if the property is referenced by any animation declaration.

A nice side effect: <UIView_animate xml="bounce.xml" props="..." /> :) Re-usable animations

Does it make sense to you?

nicklockwood commented 6 years ago

I like declarative programming :)

Yeah, I think we're definitely on the same page with this :-)

The problem I see with triggering animations using setState() is that you might sometimes want the properties to be animated and sometimes not, and you might also have multiple different animations that affect overlapping properties and which you would want to apply at different times.

I think this could possibly be addressed by naming the animations and then having a variant of setState() which takes an animation name as a second argument. This would be consistent with the solution I currently use for methods like UIScrollView.setContentOffset(_:animated:), where you invoke the animated variant by calling setState(blah, animated:true).

That might look something like:

<animation
    id="anim"
    duration="0.25"
    easing="easeIn"
    properties="..."
/>

<UIView backgroundColor="bgcolor"/>
layoutNode.setState(["bgcolor": "red"], animation: "anim")

This doesn't cover all the scenarios I described, but I think it would be a good start. With respect to the properties argument of the animation, I think there are a few options:

  1. The properties could be names of view properties, as you proposed, e.g. #myView.backgroundColor, but I don't particularly like this, for a couple of reasons: Firstly it would mean giving names to every view that you want to animate, and second it's not actually the view properties you are specifying when you call setState() - it's state variables.

  2. The properties mentioned in the animation could be the name of state fields (or any other variable that can be referenced in an expression). This actually makes a lot of sense because it would allow you to specify the animation in terms of the input you are changing. In my example above, if you specified "bgcolor" as the animation property, and then called setState(_:animation:) with a new bgcolor value, any property whose expression value depends on that variable would be animated.

  3. The simplest option, at least initially, might be to not specify any properties in the <animation/> block at all. The properties to be animated would be implied by the state you change. If you call setState(["bgcolor": "red"], animation: "anim") then it's pretty obvious you want to animate any property that depends on "bgcolor" using the "anim" config. This would also work when using a state struct rather than a dictionary, since it would only animate properties whose values have changed.

What do you think?

nicklockwood commented 6 years ago

It occurs to me that an alternative way to specify animations without having to name them would be to make them either a parent or child of the view(s) you wanted to affect. E.g:

<UIView backgroundColor="bgcolor">
    <animation duration="0.25" easing="easeIn"/>
</UIView>

or

<animation duration="0.25" easing="easeIn">
    <UIView backgroundColor="bgcolor"/>
</animation>

So now if you called setState(["bgcolor": "red"]) the color would update instantly, but if you called setState(["bgcolor": "red"], animated: true) then it would apply the animation you'd specified.

Like the solution I proposed above, it would choose which animations to apply implicitly based on the properties that are affected by your state change. The advantages I see for this approach are:

1) animations don't have to be named, and the native Swift code doesn't need to know about them 2) we can re-use the existing setState(_:animated) method, which seems quite elegant 3) different animations can be applied to different views simultaneously as a result of the same state change

Disadvantages:

1) No possibility for multiple animations that affect the same view/property 2) No easy way to share an animation block between multiple views that aren't siblings 3) In the first case it might be unclear whether an animation affects it's parent, or its siblings, and in the second case it's not obvious if it should only affect its immediate child, or all of that child's descendants as well.

As you can probably tell I've not spent a lot of time on this yet, so I'm pretty much thinking out loud, but hopefully we're narrowing down the solution space :-)

mariob commented 6 years ago

Sorry for the late reply, but its been a busy week ๐Ÿ™‚

Yeah, definitely looks like we're getting closer.

I like the idea that you don't have to know about the animations in Swift and only tell the view if you want to animate or not by specifying it in the setState call.

I think that only the first solution, where the animation is a child tag, is doable. The UIView tag must be the top-level tag in a XML file, right? (Also makes sense to have a UIView as top-level element and not an animation).

I understand your concern about:

In the first case it might be unclear whether an animation affects it's parent, or its siblings...

It's kind of hard to solve, XML is hierarchical, so there's not really a natural place to put associated data. Guess documentation will solve that.

I don't think we can solve all animation needs, but I think that we can catch a lot of simple use cases where we can avoid declaring animations in code and have the benefit of live reloading.

The only disadvantage I see is that we can only have one animation for the same view. But if we combine that with what you wrote in a previous post:

The properties mentioned in the animation could be the name of state fields (or any other variable that can be referenced in an expression).

If that's also included we could have multiple animation for one view, for example:

<UIView backgroundColor="bgcolor" alpha="someValue == 10 ? 1 : 0.5">
    <animation duration="0.25" easing="easeIn" variable="bgcolor" />
    <animation duration="0.52" easing="easeOut" variable="someValue" />
</UIView>

Hmm, might have yet another options... What if you could specify which animation to be applied when the value changes for a view property, something like this:

<UIView backgroundColor="animate(color, bgcolor)" alpha="animate(fader, someValue == 10 ? 1: 0.5)">
    <animation duration="0.25" easing="easeIn" name="color" />
    <animation duration="0.52" easing="easeOut" name="fader" />
</UIView>

This is something Expression would be able to do, right? You could re-use animations and specify them anywhere in the XML-tree (typically you put them as a child under the root-view but it wouldn't be mandatory). I guess Layout could also optimise and put all changes for one variable in one animation block?

Damn, too many options ๐Ÿ˜

mariob commented 6 years ago

@nicklockwood Any thoughts?

nicklockwood commented 6 years ago

@mariob sorry, I've not really had a chance to think about this any more since our last discussion. I think we're close to a good API, but still having trouble choosing between the different variants. It might be worth having a go at implementing it so we can try it out in practice. It's hard to get a sense for what the API is like to use without a practical use case, as toy examples like this can be misleading.

mariob commented 6 years ago

Sounds good. I can assist with writing a small app containing some common use cases if that would help.

For example, a login form with validation. Showing field validation errors underneath the text fields. Password with too few characters and incorrectly formatted e-mail address.

Might be able to "extract" more use cases from real world apps

nicklockwood commented 6 years ago

@mariob yes, that would be helpful.