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

图1 左为沙化动画,右为复原动画
响指动画
Google的方法是利用了48帧合成的一张Sprite图进行动画的:

图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()
- //1
- masksToBounds = true
- contentsGravity = CALayerContentsGravity.left
- contents = spriteSheetImage.cgImage
- bounds.size = spriteFrameSize
- //2
- let frameCount = Int(spriteSheetImage.size.width / spriteFrameSize.width)
- for frameIndex in 0.. animationValues.append(CGFloat(frameIndex) / CGFloat(frameCount))
- }
- }
-
- func play() {
- let spriteKeyframeAnimation = CAKeyframeAnimation(keyPath: "contentsRect.origin.x")
- spriteKeyframeAnimation.values = animationValues
- spriteKeyframeAnimation.duration = 2.0
- spriteKeyframeAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
- //3
- spriteKeyframeAnimation.calculationMode = CAAnimationCalculationMode.discrete
- add(spriteKeyframeAnimation, forKey: "spriteKeyframeAnimation")
- }
- }
- //1: masksToBounds = true和contentsGravity = CALayerContentsGravity.left是为了当前只显示Sprite图的第一幅画面
- //2: 根据Sprite图大小和每幅画面的大小计算出画面数量,预先计算出每幅画面的contentsRect.origin.x偏移量
- //3: 这里是关键,指定关键帧动画的calculationMode为discrete确保关键帧动画依次使用values中指定的关键帧值进行变化,而不是默认情况下采用线性插值进行过渡,来个对比图可能比较容易理解:
 
图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
- }
- //1
- let colorSpace = CGColorSpaceCreateDeviceRGB()
- let width = inputCGImage.width
- let height = inputCGImage.height
- let bytesPerPixel = 4
- let bitsPerComponent = 8
- let bytesPerRow = bytesPerPixel * width
- let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Little.rawValue
-
- guard 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)
- //2
- let imagesCount = 32
- var framePixels = Array(repeating: Array(repeating: UInt32(0), count: width * height), count: imagesCount)
- for column in 0.. for row in 0.. let offset = row * width + column
- //3
- for _ 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]
- }
- }
- }
- //4
- for 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个元素之一的相同位置。随机方法有点特别,原始图片左边的像素只会分配到前几张图片,而原始图片右边的像素只会分配到后几张。

图4 上部分为原始图片,下部分为像素分配后的32张图片依次显示效果
- //3: 这里循环2次将像素分配两次,可能 Google 觉得只分配一遍会造成像素比较稀疏。个人认为在移动端,只要一遍就好了。
- //4: 创建32张图片并返回
添加动画
(编辑:西安站长网)
【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!
|