在 iOS 中实现谷歌灭霸彩蛋

2024-02-20 19:59
文章标签 实现 谷歌 ios 彩蛋 灭霸

本文主要是介绍在 iOS 中实现谷歌灭霸彩蛋,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

示例代码下载

最近上映的复仇者联盟4据说没有片尾彩蛋,不过谷歌帮我们做了。只要在谷歌搜索灭霸,在结果的右侧点击无限手套,你将化身为灭霸,其中一半的搜索结果会化为灰烬消失…那么这么酷的动画在iOS中可以实现吗?答案是肯定的。整个动画主要包含以下几部分:响指动画、沙化消失以及背景音效和复原动画,让我们分别来看看如何实现。

在 iOS 中实现谷歌灭霸彩蛋

在 iOS 中实现谷歌灭霸彩蛋

图1 上为沙化动画,下为复原动画

 

响指动画

 


Google的方法是利用了48帧合成的一张Sprite图进行动画的:

image.png

图2 响指Sprite图片

原始图片中48幅全部排成一行,这里为了显示效果截成2行

iOS 中通过这张图片来实现动画并不难。CALayer有一个属性contentsRect,通过它可以控制内容显示的区域,而且是Animateable的。它的类型是CGRect,默认值为(x:0.0, y:0.0, width:1.0, height:1.0),它的单位不是常见的Point,而是单位坐标空间,所以默认值显示100%的内容区域。新建Sprite播放视图层AnimatableSpriteLayer:

class AnimatableSpriteLayer: CALayer {private var animationValues = [CGFloat]()convenience init(spriteSheetImage: UIImage, spriteFrameSize: CGSize ) {self.init()//1masksToBounds = truecontentsGravity = CALayerContentsGravity.leftcontents = spriteSheetImage.cgImagebounds.size = spriteFrameSize//2let frameCount = Int(spriteSheetImage.size.width / spriteFrameSize.width)for frameIndex in 0..<frameCount {animationValues.append(CGFloat(frameIndex) / CGFloat(frameCount))}}func play() {let spriteKeyframeAnimation = CAKeyframeAnimation(keyPath: "contentsRect.origin.x")spriteKeyframeAnimation.values = animationValuesspriteKeyframeAnimation.duration = 2.0spriteKeyframeAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)//3spriteKeyframeAnimation.calculationMode = CAAnimationCalculationMode.discreteadd(spriteKeyframeAnimation, forKey: "spriteKeyframeAnimation")}
}

//1: masksToBounds = true和contentsGravity = CALayerContentsGravity.left是为了当前只显示Sprite图的第一幅画面

//2: 根据Sprite图大小和每幅画面的大小计算出画面数量,预先计算出每幅画面的contentsRect.origin.x偏移量

//3: 这里是关键,指定关键帧动画的calculationMode为discrete确保关键帧动画依次使用values中指定的关键帧值进行变化,而不是默认情况下采用线性插值进行过渡,来个对比图可能比较容易理解:

在 iOS 中实现谷歌灭霸彩蛋在 iOS 中实现谷歌灭霸彩蛋

图3 左边为离散模式,右边为默认的线性模式

 

沙化消失

 


这个效果是整个动画较难的部分,Google的实现很巧妙,它将需要沙化消失内容的html通过html2canvas渲染成canvas,然后将其转换为图片后的每一个像素点随机地分配到32块canvas中,最后对每块画布进行随机地移动和旋转即达到了沙化消失的效果。

像素处理

新建自定义视图 DustEffectView,这个视图的作用是用来接收图片并将其进行沙化消失。首先创建函数createDustImages,它将一张图片的像素随机分配到32张等待动画的图片上:

class DustEffectView: UIView {private func createDustImages(image: UIImage) -> [UIImage] {var result = [UIImage]()guard let inputCGImage = image.cgImage else {return result}//1let colorSpace = CGColorSpaceCreateDeviceRGB()let width = inputCGImage.widthlet height = inputCGImage.heightlet bytesPerPixel = 4let bitsPerComponent = 8let bytesPerRow = bytesPerPixel * widthlet bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Little.rawValueguard let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo) else {return result}context.draw(inputCGImage, in: CGRect(x: 0, y: 0, width: width, height: height))guard let buffer = context.data else {return result}let pixelBuffer = buffer.bindMemory(to: UInt32.self, capacity: width * height)//2let imagesCount = 32var framePixels = Array(repeating: Array(repeating: UInt32(0), count: width * height), count: imagesCount)for column in 0..<width {for row in 0..<height {let offset = row * width + column//3for _ in 0...1 { let factor = Double.random(in: 0..<1) + 2 * (Double(column)/Double(width))let index = Int(floor(Double(imagesCount) * ( factor / 3)))framePixels[index][offset] = pixelBuffer[offset]}}}//4for frame in framePixels {let data = UnsafeMutablePointer(mutating: frame)guard let context = CGContext(data: data, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo) else {continue}result.append(UIImage(cgImage: context.makeImage()!, scale: image.scale, orientation: image.imageOrientation))}return result}
}

//1: 根据指定格式创建位图上下文,然后将输入的图片绘制上去之后获取其像素数据

//2: 创建像素二维数组,遍历输入图片每个像素,将其随机分配到数组32个元素之一的相同位置。随机方法有点特别,原始图片左边的像素只会分配到前几张图片,而原始图片右边的像素只会分配到后几张。

在 iOS 中实现谷歌灭霸彩蛋

图4 上部分为原始图片,下部分为像素分配后的32张图片依次显示效果

//3: 这里循环2次将像素分配两次,可能 Google 觉得只分配一遍会造成像素比较稀疏。个人认为在移动端,只要一遍就好了。

//4: 创建32张图片并返回

添加动画

Google的实现是给canvas中css的transform属性设置为rotate(deg) translate(px, px) rotate(deg),值都是随机生成的。如果你对CSS的动画不熟悉,那你会觉得在iOS中只要添加三个CABasicAnimation然后将它们添加到AnimationGroup就好了嘛,实际上并没有那么简单… 因为CSS的transform中后一个变换函数是基于前一个变换后的新transform坐标系。假如某张图片的动画样式是这样的:rotate(90deg) translate(0px, 100px) rotate(-90deg) 直觉告诉我应该是旋转着向下移动100px,然而在CSS中的元素是这么运动的:

在 iOS 中实现谷歌灭霸彩蛋

图5 CSS中transform多值动画 

第一个rotate和translate决定了最终的位置和运动轨迹,至于第二个rotate作用,只是叠加第一个rotate的值作为最终的旋转弧度,这里刚好为0也就是不旋转。那么在iOS中该如何实现相似的运动轨迹呢?可以利用UIBezierPath, CAKeyframeAnimation的属性path可以指定这个UIBezierPath为动画的运动轨迹。确定起点和实际终点作为贝塞尔曲线的起始点和终止点,那么如何确定控制点?好像可以将“预想”的终点(下图中的(0,-1))作为控制点。

image.png

图6 将“预想”的终点作为控制点的贝塞尔曲线,看起来和CSS中的运动轨迹差不多 

扩展问题

通过文章中描述的方式生成的贝塞尔曲线是否与CSS中的动画轨迹完全一致呢?

现在可以给视图添加动画了:

