动画的核心:LNDanmakuTrackController

  • 弹幕框架本质上来说就是一种辅助使用者做动画的工具:使用者给出自己需要放到屏幕上的视图,弹幕框架为目标视图运行动画,让其可以在屏幕中动态地展示出来;动画的核心就是弹幕轨道。

  • 轨道组件在这里被划分为了Track/TrackController两部分,Track更像是一种动画规则,TrackController才是动画真正的执行人,一个方便理解这两者关系的方法是:TrackController是干活的人,Track就是工具;如果Track是剪刀,TrackController就是用剪刀的人,剪什么,怎么剪都是TrackController做主,这样做的意义是同一个剪刀可以被不同的人用,同一个人也可以用其他工具。所以我们通常讲的轨道在这里都是指一个TrackController+Track的组合。

  • 我们已经介绍过Clock封装了CADisplayLink,并不断给出那些消逝了的小段时间片回调,通过这些回调,我们会不断从每个弹幕个体(Attributes)中扣除他们的剩余时间,就像人的生命一样,当他们的时间被扣成0时,就意味着这个弹幕的播放周期结束了。而在弹幕生命的进度不断前进的过程中,TrackController总能根据Track提供的动画状态与时间进度的映射,并更新自己包含的所有弹幕动画状态。当一个弹幕播放器中所有轨道都被同一个Clock驱动时,所有轨道以相同时间速率运行。

Track

以一个最为普遍的横向轨道为例,LNDanmakuHorizontalMoveTrack属性如下:

@interface LNDanmakuHorizontalMoveTrack : LNDanmakuAbstractTrack
@property (nonatomic, assign) CGPoint startPosition;
@property (nonatomic, assign) CGFloat width;
@end

一个横向轨道包含两个属性:起始点和持续的长度,我们可以根据这两个属性在任意一个视图上划出一条线段,当一个弹幕被放置到这个轨道上后,会从这个线段的右侧移动到左侧,这个过程持续的时间就是这条弹幕Attributes内规定的totalAliveTime。
在这个过程中Track提供了弹幕更新函数:

@implementation LNDanmakuHorizontalMoveTrack
- (void)updateAttributes:(LNDanmakuAbstractAttributes *)attributes
{
    [super updateAttributes:attributes];
    float percent = attributes.currentPercent;
    CGFloat totalDistance = self.width + attributes.size.width;
    CGFloat currentDistance = totalDistance * percent;
    CGFloat currentX = self.startPosition.x + self.width - currentDistance;
    CGFloat currentY = self.startPosition.y - attributes.size.height/2.f;
    attributes.position = CGPointMake(currentX, currentY);
}
@end

这个函数的计算过程:

  1. 对于任意一个传进来的弹幕attributes,我们首先计算出了这条弹幕需要移动的总距离,这个距离是 (屏幕的宽度+弹幕的宽度),因为弹幕出现和消失条件分别是 左侧首次出现 和 右侧首次消失。
  2. 根据总距离和弹幕已经更新好的percent(Attributes文中提到过这个percent,意思是已存活时间和总存活时间之比),计算出了这条弹幕当前应当移动的距离。
  3. 从弹幕的右侧起点累加这当前应当移动的距离就是弹幕当前应在的位置。
  4. 更新Attributes中的空间信息。
    以上就是条形轨道的Track对一条弹幕的更新过程。

TrackController

同样以条形轨道为例,条形轨道继承自一种Base移动轨道,因为所有的移动类型轨道都需要进行追击问题处理来避免弹幕重叠,因此我抽象出了Base移动轨道来让那些类似的移动轨道继承,之后的sin形状轨道或者圆形轨道都是继承自这个基类;此处我们暂先忽略这个基类额外的特性,只考虑它的刷新过程是如何作用在弹幕上的:

@interface LNDanmakuHorizontalMoveTrackController : LNDanmakuBaseMoveTrackController
@property (nonatomic, strong, readonly) LNDanmakuHorizontalMoveTrack *horizontalTrack;
@end
@interface LNDanmakuBaseMoveTrackController : LNDanmakuAbstractTrackController
@property (nonatomic, assign) NSTimeInterval spaceTimeInterval;
@end

轨道属性 spaceTimeInterval是两个弹幕之间的最小时间间隔,前文提到过了,LNDanmakuMaster的所有控制都以时间为唯一准则进行处理,所以这个间隔也是以时间为单位计算的,不同速弹幕之间的时间间隔一致。

