FBShimmering文字闪烁效果实现解析

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的使用。
这里可以参考

搞清楚这两点之后,我们先设置_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有一定的了解,代码中不论是代码的技巧还是整体动画的设计逻辑都值得深入研究和学习。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容

  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥ios动画全貌。在这里你可以看...
    每天刷两次牙阅读 8,455评论 6 30
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥iOS动画全貌。在这里你可以看...
    F麦子阅读 5,089评论 5 13
  • 转载:http://www.cnblogs.com/jingdizhiwa/p/5601240.html 1.ge...
    F麦子阅读 1,528评论 0 1
  • Core Animation其实是一个令人误解的命名。你可能认为它只是用来做动画的,但实际上它是从一个叫做Laye...
    小猫仔阅读 3,676评论 1 4
  • 一、CAShapelayer 我们知道可以不使用图片情况下利用CGpath去构建任意形状的阴影。其实我们也可...
    小猫仔阅读 1,413评论 0 5