iOS 动画 - 窗景篇(三·完结)

这篇文章是系列文章的第三篇。

看过上一篇文章的朋友,已经知道标题中的“景”指代 view,“窗”指代 view.mask,窗景篇就是在梳理 mask 及 mask 动画。如果你还不熟悉 iOS 的 mask,建议先看一下第一篇

前两篇我们介绍了 mask、mask 动画的一些用法。

这一篇作为收尾,我们来实现一个效果练练手,
也借这个效果,让大家回忆起一个简单的道理:复杂的效果,可以等价于简单效果的组合。

一、效果

这个效果如下面的动图所示:

我们截取比较有代表性的一帧,如下图所示:

从图中可以看到,波浪由两种颜色组成,各部分颜色不同。

这个效果看上去有点复杂,如果不熟悉 mask,可能一时半会儿没有思路。
但看过前两篇的朋友,可能已经暗暗在想,是窗在动?还是景在动?会不会有多套窗景?

那么接下来,我们先通过一个简单的效果来看一下原理。

注:波浪动画的实现和本文关系不大,本文不会讲述。
网上有成熟的波浪动画的教程,本文 demo 中 WaveView 类也有简要的注释。

二、一个简单的效果

这个效果如下图所示:

从图中可以看到,一张黑白图片上有一部分是彩色的。
我们当然可以通过图像处理来实现这个效果,但在本文中,我们还是使用 mask 的方式来实现。

我们回忆一下前文中的一张图:

通过对frontView 添加一个圆 mask,就形成了图中的效果。

也许有的朋友已经想到,把上图中 backView 的图换成和 frontView 一样的黑白图片,不就是本例的效果吗,如下图所示:

也就是说,这个效果看上去是黑白图片上有一部分变成了彩色,
但其实只是两张内容一样的图片重叠,黑白图片在后,彩色图片在前,而前方的彩色图片,被施加了圆形的 mask。

这个效果很简单,但能让我们意识到一件事:看上去是一张图,其实可能是多张图组合而成。

既然如此,那本文的波浪动画中,各部分颜色不同的波浪,真的只是一个波浪吗?

没错,本例中的波浪,也是多个波浪组合而成的,接下来,我们就详细的看一看。

三、多景合一

和黑白、彩色图片重叠效果一样,多色波浪也是由一组重叠的波浪 view 组合而成。

每层 view 的波浪只有一种颜色,各层 view 的波浪动画都一致,对于每一帧,所有波浪都是完全重合的。

每个波浪 view 都有自己的 mask,在 mask 们 的控制下,每层波浪 view 只显示了波浪的一部分,我们看到的多色波浪,就是各层波浪 view 可见部分的组合。

我们取两层来示意一下,如下图所示:

从图中可以看到,白底红波浪 view 有个上半圆 mask、黑底蓝波浪 view 有个下半圆 mask,两个 view 的波浪进度完全一致,组合之后就成了最右边的的效果。

捅破了这层窗户纸后,其实原理就是如此简单。

知道了原理,其实大家可以自己去动手去实现效果了,
当然,如果不着急的话,那咱们一块把流程走一遍。

四、创建4层 view

这一步很简单,创建 frame 完全一致的 4 层 view,本例中使用 WaveView 作为 view,
根据需要,设置不同的背景色(黑、白)和波浪色(红、蓝)。

这一步后,4 层波浪 view 如图所示:

示意代码如下:

// A3WaveViewController
private func addSubViews() {
    // 上大半圆 view
    view.addSubview(bigTopView)
    // 上小半圆 view
    view.addSubview(smallTopView)
    // 下大半圆 view
    view.addSubview(bigBottomView)
    // 下小半圆 view
    view.addSubview(smallBottomView)
}

五、为 4 层 view 设置 mask

这一步就是做出合适的 mask 。
本例中使用 HalfCircleView 作为 mask,分别为各层 view 设置两个大半圆和两个小半圆的 mask。

这一步后,4层 view 在 mask 的影响下如下图所示,大家可以和前文图中的 4个 view 对照着看:

示意代码如下:

// A3WaveViewController
private func makeLayout() {
    // 设置4个 view 的 mask
    
    // 大上半圆
    let width: CGFloat = 200
    let marginX = (UIScreen.main.bounds.width - width) / 2
    bigTopView.frame = CGRect(x: marginX, y: 200, width: width, height: 200)
    // 大上半圆 mask
    let bigTopMask = HalfCircleView()
    bigTopMask.part = .top
    bigTopMask.frame = CGRect(x: 0, y: 0, width: bigTopView.bounds.width, height: bigTopView.bounds.height / 2)
    bigTopView.mask = bigTopMask
    
    // 小上半圆(半径是大半圆的一半)
    smallTopView.frame = bigTopView.frame
    // 小上半圆 mask
    let smallTopMask = HalfCircleView()
    smallTopMask.part = .top
    smallTopMask.frame = CGRect(x: smallTopView.bounds.width / 4,
                                y: smallTopView.bounds.height / 4,
                                width: smallTopView.bounds.width / 2,
                                height: smallTopView.bounds.height / 4)
    smallTopView.mask = smallTopMask
    
    // 大下半圆
    bigBottomView.frame = bigTopView.frame
    // 大下半圆 mask
    let bigBottomMask = HalfCircleView()
    bigBottomMask.part = .bottom
    bigBottomMask.frame = CGRect(x: 0,
                                 y: bigBottomView.bounds.height / 2,
                                 width: bigBottomView.bounds.width,
                                 height: bigBottomView.bounds.height / 2)
    bigBottomView.mask = bigBottomMask
    
    // 小下半圆
    smallBottomView.frame = bigBottomView.frame
    // 小下半圆 mask
    let smallBottomMask = HalfCircleView()
    smallBottomMask.part = .bottom
    smallBottomMask.frame = CGRect(x: smallBottomView.bounds.width / 4,
                                   y: smallBottomView.bounds.height / 2,
                                   width: smallBottomView.bounds.width / 2,
                                   height: smallBottomView.bounds.height / 4)
    smallBottomView.mask = smallBottomMask
}

六、让4层 view 一致地执行动画

为了让各层波浪动画完全一致,我们在外部启动一个 CADisplayLink,来同时改变 4 个波浪 view 的 progress(前文图中使用了 progress 为 0.5 时作为示例)。

也就实现了本篇开始的效果:

示意代码如下:

// A3WaveViewController
func start() {
    if let displayLink = displayLink {
        displayLink.invalidate()
        self.displayLink = nil
        progress = 0
    }
    // 启动 CADisplayLink
    let displayLink = CADisplayLink(target: WeakProxy(self), selector: #selector(work))
    displayLink.add(to: RunLoop.current, forMode: .common)
    self.displayLink = displayLink
}

@objc private func work() {
    if progress < 1 {
        progress += 0.0025
        progress = min(progress, 1)
    } else {
        progress = 0
    }
    // CADisplayLink 回调时,设置4个波浪 view 的 progress
    bigTopView.progress = progress
    bigBottomView.progress = progress
    smallTopView.progress = progress
    smallBottomView.progress = progress
}

至此,效果就完成了。

当然,你可以用自己的动画 view 替换掉 WaveView、改变 view 的个数、使用其他的 mask等,
来实现自己想要的拼接效果。

当然,这种动画方式的性能未评估,也许不适合用在生产环境。
这个例子更多地是想让大家回忆起一个简单的道理:

复杂的动画,可以等价为简单动画的组合;只要还觉得复杂,就可以继续拆分。

如果你想要拆分,觉得不知道从何处下手,那么可以这么尝试:

只要某一部分的动画和其他部分有区别,就可以拆分。

简单的动画一旦实现了,组合起来了就完成了复杂的动画。

组合是我们常用的方法,比如下图的双波浪:

我们当然可以直接写个双波浪的类,但也可以组合两个波浪 view 来形成双波浪。

组合看上去有点傻,不过也有它的优势,
比如我们想复用的效果很复杂,难以改写,或者我们根本没有该效果的源码时,组合可能就是最简单的方式。

尾声

本系列只是展示了常见的 mask 效果,挂一漏万,毕竟窗无限,景无限,组合无限,效果无限。

如果你发现了有意思的 mask 动画,欢迎在评论区留言,愿我们共同进步。

本文所有示例,在 GitHub 库 里都有完整的代码。

本系列至此完结,感谢您的阅读。

传送门

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