@implementation LNDanmakuBaseMoveTrackController
- (void)update:(NSTimeInterval)elapsingTime
{
    if (elapsingTime < 0.f) {
        return;
    }
    NSMutableSet<LNDanmakuAbstractAttributes *> *shouldUnloadAttributes = [[NSMutableSet alloc] init];
    for (LNDanmakuAbstractAttributes *attributes in self.currentAttributesMSet) {
        attributes.currentAliveTime += elapsingTime;
        NSTimeInterval restAliveTime = attributes.totalAliveTime - attributes.currentAliveTime;
        if (restAliveTime <= 0) {
            [shouldUnloadAttributes addObject:attributes];
        } else {
            [self.track updateAttributes:attributes];
        }
    }
    
    for (LNDanmakuAbstractAttributes *attributes in shouldUnloadAttributes) {
        [self unloadAttributes:attributes];
    }
    [self checkCanLoadCandidate];
}
@end

这个函数是TrackController精简后的更新方法,其大体流程如下:

  • 轨道控制器接收到了一小段流逝的时间(通常是0.0167)。
  • 遍历当前的轨道上的所有弹幕,为每条弹幕的存活时间加上这段已经流逝的时间。
  • 如果在增加这段时间后,这条弹幕的存活时间已经超过了他的总存活时间,这条弹幕会被放到一个卸载集合中,并在函数的尾部被一起卸载掉。
  • 如果在增加这段时间后,这条弹幕的存活时间没有达到总存活时间,TrackController调用自己的Track更新这条弹幕的位置。

以上就是一个TrackController配合一个Track更新弹幕的过程,因为抽象出了Track,所以这样一个TrackController可以搭配多种轨道来实现不同的运动效果,例如条形轨道和圆形轨道内部分别是这样实现的:

@interface LNDanmakuHorizontalMoveTrackController : LNDanmakuBaseMoveTrackController
@property (nonatomic, strong) LNDanmakuHorizontalMoveTrack *horizontalTrack;
@end

@implementation LNDanmakuHorizontalMoveTrackController
- (LNDanmakuAbstractTrack *)track
{
    return self.horizontalTrack;
}
- (LNDanmakuHorizontalMoveTrack *)horizontalTrack
{
    if (!_horizontalTrack) {
        _horizontalTrack = [[LNDanmakuHorizontalMoveTrack alloc] init];
    }
    return _horizontalTrack;
}
@end
@interface LNDanmakuCircleTrackController : LNDanmakuBaseMoveTrackController
@property (nonatomic, strong, readonly) LNDanmakuCircleTrack *circleTrack;
@end

@implementation LNDanmakuCircleTrackController
- (LNDanmakuAbstractTrack *)track
{
    return self.circleTrack;
}
- (LNDanmakuCircleTrack *)circleTrack
{
    if (!_circleTrack) {
        _circleTrack = [[LNDanmakuCircleTrack alloc] init];
    }
    return _circleTrack;
}
@end

其实他们都继承自移动轨道基类,只是在track方法中返回了不同类型的Track而已,表现出来的动画却相差甚远,这种组合方式让TrackController和Track都得到了复用:


yuzLeP.gif

其他的轨道

LNDanmakuMaster提供了3种BaseTrackController:

  • 移动类型:LNDanmakuBaseMoveTrackController
  • 定点类型:LNDanmakuBaseStableTrackController
  • 自由类型:LNDanmakuBaseFreeTrackController
    其中前两种应该可以满足目前绝大多数弹幕需求了。

LNDanmaku也原生实现了6种TrackController+Track组合:

  • 水平移动:LNDanmakuHorizontalMoveTrackController(最常见的那种)
  • 定点轨道:LNDanmakuStableTrackController(类似B站那种中间一列,这个可以设置在任意点)
  • 心形轨道:LNDanmakuChristinaTrackController(一个心形的图案)
  • 圆形轨道:LNDanmakuCircleTrackController(一个圆形的图案)
  • pop动画轨道:LNDanmakuPopTrackController(用衰减正弦曲线模拟的spring动画,通常使用一组数值已经是计算好的静态数值,所以不需要担心计算带来的性能问题)
  • 波浪轨道:LNDanmakuSinTrackController(sin函数的形状,与条形十分类似,加上了上下波动和弹幕的旋转)

因此,通过更改Attributes提供的size、position、opacity和transform属性,作出的效果已经能够涵盖绝大部分使用场景了;即便使用者有非常特殊的需求,也可以通过继承Attributes、自定义TrackController实现,只要是继承自Abstract类的组件都可以接入到整个框架中来。

最后,Christina轨道的由来:我在编写代码的时候突然想起了2016年张宇老师的一堂课里,普及了一个关于笛卡尔的知识点:“你们知道百岁山广告里面那个乞丐是谁吗?是的,那个就是笛卡尔”,之后声情并茂地给我们讲完了整个故事,虽然后来我百度了一下,那个故事可能并不是真实的,但我仍然觉得很浪漫,或许我也可以用代码实现一些浪漫的东西,我这样想着,Christina就是那个公主的名字。

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

推荐阅读更多精彩内容