    let layer = CALayer()layer.frame = boundslayer.contents = image.cgImageself.layer.addSublayer(layer)let centerX = Double(layer.position.x)let centerY = Double(layer.position.y)let radian1 = Double.pi / 12 * Double.random(in: -0.5..<0.5)let radian2 = Double.pi / 12 * Double.random(in: -0.5..<0.5)let random = Double.pi * 2 * Double.random(in: -0.5..<0.5)let transX = 60 * cos(random)let transY = 30 * sin(random)//1: // x' = x*cos(rad) - y*sin(rad)// y' = y*cos(rad) + x*sin(rad)let realTransX = transX * cos(radian1) - transY * sin(radian1)let realTransY = transY * cos(radian1) + transX * sin(radian1)let realEndPoint = CGPoint(x: centerX + realTransX, y: centerY + realTransY)let controlPoint = CGPoint(x: centerX + transX, y: centerY + transY)//2:let movePath = UIBezierPath()movePath.move(to: layer.position)movePath.addQuadCurve(to: realEndPoint, controlPoint: controlPoint)let moveAnimation = CAKeyframeAnimation(keyPath: "position")moveAnimation.path = movePath.cgPathmoveAnimation.calculationMode = .paced//3:                 let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")rotateAnimation.toValue = radian1 + radian2let fadeOutAnimation = CABasicAnimation(keyPath: "opacity")fadeOutAnimation.toValue = 0.0let animationGroup = CAAnimationGroup()animationGroup.animations = [moveAnimation, rotateAnimation, fadeOutAnimation]animationGroup.duration = 1//4:animationGroup.beginTime = CACurrentMediaTime() + 1.35 * Double(i) / Double(imagesCount)animationGroup.isRemovedOnCompletion = falseanimationGroup.fillMode = .forwardslayer.add(animationGroup, forKey: nil)

//1: 实际的偏移量旋转了radian1弧度,这个可以通过公式x' = x*cos(rad) – y*sin(rad), y' = y*cos(rad) + x*sin(rad)算出

//2: 创建UIBezierPath并关联到CAKeyframeAnimation中

//3: 两个弧度叠加作为最终的旋转弧度

//4: 设置CAAnimationGroup的开始时间,让每层Layer的动画延迟开始

结尾

 


到这里,谷歌灭霸彩蛋中较复杂的技术点均已实现。如果您感兴趣,完整的代码(包含音效和复原动画)可以通过文章开头的链接进行查看,可以尝试将沙化图片的数量从32提高至更多,效果会越好,内存也会消耗更多 :-D。

 

作者:potato04  作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要这是一个我的iOS交流群:869685378,不管你是小白还是大牛欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!

这篇关于在 iOS 中实现谷歌灭霸彩蛋的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/729375

相关文章

Oracle查询优化之高效实现仅查询前10条记录的方法与实践

《Oracle查询优化之高效实现仅查询前10条记录的方法与实践》:本文主要介绍Oracle查询优化之高效实现仅查询前10条记录的相关资料,包括使用ROWNUM、ROW_NUMBER()函数、FET... 目录1. 使用 ROWNUM 查询2. 使用 ROW_NUMBER() 函数3. 使用 FETCH FI

Python脚本实现自动删除C盘临时文件夹

《Python脚本实现自动删除C盘临时文件夹》在日常使用电脑的过程中,临时文件夹往往会积累大量的无用数据,占用宝贵的磁盘空间,下面我们就来看看Python如何通过脚本实现自动删除C盘临时文件夹吧... 目录一、准备工作二、python脚本编写三、脚本解析四、运行脚本五、案例演示六、注意事项七、总结在日常使用

Java实现Excel与HTML互转

《Java实现Excel与HTML互转》Excel是一种电子表格格式,而HTM则是一种用于创建网页的标记语言,虽然两者在用途上存在差异,但有时我们需要将数据从一种格式转换为另一种格式,下面我们就来看看... Excel是一种电子表格格式,广泛用于数据处理和分析,而HTM则是一种用于创建网页的标记语言。虽然两

Java中Springboot集成Kafka实现消息发送和接收功能

《Java中Springboot集成Kafka实现消息发送和接收功能》Kafka是一个高吞吐量的分布式发布-订阅消息系统,主要用于处理大规模数据流,它由生产者、消费者、主题、分区和代理等组件构成,Ka... 目录一、Kafka 简介二、Kafka 功能三、POM依赖四、配置文件五、生产者六、消费者一、Kaf

使用Python实现在Word中添加或删除超链接

《使用Python实现在Word中添加或删除超链接》在Word文档中,超链接是一种将文本或图像连接到其他文档、网页或同一文档中不同部分的功能,本文将为大家介绍一下Python如何实现在Word中添加或... 在Word文档中,超链接是一种将文本或图像连接到其他文档、网页或同一文档中不同部分的功能。通过添加超

windos server2022里的DFS配置的实现

《windosserver2022里的DFS配置的实现》DFS是WindowsServer操作系统提供的一种功能,用于在多台服务器上集中管理共享文件夹和文件的分布式存储解决方案,本文就来介绍一下wi... 目录什么是DFS?优势:应用场景:DFS配置步骤什么是DFS?DFS指的是分布式文件系统(Distr

NFS实现多服务器文件的共享的方法步骤

《NFS实现多服务器文件的共享的方法步骤》NFS允许网络中的计算机之间共享资源,客户端可以透明地读写远端NFS服务器上的文件,本文就来介绍一下NFS实现多服务器文件的共享的方法步骤,感兴趣的可以了解一... 目录一、简介二、部署1、准备1、服务端和客户端:安装nfs-utils2、服务端:创建共享目录3、服

C#使用yield关键字实现提升迭代性能与效率

《C#使用yield关键字实现提升迭代性能与效率》yield关键字在C#中简化了数据迭代的方式,实现了按需生成数据,自动维护迭代状态,本文主要来聊聊如何使用yield关键字实现提升迭代性能与效率,感兴... 目录前言传统迭代和yield迭代方式对比yield延迟加载按需获取数据yield break显式示迭

Python实现高效地读写大型文件

《Python实现高效地读写大型文件》Python如何读写的是大型文件,有没有什么方法来提高效率呢,这篇文章就来和大家聊聊如何在Python中高效地读写大型文件,需要的可以了解下... 目录一、逐行读取大型文件二、分块读取大型文件三、使用 mmap 模块进行内存映射文件操作(适用于大文件)四、使用 pand

python实现pdf转word和excel的示例代码

《python实现pdf转word和excel的示例代码》本文主要介绍了python实现pdf转word和excel的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价... 目录一、引言二、python编程1,PDF转Word2,PDF转Excel三、前端页面效果展示总结一