iOS设备解锁时的文字闪烁效果非常得好看,在Github上搜索了下,发现facebook的FBShimmering完美得还原了这个效果,在这里解析下他是怎么实现这个效果的。
先简单讲一下用法,将文件导入到工程后,头文件引用FBShimmering.h
。
_shimmeringView = [[FBShimmeringView alloc] init];
_shimmeringView.shimmering = YES;
_shimmeringView.shimmeringBeginFadeDuration = 0.3;
_shimmeringView.shimmeringOpacity = 0.3;
[self.view addSubview:_shimmeringView];
_logoLabel = [[UILabel alloc] initWithFrame:_shimmeringView.bounds];
_logoLabel.text = @"这是测试文字";
_logoLabel.font = [UIFont fontWithName:@"HelveticaNeue-UltraLight" size:30.0];
_logoLabel.textColor = [UIColor whiteColor];
_logoLabel.textAlignment = NSTextAlignmentCenter;
_logoLabel.backgroundColor = [UIColor clearColor];
_shimmeringView.contentView = _logoLabel;
然后就可以达到文字闪烁这种非常酷炫的效果了
实现原理
要理解原理首先要理解maskLayer的作用和继承与Calyer的CAGradientLayer的使用。
这里可以参考
- iOS layer.mask属性用法
- CAGradientLayer的一些属性解析
搞清楚这两点之后,我们先设置_logoLabel.backgroundColor = [UIColor whiteColor]; _logoLabel.textColor = [UIColor redColor];
看看会有什么效果
可以比较清楚得看到有一个白色渐变填充的图层从底下扫过,这个图层就是CAGradientLayer。
看到这个就能明白实现的原理是用CAGradientLayer生成一个渐变图层,并将这个图层设置为label的maskLayer,并给这个图层添加了循环动画,从视觉上看到的是文字在闪烁,实际上是图层的位移动画。
核心代码分析
下面这一段是设置maskLayer大小和位置的代码
- (void)_updateMaskLayout
{
// Everything outside the mask layer is hidden, so we need to create a mask long enough for the shimmered layer to be always covered by the mask.
CGFloat length = 0.0f;
if (_shimmeringDirection == FBShimmerDirectionDown ||
_shimmeringDirection == FBShimmerDirectionUp) {
length = CGRectGetHeight(_contentLayer.bounds);
} else {
length = CGRectGetWidth(_contentLayer.bounds);
}
if (0 == length) {
return;
}
// extra distance for the gradient to travel during the pause.
CGFloat extraDistance = length + _shimmeringSpeed * _shimmeringPauseDuration;
// compute how far the shimmering goes
CGFloat fullShimmerLength = length * 3.0f + extraDistance;
CGFloat travelDistance = length * 2.0f + extraDistance;
// position the gradient for the desired width
CGFloat highlightOutsideLength = (1.0 - _shimmeringHighlightLength) / 2.0;
_maskLayer.locations = @[@(highlightOutsideLength),
@(0.5),
@(1.0 - highlightOutsideLength)];
CGFloat startPoint = (length + extraDistance) / fullShimmerLength;
CGFloat endPoint = travelDistance / fullShimmerLength;
// position for the start of the animation
_maskLayer.anchorPoint = CGPointZero;
if (_shimmeringDirection == FBShimmerDirectionDown ||
_shimmeringDirection == FBShimmerDirectionUp) {
_maskLayer.startPoint = CGPointMake(0.0, startPoint);
_maskLayer.endPoint = CGPointMake(0.0, endPoint);
_maskLayer.position = CGPointMake(0.0, -travelDistance);
_maskLayer.bounds = CGRectMake(0.0, 0.0, CGRectGetWidth(_contentLayer.bounds), fullShimmerLength);
} else {
_maskLayer.startPoint = CGPointMake(startPoint, 0.0);
_maskLayer.endPoint = CGPointMake(endPoint, 0.0);
_maskLayer.position = CGPointMake(-travelDistance, 0.0);
_maskLayer.bounds = CGRectMake(0.0, 0.0, fullShimmerLength, CGRectGetHeight(_contentLayer.bounds));
}
}
分析这段就可以看出maskLayer的整体组成部分如下图所示
下面这段是动画播放的核心代码
- (void)_updateShimmering
{
// 创建masklayer假如需要
[self _createMaskIfNeeded];
//如果设定不播放动画并且maskLayer为空则return
if (!_shimmering && !_maskLayer) {
return;
}
//进行布局
[self layoutIfNeeded];
//判断动画是否已失效
BOOL disableActions = [CATransaction disableActions];
if (!_shimmering) {
if (disableActions) {
// 假如不播放动画且动画已失效,清除maskLayer
[self _clearMask];
} else {
//不需要播放动画,但动画还在运作,停止动画
CFTimeInterval slideEndTime = 0;
//根据key获取位移动画
CAAnimation *slideAnimation = [_maskLayer animationForKey:kFBShimmerSlideAnimationKey];
if (slideAnimation != nil) {
// 获取动画已播放时间
CFTimeInterval now = CACurrentMediaTime();
CFTimeInterval slideTotalDuration = now - slideAnimation.beginTime;
// 根据已播放时间和总体时间求出剩余时间
CFTimeInterval slideTimeOffset = fmod(slideTotalDuration, slideAnimation.duration);
//创建结束动画
CAAnimation *finishAnimation = shimmer_slide_finish(slideAnimation);
// 设定结束动画的开始时间
finishAnimation.beginTime = now - slideTimeOffset;
// 设定结束时间,并添加结束动画
slideEndTime = finishAnimation.beginTime + slideAnimation.duration;
[_maskLayer addAnimation:finishAnimation forKey:kFBShimmerSlideAnimationKey];
}
// 在结束动画播放完毕后播放淡入动画(这里需要注意的是,淡入淡出动画都是对maskLayer的子layer fadeLayer起作用)
CABasicAnimation *fadeInAnimation = fade_animation(_maskLayer.fadeLayer, 1.0, _shimmeringEndFadeDuration);
fadeInAnimation.delegate = self;
[fadeInAnimation setValue:@YES forKey:kFBEndFadeAnimationKey];
fadeInAnimation.beginTime = slideEndTime;
[_maskLayer.fadeLayer addAnimation:fadeInAnimation forKey:kFBFadeAnimationKey];
// 淡入淡出动画的开始时间为位移动画的结束时间(这一步只做数据展示,对整体动画没影响)
_shimmeringFadeTime = slideEndTime;
}
} else {
// 添加淡出动画
CABasicAnimation *fadeOutAnimation = nil;
if (_shimmeringBeginFadeDuration > 0.0 && !disableActions) {
fadeOutAnimation = fade_animation(_maskLayer.fadeLayer, 0.0, _shimmeringBeginFadeDuration);
[_maskLayer.fadeLayer addAnimation:fadeOutAnimation forKey:kFBFadeAnimationKey];
} else {
BOOL innerDisableActions = [CATransaction disableActions];
[CATransaction setDisableActions:YES];
_maskLayer.fadeLayer.opacity = 0.0;
[_maskLayer.fadeLayer removeAllAnimations];
[CATransaction setDisableActions:innerDisableActions];
}
// 开始位移动画
CAAnimation *slideAnimation = [_maskLayer animationForKey:kFBShimmerSlideAnimationKey];
// 设置位移动画时间间隔
CGFloat length = 0.0f;
if (_shimmeringDirection == FBShimmerDirectionDown ||
_shimmeringDirection == FBShimmerDirectionUp) {
length = CGRectGetHeight(_contentLayer.bounds);
} else {
length = CGRectGetWidth(_contentLayer.bounds);
}
CFTimeInterval animationDuration = (length / _shimmeringSpeed) + _shimmeringPauseDuration;
if (slideAnimation != nil) {
// 确保已存在的位移动画的播放次数
[_maskLayer addAnimation:shimmer_slide_repeat(slideAnimation, animationDuration, _shimmeringDirection) forKey:kFBShimmerSlideAnimationKey];
} else {
// 添加位移动画
slideAnimation = shimmer_slide_animation(animationDuration, _shimmeringDirection);
slideAnimation.fillMode = kCAFillModeForwards;
slideAnimation.removedOnCompletion = NO;
if (_shimmeringBeginTime == FBShimmerDefaultBeginTime) {
_shimmeringBeginTime = CACurrentMediaTime() + fadeOutAnimation.duration;
}
slideAnimation.beginTime = _shimmeringBeginTime;
[_maskLayer addAnimation:slideAnimation forKey:kFBShimmerSlideAnimationKey];
}
}
}
总结
想要理解原理需要对layer和CAAnimation有一定的了解,代码中不论是代码的技巧还是整体动画的设计逻辑都值得深入研究和学习。