在 iOS 中實現谷歌滅霸彩蛋

示例代碼下載css

最近上映的復仇者聯盟4聽說沒有片尾彩蛋,不過谷歌幫咱們作了。只要在谷歌搜索滅霸,在結果的右側點擊無限手套,你將化身爲滅霸,其中一半的搜索結果會化爲灰燼消失...那麼這麼酷的動畫在iOS中能夠實現嗎?答案是確定的。整個動畫主要包含如下幾部分:響指動畫、沙化消失以及背景音效和復原動畫,讓咱們分別來看看如何實現。html

圖1 左爲沙化動畫,右爲復原動畫git

響指動畫

Google的方法是利用了48幀合成的一張Sprite圖進行動畫的: github

圖2 響指Sprite圖片canvas

原始圖片中48幅所有排成一行,這裏爲了顯示效果截成2行swift

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..<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 = truecontentsGravity = CALayerContentsGravity.left是爲了當前只顯示Sprite圖的第一幅畫面
//2: 根據Sprite圖大小和每幅畫面的大小計算出畫面數量,預先計算出每幅畫面的contentsRect.origin.x偏移量
//3: 這裏是關鍵,指定關鍵幀動畫的calculationModediscrete確保關鍵幀動畫依次使用values中指定的關鍵幀值進行變化,而不是默認狀況下采用線性插值進行過渡,來個對比圖可能比較容易理解:app

圖3 左邊爲離散模式,右邊爲默認的線性模式dom

沙化消失

這個效果是整個動畫較難的部分,Google的實現很巧妙,它將須要沙化消失內容的html經過html2canvas渲染成canvas,而後將其轉換爲圖片後的每個像素點隨機地分配到32canvas中,最後對每塊畫布進行隨機地移動和旋轉即達到了沙化消失的效果。函數

像素處理

新建自定義視圖 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..<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: 根據指定格式建立位圖上下文,而後將輸入的圖片繪製上去以後獲取其像素數據
//2: 建立像素二維數組,遍歷輸入圖片每一個像素,將其隨機分配到數組32個元素之一的相同位置。隨機方法有點特別,原始圖片左邊的像素只會分配到前幾張圖片,而原始圖片右邊的像素只會分配到後幾張。

圖4 上部分爲原始圖片,下部分爲像素分配後的32張圖片依次顯示效果

//3: 這裏循環2次將像素分配兩次,可能 Google 以爲只分配一遍會形成像素比較稀疏。我的認爲在移動端,只要一遍就行了。
//4: 建立32張圖片並返回

添加動畫

Google的實現是給canvascsstransform屬性設置爲rotate(deg) translate(px, px) rotate(deg),值都是隨機生成的。若是你對CSS的動畫不熟悉,那你會以爲在iOS中只要添加三個CABasicAnimation而後將它們添加到AnimationGroup就行了嘛,實際上並無那麼簡單... 由於CSStransform中後一個變換函數是基於前一個變換後的新transform座標系。假如某張圖片的動畫樣式是這樣的:rotate(90deg) translate(0px, 100px) rotate(-90deg) 直覺告訴我應該是旋轉着向下移動100px,然而在CSS中的元素是這麼運動的:

圖5 CSS中transform多值動畫

第一個rotatetranslate決定了最終的位置和運動軌跡,至於第二個rotate做用,只是疊加第一個rotate的值做爲最終的旋轉弧度,這裏恰好爲0也就是不旋轉。那麼在iOS中該如何實現類似的運動軌跡呢?能夠利用UIBezierPath, CAKeyframeAnimation的屬性path能夠指定這個UIBezierPath爲動畫的運動軌跡。肯定起點和實際終點做爲貝塞爾曲線的起始點和終止點,那麼如何肯定控制點?好像能夠將「預想」的終點(下圖中的(0,-1))做爲控制點。

圖6 將「預想」的終點做爲控制點的貝塞爾曲線,看起來和CSS中的運動軌跡差很少

擴展問題
經過文章中描述的方式生成的貝塞爾曲線是否與CSS中的動畫軌跡徹底一致呢?

如今能夠給視圖添加動畫了:

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: 實際的偏移量旋轉了radian1弧度,這個能夠經過公式x' = x*cos(rad) - y*sin(rad), y' = y*cos(rad) + x*sin(rad)算出
//2: 建立UIBezierPath並關聯到CAKeyframeAnimation
//3: 兩個弧度疊加做爲最終的旋轉弧度
//4: 設置CAAnimationGroup的開始時間,讓每層Layer的動畫延遲開始

結尾

到這裏,谷歌滅霸彩蛋中較複雜的技術點均已實現。若是您感興趣,完整的代碼(包含音效和復原動畫)能夠經過文章開頭的連接進行查看,能夠嘗試將沙化圖片的數量從32提升至更多,效果會越好,內存也會消耗更多 :-D。

參考資料

  1. www.calayer.com/core-animat…
  2. stackoverflow.com/questions/3…
  3. weibo.com/1727858283/…
相關文章
相關標籤/搜索