问题
I am studying Animation in Core Animation Programming Guide and I get stuck on comprehending pause and resume animation on a layer.
The document tells me how to pause and resume animation without clear explanation. I think the key is to understand what is timeOffset
and beginTime
method of CAlayer
.
These code is pause and resume animation. In resumeLayer
method, layer.beginTime = timeSincePause;
this line really make me confused.
-(void)pauseLayer:(CALayer*)layer {
CFTimeInterval pausedTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];
layer.speed = 0.0;
layer.timeOffset = pausedTime;
}
-(void)resumeLayer:(CALayer*)layer {
CFTimeInterval pausedTime = [layer timeOffset];
layer.speed = 1.0;
layer.timeOffset = 0.0;
layer.beginTime = 0.0;
CFTimeInterval timeSincePause = [layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
layer.beginTime = timeSincePause;
}
Any help will be appreciated.
回答1:
In the header file of CAMediaTiming, we can see these codes:
/* The begin time of the object, in relation to its parent object, if
* applicable. Defaults to 0. */
@property CFTimeInterval beginTime;
/* The basic duration of the object. Defaults to 0. */
@property CFTimeInterval duration;
/* The rate of the layer. Used to scale parent time to local time, e.g.
* if rate is 2, local time progresses twice as fast as parent time.
* Defaults to 1. */
@property float speed;
/* Additional offset in active local time. i.e. to convert from parent
* time tp to active local time t: t = (tp - begin) * speed + offset.
* One use of this is to "pause" a layer by setting `speed' to zero and
* `offset' to a suitable value. Defaults to 0. */
@property CFTimeInterval timeOffset;
What important is the formula:
t = (tp - begin) * speed + offset
This formula defines how the global time (or parent time, tp) maps to the local time of the layer. And this formula can explain everything of the listed codes:
- At time A, the animation is paused. After sets the speed = 0, and timeOffset = pauseTime, the local time of the layer is equal pauseTime. And local time will not increase any more because speed = 0;
- At time B, the animation is resumed. After sets the speed = 1.0, timeOffset = 0, beginTime = 0, the layer's local time is equal to the global time (or tp) which is (timePause + timeSinacePause). But we need the animation starts from the time point #A, so we set the beginTime = timeSincePaused, and then the layer's local time is equal to timePause. Of cause, this incurs the animation continue from the point paused.
回答2:
Let's have a test at the two properties of a layer: beginTime
and timeOffset
.
Prerequisite
We get the CALayer's time space using [layer convertTime:CACurrentMediaTime() fromLayer:nil]
1、Assign 5.0 to layer's beginTime
(beginTime
is 0 before) :
NSLog(@"CACurrentMediaTime:%f", [t1.layer convertTime:CACurrentMediaTime() fromLayer:nil]) ;
t1.layer.beginTime = 5.0 ;
NSLog(@"CACurrentMediaTime:%f",[t1.layer convertTime:CACurrentMediaTime() fromLayer:nil]) ;
result log is:
2014-01-15 11:00:33.811 newUserInterface[1404:70b] CACurrentMediaTime:7206.884498
2014-01-15 11:00:33.811 newUserInterface[1404:70b] CACurrentMediaTime:7201.885088
The result shows that if I add 5.0 on beginTime
, the layer's time will minus 5.0. If an animation is in flight, adding 5.0 on beginTime
will cause the animation redo the animation 5.0 seconds ago.
2、Assign 5.0 to layer's timeOffset
(timeOffset
is 0 before):
NSLog(@"CACurrentMediaTime:%f", [t1.layer convertTime:CACurrentMediaTime() fromLayer:nil]) ;
t1.layer.timeOffset = 5.0 ;
NSLog(@"CACurrentMediaTime:%f",[t1.layer convertTime:CACurrentMediaTime() fromLayer:nil]) ;
result is:
2014-01-15 11:09:07.757 newUserInterface[1449:70b] CACurrentMediaTime:7720.851464
2014-01-15 11:09:07.758 newUserInterface[1449:70b] CACurrentMediaTime:7725.852011
The result shows that if I add 5.0 on timeOffset
, the layer's time will add 5.0. If an animation is in flight, adding 5.0 on timeOffset will cause the animation jump to the animation it would do 5.0 seconds later.
Comprehend pause and resume animation on a layer
Here is an example, t1
is an subview of UIViewController's root view. I do an animation on t1 which animates the position of t1.layer
.
If an animation is added to a layer, the layer will calculate when to animate the animation according to the animation's beginTime
, if the beginTime
is 0, it will animate it immediately.
CABasicAnimation * b1 = [CABasicAnimation animationWithKeyPath:@"position"] ;
b1.toValue = [NSValue valueWithCGPoint:CGPointMake(160.0, 320.0)] ;
b1.duration = 10.0f ;
[t1.layer addAnimation:b1 forKey:@"pos"] ;
NSLog(@"CACurrentMediaTime:%f", [t1.layer convertTime:CACurrentMediaTime() fromLayer:nil]) ;
log shows 2014-01-15 11:25:53.975 newUserInterface[1530:70b] CACurrentMediaTime:8727.108740
, which means the animation will begin at 8727 and stop at 8727+10 in t1.layer's time space.
When the animation is in flight, and I pause the animation using - (void)pauseLayer:(CALayer*)layer
method.
layer.speed = 0.0;
will cause the layer stop and layer's time will be set to 0. (I know that because when set layer.speed to 0, I immediately fetch the layer's time and log it)
layer.timeOffset = pausedTime;
will add pauseTime to layer's time(assuming layer.timeOffset is 0), now the layer's time is pausedTime.
- (void)pauseLayer:(CALayer *)layer
{
CFTimeInterval pausedTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil]; // pauseTime is the time with respect to layer's time space
layer.speed = 0.0; // layer's local time is 0
layer.timeOffset = pausedTime; // layer's local time is pausedTime, so animation stop here
}
Then I will resume the animation using - (void)resumeLayer:(CALayer*)layer
method.
-(void)resumeLayer:(CALayer*)layer {
CFTimeInterval pausedTime = [layer timeOffset];
layer.speed = 1.0;
layer.timeOffset = 0.0;
layer.beginTime = 0.0;
CFTimeInterval timeSincePause = [layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
layer.beginTime = timeSincePause;
}
If I stop the animation at 8727+1(means the animation animate for 1 second), in pauseLayer
method, layer.speed = 0
will set layer's time to 0 and layer.timeOffset = pausedTime;
will add pausedTime on layer's time, so the layer's time is pausedTime.
Wait for a moment, let's have a summary now. layer.speed
is 0.0, 'layer.timeOffset' is equal to pausedTime
which is 8727+1 and layer's time is pausedTime too. Please keep in your mind, we will use them soon.
Let's continue, I resume the animation at 8727+11 with resumeLayer
method, layer.speed = 1.0;
it will add 8727+11 on layer's time, so layer's time is 8727+1+8727+11, layer.timeOffset = 0.0;
it causes layer's time minus 8727+1 because layer.timeOffset is 8727+1 before, layer's local time is 8727+11 now. timeSincePause
is (8727+11-8727-1)=10.
layer.beginTime = timeSincePause;
it cause layer's time minus 10. Now layer's local time is 8727+1 which is the time I paused animation.
I will show you the code and log:
- (void)pauseLayer:(CALayer *)layer
{
NSLog(@"%f", CACurrentMediaTime()) ;
NSLog(@"pauseLayer begin:%f", [t1.layer convertTime:CACurrentMediaTime() fromLayer:nil]) ;
CFTimeInterval pausedTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil] ;
layer.speed = 0.0 ;
NSLog(@"pauseLayer after set speed to 0:%f",[t1.layer convertTime:CACurrentMediaTime() fromLayer:nil]) ;
layer.timeOffset = pausedTime ;
NSLog(@"pauseLayer after set timeOffset:%f",[t1.layer convertTime:CACurrentMediaTime() fromLayer:nil]) ;
}
- (void)resumeLayer:(CALayer *)layer
{
NSLog(@"%f", CACurrentMediaTime()) ;
NSLog(@"resumeLayer begin:%f",[t1.layer convertTime:CACurrentMediaTime() fromLayer:nil]) ;
CFTimeInterval pausedTime = layer.timeOffset ;
layer.speed = 1.0 ;
NSLog(@"resumeLayer after set speed to 1:%f",[t1.layer convertTime:CACurrentMediaTime() fromLayer:nil]) ;
layer.timeOffset = 0.0;
NSLog(@"resumeLayer after set timeOffset to 0:%f",[t1.layer convertTime:CACurrentMediaTime() fromLayer:nil]) ;
layer.beginTime = 0.0 ;
NSLog(@"resumeLayer after set beginTime to 0:%f",[t1.layer convertTime:CACurrentMediaTime() fromLayer:nil]) ;
CFTimeInterval timeSincePause = [layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime ;
layer.beginTime = timeSincePause ;
NSLog(@"resumeLayer after set beginTime to timeSincePause:%f",[t1.layer convertTime:CACurrentMediaTime() fromLayer:nil]) ;
}
log:
2014-01-15 13:14:34.157 newUserInterface[1762:70b] 15247.550325
2014-01-15 13:14:34.158 newUserInterface[1762:70b] pauseLayer begin:15247.550826
2014-01-15 13:14:34.158 newUserInterface[1762:70b] pauseLayer after set speed to 0:0.000000
2014-01-15 13:14:34.159 newUserInterface[1762:70b] pauseLayer after set timeOffset:15247.551284
2014-01-15 13:14:40.557 newUserInterface[1762:70b] 15253.950505
2014-01-15 13:14:40.558 newUserInterface[1762:70b] resumeLayer begin:15247.551284
2014-01-15 13:14:40.558 newUserInterface[1762:70b] resumeLayer after set speed to 1:30501.502810
2014-01-15 13:14:40.559 newUserInterface[1762:70b] resumeLayer after set timeOffset to 0:15253.952031
2014-01-15 13:14:40.559 newUserInterface[1762:70b] resumeLayer after set beginTime to 0:15253.952523
2014-01-15 13:14:40.560 newUserInterface[1762:70b] resumeLayer after set beginTime to timeSincePause:15247.551294
Anther question is in resumeLayer
method: why not combine the two assignment line to one layer.beginTime = timeSincePause;
, the reason is in [layer convertTime:CACurrentMediaTime() fromLayer:nil]
, the result value is related to layer.beginTime.
layer.beginTime = 0.0;
CFTimeInterval timeSincePause = [layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
layer.beginTime = timeSincePause;
Tell the truth, I still don't know how animation works behind, what I am doing is analyzing the result, it's not a good solution. I'm very glad that anyone who has ideas on this could share. Thanks!
回答3:
From the docs:
beginTime
beginTime Specifies the begin time of the receiver in relation to its parent object, if applicable. (required)
Here's a great explanation of it taken from here:
If an animation is in an animation group, beginTime is the offset from the beginning of its parent object — the animation group. So if beginTime of the animation is 5, it begins 5 seconds after the animation group begins.
If an animation is added directly to a layer, beginTime is still the offset from the beginning of its parent object — the layer. But since the beginning of a layer is in the past1, I can not simply set beginTime to 5 to delay the animation 5 seconds, because 5 seconds after the beginning of a layer is probably still a past time. What I usually really want is a delay relative to when the animation is added to the layer — denoted by addTime.
This is the code from the question with an addition of logs:
- (IBAction)start:(UIButton *)sender
{
[UIView animateWithDuration:10 animations:^() {//Move square to x=300
}completion:^(BOOL finished){}];
}
- (IBAction)pause:(UIButton *)sender
{
CFTimeInterval pausedTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];
layer.speed = 0.0;
layer.timeOffset = pausedTime;
NSLog(@"pausedTime: %f",pausedTime);
}
- (IBAction)resume:(UIButton *)sender
{
CFTimeInterval pausedTime = [layer timeOffset];
layer.speed = 1.0;
layer.timeOffset = 0.0;
layer.beginTime = 0.0;
NSLog(@"CACurrentMediaTime: %f",[layer convertTime:CACurrentMediaTime() fromLayer:nil]);
CFTimeInterval timeSincePause = [layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
NSLog(@"timeSincePause: %f",timeSincePause);
layer.beginTime = timeSincePause;
}
Output:
pausedTime: 20000
CACurrentMediaTime: 20005
timeSincePause: 5 // <- that's your begin time. When you hit resume you want to begin the animation from that relative time.
To sum it all up,
The animation duration is a total of 10, I stopped the animation at 5 and I also want it to be my beginTime when I resume the animation.
So, I save the pause time and subtract it from my current time in order to get the relative animation time that has passed.
来源:https://stackoverflow.com/questions/20946481/comprehend-pause-and-resume-animation-on-a-layer