WWDC2017 Session 230

Transcript

[ Background Sounds ]
>> Hey everyone, I'm Joe Cerra
and I'm an engineer UIKit and
welcome to advanced animations
with UIKit.
So, today we have a lot to talk
about.
We're going to start off by
covering some of the basics and
how animations work and also how
animations are timed.
We're going to discuss how to
make animations fully
interactive and interruptible
using some modern techniques.
Then we're going to talk about
some of the new API that we're
providing this year in iOS 11.
And then we're going to use all
of that knowledge and I'm going
to show you how to coordinate
animations.
And then finally, we're going to
go over some tips and tricks and
a few more techniques to help
you create really great
animations for your users.
So, let's get into it.
UIView-based animations have
been around really since the
dawn of iOS.
And here's a quick refresher on
how they work.
So, say I have a UIView that
renders a circle and I'd like
the animate it from its position
of X equals zero to 100.
Well the way I might do this is
by calling the UIView.animate
method.
Here I'm calling UIView.animate
with a duration, performing some
animations in an animation
block.
In this case, I'm offsetting the
X value of our circle by 100.
And when I do that UIKit will
implicitly create an animation,
add it to our layer and produce
the following animation.
But last year we introduced the
UIViewPropertyAnimator, which
gives us a lot more control for
animations than its predecessor
UIViewAnimate.
Now this includes being able to
provide custom timing functions
and the ability to make your
animations fully interactive and
interruptible really easily.
In fact, with property animator
you can modify your animations
on the fly.
So, let's take a look at our
animation again and this time
we're going to use an animator
to animate it.
So, we create our animator
providing a duration and a
timing curve provider
animations.
And then we call startAnimator
which actually then runs that
animation block producing this
animation.
Now in the last two examples we
drove our animation with a
linear timing curve.
And what a timing curve is, is
essentially a function that maps
time to progress or the fraction
of elapsed time of your
animation to the fraction of
progress of your animation.
Now linear timing curves are
actually kind of interesting and
this is because the fraction of
time is equal to the fraction of
progress and we'll see why this
is interesting in just a second.
There are also of course, timing
curves other than linear, such
as the built-in ease in timing
function which starts off slowly
and then accelerates.
And the built-in ease out timing
function which starts off
quickly and then decelerates.
Well with property animators you
can provide your own custom
timing functions, such as this
one.
And the way you do that is by
providing two cubic Bezier
control points.
Now we'll see a little bit later
why it can be really useful to
be able to provide your own
custom timing functions.
So that's essentially how
animations work.
Now I want to talk about how to
make your animations fully
interactive.
So, an interactive animation is
one in which the user's actions
interactively drive the progress
of your animation.
So, here's a familiar example
where a gesture is driving
interactive animation and now
we're using 3D Touch to drive
another animation.
And we can, of course, dismiss
our Control Center view
interactively with a gesture.
So, let's do a quick
demonstration here.
So, we're going to add a
PanGestureRecognizer to our
circle and we're going to
animate it, but we're going to
do so with our finger by
scrubbing an animator.
And at this point we're going to
lift our finger off and continue
that animation to its target
position.
Now here's the code to do this.
We're going to save an instance
of our property animator in our
hand gesture recognizer handler.
We're going to create that
animator and initialize it with
some animations in an ease out
timing function.
Afterwards we're going to call
pauseAnimation immediately after
and that's going to run that
animation block producing that
animation implicitly.
And what property animator does
is essentially sets the speed of
that animation to zero.
So now we can interact with it.
So, we're going to scrub our
animator's fraction complete
based on the distance our finger
travels relative to the total
distance of the animation, which
in this case is 100 because
we're animating from zero to
100.
And when our finger is lifted
off we're just going to call
continueAnimation.
Now that's really easy, but
there's actually two really
interesting moments that occur
here and that is when we pause
our animation and then when we
continue it, so let's take a
look at those.
So, we've just created our
animator and we're about to
pause it so we can interact with
it.
Notice that our animator has
been created with an ease out
timing function.
So, let's call pause and see
what happens.
So, our animator becomes active,
but we've just converted our
timing curve automatically into
a linear timing curve.
Why did the property animator do
that?
Well it turns out this makes
scrubbing your animation really
easy because of that property of
linear timing functions where
the fraction of time is equal to
the fraction of progress, you
can now scrub both time and
progress uniformly.
Now let's take a look at what
happens when we continue our
animation.
So, we're scrubbing our
animation here and now our
finger is lifted off and we're
about call continueAnimation, so
let's see what that does.
So, we convert back to that ease
out timing function, but also
something interesting happened.
We remapped time in the process.
So, fraction complete which was
once 50% is now 10% and the
reason for that is we want to
keep our progress value stable
when we convert back to that
ease out timing function.
Also, I want to draw attention
to our duration factor here,
which is zero and what this
means is for our property
animator to use whatever
remaining time it has left,
which in this case would be 90%
of its original duration.
So, if for example, our animator
was created with a duration of
two seconds it would continue in
1.8 seconds.
So that's how interactive
animations work.
Now let's talk about how to make
our animations fully
interruptible.
Now an interruptible animation
is one in which the user's
actions interrupt or pause a
currently running animation.
Here's an example that you're
probably familiar with, with
Safari.
When you flick your finger, it
accelerates and then it sort of
decelerates.
But if you touch the screen
again you interrupt that
animation at which point you can
scrub it.
So, we're going to do one more
demonstration this time and
we're going to add our
PanGestureRecognizer to our
circle, but this time we're
going to let it animate for a
little bit and then we're going
to catch it midflight.
So here it is animating then we
catch it with our finger at this
point we can scrub it, but we've
already seen how that works so
instead, we're just going to
lift our finger and continue the
animation to its target
destination.
So, here's the code from before
and we're just going to make a
few changes so we can support
both interactive and
interruptible animations.
So, we're going to introduce a
new method here called
animateTransitionIfNeeded and
this is a custom method, this is
not a UIKit method for example.
So, what this does is it'll
initiate our transition if it
isn't already running.
Were also going to introduce a
new property called
progressWhenInterrupted and this
is going to save any relative
progress made by your animator
prior to it being interrupted.
Now when our gesture begins
we're going to create that
animator again, but this time
only if our transition isn't
running.
We're then going to pause it so
that we can interact with it and
we're going to save any relative
progress made by it prior to
being interrupted.
When our finger moves we're
going to scrub our animator's
fraction complete, but this time
we're going to scrub our
animator based on the distance
our finger has traveled relative
to any progress it made prior to
being interrupted.
And when our finger lifts we're
going to continue our animation.
But to make this example a
little bit more interesting
let's continue with an ease out
timing function and let's assume
that our animation was created
with an ease and timing function
just to see what that does.
So, here we are about to
interrupt our animation and
fraction completes about 50% sol
it's ran for about half its
duration and progress is only
about 10% because we're on an
ease out timing function.
So, let's call pause and watch
what happens.
So again, we convert into that
linear timing function to make
scrubbing really easy, but we
also have remapped time just as
we did before to keep progress
stable so our animation doesn't
jump.
Now when we call
continueAnimation we're going to
do so with an ease out timing
function and again, you can see
we convert back into that new
timing function and remap time
again.
So, this is a really subtle
aspect of property animators
that's really important to
understand when you're
manipulating them.
So that's how to make animations
fully interactive and
interruptible.
Now let's talk about some of the
new API that we're providing
this year.
So, new in iOS 11 property
animator is getting two new
properties, scrubsLinearly and
pausesOnCompletion.
It's also getting a new behavior
which is starting as paused, so
let's talk about these.
So, in the last couple of
examples you saw that when we
paused our animator it converts
the timing function into a
linear timing function and it
does this to make scrubbing the
progress of your animator really
easy.
But you know sometimes it's
really useful for your animator
to maintain its pacing when it's
being driven interactively.
And now you can do that by
disabling linear scrubbing.
And here's just a quick example
where linearly scrubbing the
opacity of the circle on top and
we're nonlinearly scrubbing the
opacity of the circle on the
bottom according to an ease out
timing function.
We'll see a little bit later
when we're coordinating
transitions this could be a
really interesting thing to do
to create some pretty compelling
animations.
Animators can now also pause on
completion.
So, when an animator's
animations finish it will
automatically transition into
the inactive state.
And when it does that it's going
to release any animations that
it was previously tracking which
means you cannot manipulate or
even reverse them after they've
finished.
But now if you set pauses on
completion to true your animator
will pause at 100% fraction
complete allowing you to at any
point in the future reverse
those animations.
And to give you guys a little
bit of insight here we actually
use this in UIKit for
Drag-and-Drop.
So, here's an example of a lift
animation and as you may be
aware, you can provide your own
alongside animations to
customize this.
And we're actually going to
drive those animations by a
property animator that pauses on
completion internally in UIKit.
And because of that we could
easily reverse those animations
for you at any time after the
user lifts their finger even if
your animations have already
finished.
I also should mention that we're
not going to call your
animator's completion handler
when it pauses on completion,
but if you are interested to
know when those animations have
finished you can simply observe
the running property.
And finally, now you can create
a property animator and start it
before you've provided any
animations to it.
Now what this does is any
animation blocks that you
subsequently provide will be ran
immediately instead of escaping.
And this is great if you're
transitioning any of your UIView
animate code over to using
property animators.
Now I just want to spend a
couple minutes here talking
about springs.
So, spring animations are
pervasive in UIKit in iOS and
they add a sense of realism to
your animations.
Now in UIKit we provide two
kinds of springs, critically
damped springs and under damped
springs.
A critically damped spring is
one which accelerates quickly
and it sort of decelerates just
as quickly hovering over its
target value, whereas an under
damped spring accelerates
quickly beyond its target value
and then oscillates.
So, spring animations are unique
in this way and we think of
spring animations just as we
think about timing curves.
But spring animations are also
unique in that they always
animate from their current
presentation value.
And there are a couple reasons
why we do this.
So, the first is, it may
actually be undefined to convert
a spring animation to a cubic
animation and this is because
cubic timing functions don't
oscillate or overshoot their
value as they're bounded by
minimum and maximum values.
And the second reason it's a
little bit more interesting and
this is if you animate with a
two-dimensional initial velocity
with unique DX and DY components
property animator is actually
going to decompose that for you
and create two spring
animations.
And because those two spring
animations have different
velocities they're going to be
desynchronized so we're not
going to be able to convert
those onto a cubic animation.
So, if you do have to interrupt
the spring animation here are a
few best practices.
So, the first thing you can do
is consider stopping your spring
animation, promoting current
presentation value to model
value, and then creating a
brand-new animation from there.
The second thing you can do is
consider using a critically
damped spring without initial
velocity as these don't
overshoot or oscillate.
And then finally, if you are
animating with a two-dimensional
initial velocity with unique X
and Y components you can
consider decomposing that
yourself and driving the X
animations with one animator and
the Y animations with another.
So that's how property
animations work.
Now let's talk about how to use
all that knowledge to coordinate
animations.
So, for this we're going to
build a fully interactive
interruptible animated
transition that coordinates
across multiple uniquely timed
animators.
So, say that we have an app and
our app is showing some piece of
content and anchored to the
bottom of the screen is the
Comments button and when we tap
on it it expands showing our
comments view.
Now we might implement this
using UIView controller animated
transitioning for example, but
we'd like this to be completely
interactive and interruptible so
I'd like to show you how to do
that.
So, the first thing we're going
to do is add two gesture
recognizers, a tap gesture
recognizer so that we can tap on
it and expands and tap on it
again it will collapse.
We'd also like to be able to tap
on it while that animation is
running so it can be reversed.
And we're going to add a pan
gesture recognizer so we can
interact with it.
So, here's our code form before
and we're going to make just a
couple modifications to it to
create this infrastructure that
we're going to build on.
So, the first thing we're going
to do is we're going to replace
our instance of our property
animator with a collection of
running property animators.
And for that collection of
running property animators if we
ever create a property animator
we're just going to add it to
that collection and let's assume
that when those animations
finish it's automatically
removed.
Next, we're going to reintroduce
our animateTransitionIfNeeded
method and it's going to take a
target state to animate to.
So, if we look at this if our
runningAnimators.isEmpty that
means there's no transition
currently running, so we're
going to initiate a transition
if that is the case.
And we're going to do that by
creating a new property animator
for our frame which is going to
use a critically damped spring.
We're then going to perform our
animations and we're going to
start that animator and add it
to that collection of running
animators.
Next, in our tap gesture
recognizer handler we're going
to call this method and this is
going to animate or reverse our
running transition.
So, if our transition isn't
running we're going to just
initiate our transition,
otherwise we're going to iterate
through all of our running
property animators and reverse
them.
Now for these next three methods
we're just going to extract our
pan gesture recognizer handling
code from before, so I'm just
going to quickly summarize what
these do.
So, for
startInteractiveTransition this
is called when your gesture
begins and it's just going to
initiate the transition if it
isn't running, pause all of your
animators uniformly, and save
any relative progress made by
them.
UpdateInteractiveTransition is
going to scrub your animators
uniformly relative to the
distance your finger travels and
any progress that your animators
made prior to being interrupted.
And then finally, when your
finger is lifted we just call
continue animation on all your
animators conditionally
reversing them based on the
direction your finger was
traveling.
So, let's check this out.
So, we can drive our animation
non-interactively by tapping, we
can also interactively drive it
by pulling up or pulling down,
and we can tap on it again while
it's running to interrupt it and
reverse it.
And we can also capture
animation while it's running and
at which point we can scrub it.
So now we've created this
infrastructure and right now we
just have a frame animation
which isn't that interesting, so
let's make this a little bit
more interesting.
So, first thing we're going to
do is we're going to add an
interactive blur.
Now in iOS 8 we introduced
UIVisualEffectView, which allows
us to add blur and vibrancy to
our view hierarchy.
And it turns out that the
effects property of
VisualEffectsView is an
animatable property so that's
great.
So, what we're going to do here
is the only code changes we're
going to make is to animate
transition if needed and we're
just going to create a new
animator for our blur, which is
going to use a critically damped
spring at least for now.
And then we're going to perform
our animations here either
setting or unsetting the blur
effect, starting our blur
animator and adding it to that
collection and here's what we
get.
So, we can now have an
interactive blur animation, but.
Let's see that again in slow
motion because I've got to be
honest with you guys, I don't
know if I'm really feeling this
blur animation right now.
I feel like it's animating
potentially a little bit too
quickly.
Now let's drive this and take a
look at it.
So maybe it looks a little
better, but it still doesn't
look quite right.
Now there are a few reasons for
this.
The first is because we're using
a critically damped spring our
blur is going to animate in too
quickly.
And because our property
animator is going to linearly
scrub it our blur is still going
to animate in a little bit too
quickly and it's going animate
out too slowly.
So, in order to fix this, we're
actually going to provide our
own custom timing curves.
So, our custom ease in function
so that our blur animates in
really slowly and a custom ease
out timing function so it
animates out really quickly.
And because these are inversions
of each other we're going to get
symmetric pacing, which means
that the path of our animation
on its way out will match that
of its path on the way in.
So, here's the code for this,
we're just creating cubic timing
parameters based on the target
state and we're going to disable
linear scrubbing here so that
our animator maintains its
pacing when we're driving it
interactively.
So, let's check this out again.
Much better.
It's really subtle, but it's
much better this time.
And let's also do that a little
bit more slowly so you can
really get a sense of what it
looks like.
And also, let's drive that
interactively so you can see
that it does indeed maintain its
pacing when you're driving it
interactively.
Cool, so now we have two
property animators with unique
timing characteristics
contributing to our overall
transition.
But let's make it a little bit
more interesting.
I'd now like to demonstrate a
technique I like to call view
morphing.
So, say that we have a label; in
this case we have a label.
It's blue, it's kind of small,
it's got a regular typeface and
let's say we want it to be a lot
bigger, maybe a different color,
maybe a heavier typeface.
What would that transition look
like?
Well this is what I call view
morphing, it's the scaling
translation and opacity blending
of two views.
So, in this case, we're going to
use UILabels, but this technique
is generally applicable to any
view or view hierarchy; not just
labels.
So, [inaudible] we're going to
take our comments label and
we're going to make it blue and
when it expands we're going to
want it to look like this.
And notice it's much darker, but
it's also slightly inset from
the top of its parent view.
And the way that we're going to
animate that is like this.
So, how are we going to build
this?
Well, UILabels don't expose any
animatable properties, but
that's okay because as I
mentioned, this is generally
application to any view or view
hierarchy, not just labels.
What we're going to do is use
UIView's oft overlooked
transform property and we're
going to compute the scale and
translation for both of our
labels so that we can blend them
into each other.
And we're also going to animate
their opacities so that we can
blend them together.
So, first thing we're going to
do is compute the scale.
It turns out this is really easy
to do, it's just a simple
dimensional ratio based on the
target dimension and your
current dimension.
And, in fact, once you computed
one of these you basically get
the other one for free by taking
the inverse.
Now computing translations is a
little bit more interesting and
this is because we're animating
our scale, which is going to
affect our bounds during the
transition, so we can't just
simply take the Y offsets.
What we can do is pre-apply that
scale of transform in order to
obtain a new value for our Y
offsets and we could use that
for our translation.
And now we're going to drive
this using three animators, a
critically damped spring to
drive our transform so that it
follows the overall path of the
transition and then ease in and
ease out animators to perform
the opacity blending, both of
which are going to scrub
nonlinearly.
And here's the code for this and
I've also omitted some of the
repetitive bits.
So, we created transform
animator, we animate the
transform of our labels.
The incoming label is getting
the identity transform and
that's because it's already been
pre-scaled and translated down
to match the bounds of the
outgoing label.
And the outgoing label is going
to get transformed such that it
matches the bounds of the
incoming label prior to it being
animated.
And then we create two property
animators here to blend our
alphas, we disable linear
scrubbing here so they maintain
their pacing and let's see what
that gets us.
So again, non-interactively we
can animate this transition, but
we can also interactively
animate it and it looks great.
And we can even animate it and
then interrupt it and it just
works.
What I think is really cool --
[ Applause ]
What I think is really cool
about this is we now have one
cohesive animated transition
that's being driven by six
different property animators,
five of which have their own
timing characteristics.
Now being able to do this prior
to property animators required a
ton of code and complexity, but
now with this infrastructure
we've set up we can use property
animators to easily achieve
these effects just by declaring
our timing characteristics and
scrubbing behaviors.
So that's how to coordinate
animations.
Now let's talk about some tips
and tricks and some additional
techniques that you can use when
you're creating animations for
users.
So, is there anyone out there
that's ever tried to animate a
corner radius before?
I imagine maybe a few of you.
So, to do this you pretty much
have to manually set your corner
radius or if you want to animate
it you have to create a CA basic
animation and set its to and
from values.
What we'd like to do is animate
our corner radius such that it's
interactive.
So, how are we going to do this?
Well I'm really happy to let you
guys know that corner radius is
now a fully animatable property
in UIKit.
[ Applause ]
Now because UIView doesn't
expose the corner radius
property you actually have to
reach into your views backing
layer to modify the corner
radius, but as long as you do
that within an animation block
we're going to implicitly create
that animation for you and it'll
be fully trackable and
scrubbable.
In fact, you can even do this
from the UIView animate method.
So that takes care of our
interactive corner radius
animation.
Now what about these two guys,
we only want to animate the top
left and right corners, so how
do we do that?
Well I'm also happy to let you
guys know we're adding a new
property to CALayer, which is
maskedCorners.
[ Applause ]
Now this allows us to
selectively choose which corners
we want to apply our corner
radius mask to, which in our
case is going to be the top left
and top right corners.
And now finally, here's the code
to do this again, omitting some
of the repetitive bits.
So, we're just creating a new
animator here, we're going to
use a linear timing function and
we're going to perform our
corner radius animations and
that's pretty much all we need
to and that gives us the
following animation.
And it's subtle, but we are
indeed interactively scrubbing
the corner radius there, which
is cool.
Now if there's one underlying
message thus far it's that it's
really important for all your
animators to share the same unit
duration.
This makes scrubbing them really
easy and it makes it possible to
uniformly scrub them.
But, you know, sometimes it's
kind of useful to have an
animation that finishes a little
early or one that maybe starts
with a delay.
And an example this could be in
the following animation where if
you notice the Details button
here it animates in about
halfway through is when it
starts.
And it's fully animated out
around that same halfway point.
And if we try this interactively
you can really see it, so it
just starts animating at this
point and it's fully animated
out right around there.
And if you look across UIKit you
can actually see a lot of places
where we do this.
In fact, UINavigationBar since
iOS 7 has had this effect when
you drive that animation
interactively.
So, we could create an animator
with reduced duration or we
could use a delay factor here,
but that's really going to
complicate our scrubbing code.
It turns out there's a much more
elegant solution here and that
is using keyframe animations.
So, if we look at the UIView
headers we see the following two
keyframe methods.
And I'd like to draw your
attention to RelativeStartTime
and RelativeDuration.
So, in order to create this
effect, we perform a keyframe
animation inside of our property
animator, so we call
UIView.animateKeyframes with a
relative duration of zero.
And what this means is our
keyframe animation will inherit
the duration of its outer
property animator and in fact,
if you nest animations like this
you'll get this inheritance
behavior for free.
So, this can be really useful if
you're not using property
animators.
Now when we expand our comments
view we want that animation to
start late so we're going to use
a relative start time of 0.5 and
we're going to compensate for
that by using a relative
duration of 0.5.
And when we collapse it we're
going to use a relative start
time of zero because we want it
to immediately begin or we use a
relative duration of 0.5 so that
it finishes early.
And I'd like to draw your
attention to these two
parameters here.
So, you don't have to use a
linear timing function for your
keyframe animations you can use
any timing function you want,
including your own custom timing
functions.
And if you have multiple
keyframes here, which we don't,
you could actually interpolate
them using the options parameter
or control the way that they're
interpolated using the options
parameter.
Now lastly, I'd like to talk a
little bit about additive
animations.
So, additive animations are
really powerful and I find them
really interesting because they
allow us to modify or to animate
a single property of a view with
multiple simultaneously running
animations.
Now for this demonstration the
sort of point of it is that I'd
like to demonstrate how to think
a little bit more additively
when we're designing and
building our animations.
So, let's say that we have a
square and let's say that we'd
like to animate it by 360
degrees times 10.
That animation might look
something like this.
Now you might be thinking well
that's just as easy as animating
the transform our view and you
wouldn't actually be wrong.
But code such as this, which is
animating our view by 20 pi
radians, does not produce that
animation.
It actually produces this
animation.
Did you see it?
Well, it actually didn't animate
and there's a really good reason
for that.
That's because Core Animation
only cares about the total
displacement when you're
animating the transform of your
view.
So, in this case, the target
rotational value, which is 20 pi
radians, is the same orientation
as the current rotational value.
So, the total displacement there
is actually zero.
Now Core Animation will actually
produce an animation for you,
but it's to and from values are
going to be the same.
And you know a similar problem
exists if you try to rotate a
view by 180 degrees
counterclockwise by specifying
negative pi radians.
And that's because Core
Animation because it only cares
about total displacement is
going to look for the shortest
path, which when ambiguous,
which it is here, will be
clockwise.
So, how do we do this?
Well we could drop down a core
animation and create our own CA
basic animation and manually set
our to and from values and
that's perfectly fine and would
work.
But we wouldn't get the tracking
and scrubbing behavior from
property animators.
And it also would make this
example much less interesting.
What we could do is we could
decompose this into several
smaller additive rotational
animations and animate them all
together to create our desired
effect.
So, it turns out transform is an
additively animatable property
as long as it's affine along
with frame, bound, center, and
position.
So, in our solution we're
actually going to create 20
animations altogether and each
animation is going to animate
for 180 degrees and altogether
they're going to contribute to
this following animation.
Now what's kind of cool is we
actually have 20 animations
running simultaneously here.
And I'm not suggesting that this
is a good idea generally, but it
does help us think a little bit
more additively when we're
designing and composing and
creating our animations.
So, the point of this is to
consider how you can chain many
animations together or compose
together to create interesting
transitions.
So today, we learned about how
to make animations fully
interactive and interruptible
using some modern techniques.
We also talked about how to
coordinate several animations
together that all have unique
timing functions.
And we looked at some techniques
in order to help us create some
really awesome animations for
our users.
It is my hope that you walk away
from this presentation and
consider making more of your
animated transitions fully
interactive and interruptible.
So, we have a few related
sessions.
If you've missed any of these I
encourage you to watch them
online and there are a few
interesting sessions from prior
years.
So, if you're interested in
animations, I highly encourage
you to check these out,
especially last year's session
where we introduced
UIViewPropertyAnimator.
For more information, please
feel free to visit the following
URL and thank you all very much.
[ Applause ]