iOS UIWebView进度条

加载进度条

1 加载策略

研究过微信的WebView加载显示策略后,我们模仿并制定了自己的策略。前10秒,匀速前进至整个进度条长度的60%;中间10秒,匀速前进至85%处;最后10秒,匀速前进至95%处。如果进度条加载过程中,网页加载完毕,执行最后一步,加速前进至100%,并消失。

Step Interval Range Speed
1 4s 0%~60% EaseOut
2 10s 60%~85% Linear
3 10s 85%~95% Linear
4 0.5s X%~100% EaseIn

2 动画选型

蓝色的加载进度条我们使用CALayer实现,CALayer的初始位置处于{(0, 0), {0, height}},宽度为0,所以是不可见的。根据上述的加载策略,我们需要为CALayer加上四种Animation,其中前三种Animation是串行,并且是连续的。

2.1 串行动画设置

设置串行动画有多种思路。一种是每次只加入一个Animation,当该Animation结束的时候,加入后一个Animation。还有一种方式是一次性设置好多个Animation,一次性加入到CALayer中。动画的初始化如下,进度条的实现方式是对bounds.size.width这个虚拟的keypath进行变化。

#define kProgressBarWidthKeyPath @"bounds.size.width"

- (CABasicAnimation *)makeAnimation:(CFTimeInterval)duration toProgress:(CGFloat)to {
    CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:kProgressBarWidthKeyPath];
    anim.toValue = @(self.frame.size.width * 2 * to);
    anim.duration = duration;
    anim.delegate = _animationDelegate;
    return anim;
}

2.1.1 每次加入一个串行动画

每次加入一个串行动画的关键之处就是在动画结束时加入后一个Animation。所以在animationDidStop中,我们获取到当前的动画,找到动画数组中当前动画的后一个动画,并加入到CALayer中执行。值得注意的是参数anim和CALayer使用addAnimation:forKey:加入的Animation对象并不是一个对象,也就是说生成的animation和执行的animation虽然大部分的设置参数相同,但并不是同一个对象。参数flag表示该动画是自然结束的还是手动中止,CAAnimation并没有明确的函数自我中止,只能被过CALayer移除该CAAnimation才能中止该动画。这种方法的缺点是回调函数太复杂了,实现方式不简洁。

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
    DebugLog(@"animationDidStop: %@", @(flag));
    if (!flag) {
        return;
    }
    CAAnimation *animation = [_progressLayer animationForKey:_currentAnimationKey];
    NSInteger idx = [_animations indexOfObject:animation];

    if (idx != NSNotFound) {
        if (animation != _animations.lastObject) {
            CAAnimation *next = _animations[idx+1];
            _currentAnimationKey = [[NSUUID UUID] UUIDString];
            [_progressLayer addAnimation:next forKey:_currentAnimationKey];
        }
    }
}

2.1.2 一次性加入多个动画

一次加入多个串行动画的时候,应当设置好动画的beginTime和duration,第一个动画的beginTime是CACurrentMediaTime(),后一个动画的beginTime2是前一个动画的终止时间beginTime1+duration1。

CAAnimation *previous = nil;
for (NSUInteger i = 0; i < count; i++) {
    CAAnimation *anim = _animations[i];
    anim.beginTime = !previous ? CACurrentMediaTime() : previous.beginTime + previous.duration;
    [_progressLayer addAnimation:anim forKey:nil];
    previous = anim;
 }

2.2 动画连续性

无论上上面哪种设置,都需要考虑到串行动画的连续性问题,即前一个动画结束后,后一个动画开始前不能有断开的情况发生。默认情况下,CAAnimation完成后,会自动从CALayer中移除,那么CALayer的动画属性就会跳跃到其初始值。第二个动画再次启动的时候,会从初始值再变化到下个动画的初始值。有两种方法可以达到连续性的效果,一种是动画结束后手动设置bounds的宽度为动画结束时的值。由于bounds属性本身是隐式动画属性,为了防止跳跃性,需要手动临时禁止隐式动画。

- (void)setProgressBarToAnimationToValue {
    [CATransaction begin];
    [CATransaction setDisableActions:YES];

    CABasicAnimation *currentAnim = [_progressLayer animationForKey:_currentAnimationKey];
    CGFloat width = [currentAnim.toValue floatValue];
    CGRect bounds = _progressLayer.bounds;

    _progressLayer.bounds = (CGRect) {bounds.origin, {width, bounds.size.height}};

    [CATransaction commit];
}

另外一种方式是当动画结束的时候,不移除动画,并且设置动画结束时,将动画属性的值保留在结束时的值。为此我们在生成Animation的时候设置两个属性。设置removedOnCompletion后,动画不被移除;设置fillMode为forward时,会保留动画结束时状态。

anim.removedOnCompletion = NO;
anim.fillMode = kCAFillModeForwards;

2.3 关键帧动画

关键帧动画CAKeyframeAnimation是第三种方案,因为Keyframe Animation本身就是串行动画,提供设置若干个串行的values,每个value的keyTime,value之间的timingFunction。

3 启动和结束

启动动画是在UIWebView初始化的时候就发起,如果动画加载的过程中网页加载完成或者加载失败,就会执行第4种的结束动画。值得注意的是,在动画结束回调中,我们需要将所有的动画移除。

4 注意事项

开发过程中我们有一些事项需要注意,避免一些问题的发生,主要是循环引用的防止。

4.1 防止循环引用

CAAnimation的delegate是strong,所以设置时会产生循环引用:
self–(strong)–>CAAnimation–(strong)–>self[作为delegate]。因为delegate是strong的,可以使用YYKit中的YYWeakProxy类来解决这个强引用的问题。YYWeakProxy实际上是一个NSProxy,可以设置一个target,这个target就是我们原先的CAAnimation的delagete所指,而且在YYWeakProxy类中被声明为weak。这样就解除了循环引用: self–(strong)–>CAAnimation–(strong)–>YYWeakProxy–(weak)–>self[作为delegate]。

YYWeakProxy 实现了NSProxy的三个方法forwardInvocation, methodSignatureForSelector, respondsToSelector方法,将方法的执行转发给weak target。当然我们自己实现一个CAAnimationDelegateWrapper类,内含CAAnimationDelegate的两个方法,弱引用真正的CAAnimationDelegate,也是可以的。

当然我们实现了自己的CAAnimationDelegateWrapper类:

CAAnimation-strong-delegate.png CAAnimation-weak-delegate.png

4.2 参数配置

无论是使用动画数组还是类键帧实现的话,文章开始处的表格就是对动画数组的配置,包括动画时间,动画进度比例,和动画速度三大参数。