在iOS中實(shí)現(xiàn)谷歌滅霸彩蛋的完整示例
前言
最近上映的復(fù)仇者聯(lián)盟4據(jù)說沒有片尾彩蛋,不過谷歌幫我們做了。只要在谷歌搜索滅霸,在結(jié)果的右側(cè)點(diǎn)擊無限手套,你將化身為滅霸,其中一半的搜索結(jié)果會(huì)化為灰燼消失...那么這么酷的動(dòng)畫在iOS中可以實(shí)現(xiàn)嗎?答案是肯定的。整個(gè)動(dòng)畫主要包含以下幾部分:響指動(dòng)畫、沙化消失以及背景音效和復(fù)原動(dòng)畫,讓我們分別來看看如何實(shí)現(xiàn)。
圖1 左為沙化動(dòng)畫,右為復(fù)原動(dòng)畫
響指動(dòng)畫
Google的方法是利用了48幀合成的一張Sprite圖進(jìn)行動(dòng)畫的:
圖2 響指Sprite圖片
原始圖片中48幅全部排成一行,這里為了顯示效果截成2行
iOS 中通過這張圖片來實(shí)現(xiàn)動(dòng)畫并不難。CALayer有一個(gè)屬性contentsRect,通過它可以控制內(nèi)容顯示的區(qū)域,而且是Animateable的。它的類型是CGRect,默認(rèn)值為(x:0.0, y:0.0, width:1.0, height:1.0),它的單位不是常見的Point,而是單位坐標(biāo)空間,所以默認(rèn)值顯示100%的內(nèi)容區(qū)域。新建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..<frameCount { 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是為了當(dāng)前只顯示Sprite圖的第一幅畫面
//2: 根據(jù)Sprite圖大小和每幅畫面的大小計(jì)算出畫面數(shù)量,預(yù)先計(jì)算出每幅畫面的contentsRect.origin.x偏移量
//3: 這里是關(guān)鍵,指定關(guān)鍵幀動(dòng)畫的calculationMode為discrete確保關(guān)鍵幀動(dòng)畫依次使用values中指定的關(guān)鍵幀值進(jìn)行變化,而不是默認(rèn)情況下采用線性插值進(jìn)行過渡,來個(gè)對比圖可能比較容易理解:
圖3 左邊為離散模式,右邊為默認(rèn)的線性模式
沙化消失
這個(gè)效果是整個(gè)動(dòng)畫較難的部分,Google的實(shí)現(xiàn)很巧妙,它將需要沙化消失內(nèi)容的html通過html2canvas渲染成canvas,然后將其轉(zhuǎn)換為圖片后的每一個(gè)像素點(diǎn)隨機(jī)地分配到32塊canvas中,最后對每塊畫布進(jìn)行隨機(jī)地移動(dòng)和旋轉(zhuǎn)即達(dá)到了沙化消失的效果。
像素處理
新建自定義視圖 DustEffectView,這個(gè)視圖的作用是用來接收圖片并將其進(jìn)行沙化消失。首先創(chuàng)建函數(shù)createDustImages,它將一張圖片的像素隨機(jī)分配到32張等待動(dòng)畫的圖片上:
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..<width { for row in 0..<height { 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: 根據(jù)指定格式創(chuàng)建位圖上下文,然后將輸入的圖片繪制上去之后獲取其像素?cái)?shù)據(jù)
//2: 創(chuàng)建像素二維數(shù)組,遍歷輸入圖片每個(gè)像素,將其隨機(jī)分配到數(shù)組32個(gè)元素之一的相同位置。隨機(jī)方法有點(diǎn)特別,原始圖片左邊的像素只會(huì)分配到前幾張圖片,而原始圖片右邊的像素只會(huì)分配到后幾張。
圖4 上部分為原始圖片,下部分為像素分配后的32張圖片依次顯示效果
//3: 這里循環(huán)2次將像素分配兩次,可能 Google 覺得只分配一遍會(huì)造成像素比較稀疏。個(gè)人認(rèn)為在移動(dòng)端,只要一遍就好了。
//4: 創(chuàng)建32張圖片并返回
添加動(dòng)畫
Google的實(shí)現(xiàn)是給canvas中css的transform屬性設(shè)置為rotate(deg) translate(px, px) rotate(deg),值都是隨機(jī)生成的。如果你對CSS的動(dòng)畫不熟悉,那你會(huì)覺得在iOS中只要添加三個(gè)CABasicAnimation然后將它們添加到AnimationGroup就好了嘛,實(shí)際上并沒有那么簡單... 因?yàn)镃SS的transform中后一個(gè)變換函數(shù)是基于前一個(gè)變換后的新transform坐標(biāo)系。假如某張圖片的動(dòng)畫樣式是這樣的:rotate(90deg) translate(0px, 100px) rotate(-90deg) 直覺告訴我應(yīng)該是旋轉(zhuǎn)著向下移動(dòng)100px,然而在CSS中的元素是這么運(yùn)動(dòng)的:
圖5 CSS中transform多值動(dòng)畫
第一個(gè)rotate和translate決定了最終的位置和運(yùn)動(dòng)軌跡,至于第二個(gè)rotate作用,只是疊加第一個(gè)rotate的值作為最終的旋轉(zhuǎn)弧度,這里剛好為0也就是不旋轉(zhuǎn)。那么在iOS中該如何實(shí)現(xiàn)相似的運(yùn)動(dòng)軌跡呢?可以利用UIBezierPath, CAKeyframeAnimation的屬性path可以指定這個(gè)UIBezierPath為動(dòng)畫的運(yùn)動(dòng)軌跡。確定起點(diǎn)和實(shí)際終點(diǎn)作為貝塞爾曲線的起始點(diǎn)和終止點(diǎn),那么如何確定控制點(diǎn)?好像可以將“預(yù)想”的終點(diǎn)(下圖中的(0,-1))作為控制點(diǎn)。
圖6 將“預(yù)想”的終點(diǎn)作為控制點(diǎn)的貝塞爾曲線,看起來和CSS中的運(yùn)動(dòng)軌跡差不多
擴(kuò)展問題
通過文章中描述的方式生成的貝塞爾曲線是否與CSS中的動(dòng)畫軌跡完全一致呢?
現(xiàn)在可以給視圖添加動(dòng)畫了:
let layer = CALayer() layer.frame = bounds layer.contents = image.cgImage self.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.cgPath moveAnimation.calculationMode = .paced //3: let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation") rotateAnimation.toValue = radian1 + radian2 let fadeOutAnimation = CABasicAnimation(keyPath: "opacity") fadeOutAnimation.toValue = 0.0 let animationGroup = CAAnimationGroup() animationGroup.animations = [moveAnimation, rotateAnimation, fadeOutAnimation] animationGroup.duration = 1 //4: animationGroup.beginTime = CACurrentMediaTime() + 1.35 * Double(i) / Double(imagesCount) animationGroup.isRemovedOnCompletion = false animationGroup.fillMode = .forwards layer.add(animationGroup, forKey: nil)
//1: 實(shí)際的偏移量旋轉(zhuǎn)了radian1弧度,這個(gè)可以通過公式x' = x*cos(rad) - y*sin(rad), y' = y*cos(rad) + x*sin(rad)算出
//2: 創(chuàng)建UIBezierPath并關(guān)聯(lián)到CAKeyframeAnimation中
//3: 兩個(gè)弧度疊加作為最終的旋轉(zhuǎn)弧度
//4: 設(shè)置CAAnimationGroup的開始時(shí)間,讓每層Layer的動(dòng)畫延遲開始
結(jié)尾
到這里,谷歌滅霸彩蛋中較復(fù)雜的技術(shù)點(diǎn)均已實(shí)現(xiàn)。如果您感興趣,完整的代碼(包含音效和復(fù)原動(dòng)畫)可以通過文章開頭的鏈接進(jìn)行查看,可以嘗試將沙化圖片的數(shù)量從32提高至更多,效果會(huì)越好,內(nèi)存也會(huì)消耗更多 :-D。
參考資料
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對腳本之家的支持。
相關(guān)文章
Objective C從遠(yuǎn)程url下載圖片方法匯總
本文給大家分享了2則使用Objective C從遠(yuǎn)程url下載圖片的方法,都是個(gè)人項(xiàng)目中使用的,匯總下推薦給大家,有需要的小伙伴可以參考下。2015-05-05iOS開發(fā)之運(yùn)動(dòng)事件和遠(yuǎn)程控制
在iOS中事件分為三類:觸摸事件:通過觸摸、手勢進(jìn)行觸發(fā)(例如手指點(diǎn)擊、縮放),運(yùn)動(dòng)事件:通過加速器進(jìn)行觸發(fā)(例如手機(jī)晃動(dòng)),遠(yuǎn)程控制事件:通過其他遠(yuǎn)程設(shè)備觸發(fā)(例如耳機(jī)控制按鈕)今天我們來詳細(xì)探討下運(yùn)動(dòng)事件和遠(yuǎn)程控制2016-04-04iOS實(shí)現(xiàn)一個(gè)可以在屏幕中自由移動(dòng)的按鈕
經(jīng)常在手機(jī)上看到可以隨意移動(dòng)的按鈕,正巧最近工作遇到了這個(gè)需求,索性就寫一個(gè),下面這篇文章主要給大家介紹了利用iOS實(shí)現(xiàn)一個(gè)可以在屏幕中自由移動(dòng)的按鈕的相關(guān)資料,需要的朋友可以參考借鑒,下面來一起看看吧。2017-07-07iOS利用MJRefresh實(shí)現(xiàn)自定義刷新動(dòng)畫效果
本文主要介紹iOS 利用MJRefresh實(shí)現(xiàn)自定義動(dòng)畫的上拉刷新下拉加載效果,要想實(shí)現(xiàn)此功能,首先得有一套load的圖片數(shù)組。接下來通過本文給大家詳解介紹實(shí)現(xiàn)過程2017-02-02