關于 iOS 中的動畫

蘋果中的動畫采用的是 "按需求播放" 這樣的形式, 即不需要自己計算許多參數, 只需要提供如何動畫的要求, 系統自動去計算相關的參數.

需要將動畫看作是用戶交互的一種反饋或提示, 而不是簡單的效果而已.

1 繪圖, 動畫和線程

繪圖和動畫是相輔相成的, 即當提供繪制指令后, 系統并不立即執行, 而是等到一個繪制時機統一執行, 這個時機稱為 redraw moment.

動畫和繪圖的執行是一個道理. 動畫擁有幀(frame)的概念, 即動畫是由一張一張的幀組成的.

蘋果將執行動畫的系統組件稱為 animation server.

動畫是在用戶和真實的屏幕顯示中間插入了一段"電影"畫面, 當動畫結束后, 這個電影畫面也就從屏幕上移除了, 然后恢復真實屏幕的顯示. 但用戶不會察覺到這點, 因為當動畫結束后, 真實屏幕上的繪制也會變為和動畫結束時的狀態一樣, 但結束狀態需要程序員來保證其正確性.

一個簡單 view 動畫流程如下:

  1. 將 view 由位置 1 移動到位置 2, 由于沒有到 redraw 時機, 故現在屏幕沒有任何變化.
  2. 提出一個動畫請求, 動畫的內容是 view 從位置 1 移動到位置 2. 由于沒有到 redraw 時機, 故屏幕也沒有任何變化.
  3. 系統將所有代碼執行完畢后, 出現空閑時機, 即 redraw 時機.
  4. 在 redraw 時機后, 系統將動畫進行播放.
  5. 動畫結束后, view 也和動畫的最后一幀的狀態一致.

需要對動畫過程有正確認識: 動畫只是在真實屏幕上的一層"電影"效果. 不過實際上并不是真的存在"電影"效果, 只是為了方便理解.

真實的情況是, 在進行動畫時, 并非現有的 layer 在進行動畫, 而是單獨的一個 presentation layer, 在這個圖層上顯示動畫的每一幀效果.

layer 的 presentation layer 可以通 presentation 方法來訪問, 而 presentation layer 對應的 layer 可以通過 presentation layer 的 model 屬性來獲取.

另外 Animation server 組件是自動在單獨線程執行的, 所以不用擔心線程管理問題. 但需要對自己的界面終態進行操作, 以符合動畫終態.

在動畫結束事件到達時, 可以在對應的方法中去開始下一個動畫, 或者是去做一些清理工作.

2 ImageView 和 Image 動畫

繪圖也是首先講的 Image 繪制, 這里就先來看 Image 的動畫.

給 UIImageView 的 animationImageshighlightedAnimationImages 屬性設置一個 UIImage 數組, 這個數組代表的就是動畫的每一幀, 然后向 UIImageView 發送 startAnimating 消息, 它就開始動畫. 動畫時根據 animationDuration 的設置來決定如何計算幀的出現時間. 默認情況下動畫是無限循環的, 可以通過 animationRepeatCount 來設置循環次數. 還可通過 stopAnimating 消息來結束動畫. 在動畫開始前和結束后, 它都是在顯示 image 屬性或 highlightedImage 屬性對應的圖片.

有一個技巧就是通過圖片上下文繪制若干張圖片保存到數組中, 然后將這個數組賦值給 UIImageView 的動畫圖片數組, 讓 UIImageView 來進行動畫.

3 View 動畫

所有的動畫本質上都是圖層動畫. 只是在系統中為 UIView 提供了一些屬性, 可以方便直接通過 UIView 進行動畫.

對 View 進行動畫的方式目前有三種:

  • 開始--提交方式: 很少使用
  • 動畫塊方式: 當前最常用, 除了需要重復進行固定次數循環動畫的情況.
  • 屬性動畫器: iOS 10 之后新增. 它不是來替代動畫塊的, 而是對動畫塊的擴展和補充. 這個應該會在未來慢慢推廣使用.

下面就來看一些動畫基礎.

對于 View 而言, 在動畫塊包裹中的所有內容, 只要是對可動畫屬性的修改, 就會生成動畫. 并且由于是直接在操作諸如位置等屬性, 故視圖的終態就和動畫終態是一致的. 但這里有一個問題, 如果使用約束的話, 約束沒有改變, 則在未來的任何時候, 如果重新布局, 則界面中的狀態會回到初態, 所以約束布局的情況下需要單獨在動畫時對約束進行處理, 即重新定義約束后, 再在動畫塊中調用 view 的 layoutIfNeeded 方法.

如果想要動畫塊中的某些可動畫屬性改變不會計入動畫, 則可以調用 UIView 的類方法 performWithoutAnimation, 將這些代碼寫到其中即可.

當進行重復動畫時, 指定重復次數需要一些技巧. 默認情況下 UIView 的動畫如果指定重復選項, 動畫是永遠重復的.

不過可以通過在動畫塊中通過 UIView 的類方法 setAnimationRepeatCount 設置次數. 即不指定 repeat 選項的情況下使用這個方法來指定重復次數.

彈性動畫的兩個參數:

  • Damping ratio: 取值0到1, 描述的是最終的震蕩效果, 值越小, 最終晃動越大, 0.8 是比較合適的數值.
  • Initial velocity: 初速度, 值越大的話, 則動畫將要結束時, 到達終點后偏離終點的距離越大. 一般設置為0, 看需要什么效果來定.

4 取消 View 動畫

動畫在進行過程中如何停止動畫?

在 iOS 10 引入屬性動畫器之前, 往往都是將動畫從圖層中移除的方式來停止動畫.

但這個方法的缺點很明顯: 移除動畫后, 正在進行的動畫就生硬地沒了, 視圖也直接到達終態.

故取消動畫的一個正確做法(或者說是普遍做法)是讓動畫加速行進至終態. 這正是動畫疊加的絕佳應用場合.

但之前的動畫是在行進過程中, 只有先將所有動畫取消, 但這樣會導致視圖直接處于終態. 如果可以獲取到在取消時刻的動畫狀態, 然后取消之前動畫, 再插入一個新的加速動畫, 則可以將這個問題解決.

而上述方法的核心就是利用 presentation layer, 將視圖在取消時刻的狀態和當前 presentation layer 的狀態設置為一致, 然后再開始新加速后的動畫.

流程即:

  1. 獲取視圖根圖層的 presentation layer 的當前某個需要的狀態, 然后將其賦值給視圖的根圖層.
  2. 將視圖動畫(在根圖層上的動畫)取消掉.
  3. 新建一個動畫, 該動畫的終態是原來的終態, 但時間縮短很多.
    • 如果動畫取消的上下文意味著動畫回到原來位置, 則將這個動畫的終態設置為視圖初始狀態即可.
    • 另外如果取消在當前的意思是停在當前位置, 則就不加新建的這個動畫.)

這樣的效果就可以很平滑了.

    private func cancelSprintAnimation() {
        let presentationLayer = self.aView.layer.presentation()!
        // 將進行動畫的視圖狀態賦值為其 presentationLayer 的當前狀態, 這里是 position 
        self.aView.layer.position = presentationLayer.position
        // 移除之前的動畫
        self.aView.layer.removeAllAnimations()
        print(aView.center)
        // 開始新的加速動畫, 這里是加速到最終位置.
        UIView.animate(withDuration: 0.1) {
            self.aView.center.y = self.view.bounds.height / 2.0
        }
    }

    private func performSpringAnimation() {
        print("進行彈性動畫")
        UIView.animate(withDuration: 2.0, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: [], animations: {
            self.aView.center.y = 450
        })
    }

上面設置 layer 的屬性為 presentation layer 的當前屬性, 因為動畫實際是 presentation layer 在進行的.

取消 repeat 動畫

重復動畫取消的話, 也是采用加速到終態或加速回到初態. 或保持在當前狀態.

因為重復動畫不會被其他動畫疊加, 所以在重復動畫上添加動畫實際就自動把重復動畫取消了. 然后利用 beginFromCurrentState 選項, 即可做到之前的平滑取消動畫.

    private func performRepeatAnimation() {
        UIView.animate(withDuration: 2.0, delay: 0, options: [.repeat, .autoreverse], animations: {
            self.aView.center.y = 600
        }, completion: nil)
    }

    private func cancelRepeatAnimation() {// 取消重復動畫
        UIView.animate(withDuration: 0.1, delay: 0, options: [.beginFromCurrentState], animations: {
            self.aView.center.y = self.view.frame.height / 2.0
        }, completion: nil)
    }

取消重復動畫時, 如果下一個加速動畫的終態是和重復終態相同的話, 則有 bug... 取消動畫沒有被執行, 故需要加速動畫的終態不和重復終態一樣, 比如下面的本來需要 600, 這里設置 600.000001:

    private func cancelRepeatAnimation() {
        UIView.animate(withDuration: 0.1, delay: 0, options: [.beginFromCurrentState], animations: {
            self.aView.center.y = 600.000001
        }, completion: nil)
    }

如果想要的效果是在當前位置停下, 則也是獲取 presentation layer 的當前參數, 然后在動畫塊中賦值給 layer 即可.

可以使用一個視圖屬性將若干可動畫屬性進行改變, 然后進行動畫, 這個先了解一下.

5 幀動畫

原理都一樣, 不過可以通過幀動畫來組裝一些復雜的動畫效果.

6 轉變動畫

transition 動畫表示的是視圖內容的改變, 有兩種:

  • transition(with:duration:options:animations:completion:): 對一個視圖的內容改變進行動畫, 提供的選項詳見文檔.
  • transition(from:to:duration:options:completion:): 將 fromView 替換為 toView.

7 隱式 Layer 動畫

只有當 Layer 布局并顯示到界面上后, 改變它的可動畫屬性才會出現動畫. 另外根 layer 只能進行顯式動畫.

layer 的隱式動畫都是在 CATransaction 的上下文中執行的. 另外總是有一個不可見的 CATransaction "包圍著"代碼, 故可以不用調用 CATransaction 的 begin 和 commit 也能改變動畫的一些屬性, 比如:

CATransaction.setAnimationDuration(0.8)
arrow.transform = CATransform3DRotate(arrow.transform, .pi/4.0, 0, 0, 1)

8 核心動畫

下面正式進入 Core Animation 的內容.

CA 動畫就是指的顯式圖層動畫, 主要使用 CAAnimation 和它的子類來實現動畫.

在使用 CA 進行動畫時, 視圖的終態不會自動設置, 需要手動設. 不然視圖在動畫完成后會回到原位置, 因為動畫是在 layer 上進行的.

8.1 CABasicAnimation

CA 的使用方式是: 創建動畫對象(CAAnimation子類型), 然后將它添加到 layer 上即可. 添加時需要一個 key, 這個key用于標志唯一的動畫.

添加了動畫對象后, layer 就開始動畫(redraw 時機), 但動畫結束后被移除掉, 此時 layer 的狀態又會恢復到動畫前, 故需要手動修改終態.

使用 CA 動畫的基本模式是:

  1. 獲取 layer 的某個當前動畫對應的屬性的初始值和終值.
  2. 將 layer 的屬性改變為終值, 如果不想隱式動畫起作用, 需要調用 setDisableActions(true).
  3. 創建 CA 動畫, 在其中指定需要動畫的屬性
  4. 將動畫添加到 layer 上.

顯式動畫被添加到 layer 時, 實際添加的是它的不可變副本.

8.2 CAKeyFrameAnimation

這個是圖層幀動畫類

8.3 CASpringAnimation

這個是圖層彈性動畫類.

9 動畫組

可以把一組動畫通過 CAAnimationGroup 組合在一起, 每個單獨的動畫都添加到它的 animation 屬性中, 通過動畫的延遲和時長來決定動畫的執行順序, 從而可以完成許多復雜的效果.

CAAnimationGroup 本身就是 CAAnimation 的子類, 可以把它當成是一個父動畫, 可以在其中添加子動畫. 其中的子動畫會繼承它的一些默認屬性值(如果子動畫沒有設置的話).

下面來實現一個動船的動畫:

第一個動畫: 圖層沿指定曲線路徑運動.

func createAnim1() -> CAAnimation {
    let areaHeight: CGFloat = 200
    let verticalSpace: CGFloat = 7
    let path = CGMutablePath()
    var leftright: CGFloat = 1 // 表示現在是朝左還是朝右, 左為 -右為 1.
    // 以下代碼生成需要的路徑
    var next: CGPoint = self.view.layer.position   // 路徑起點
    var pos: CGPoint
    path.move(to: CGPoint(next.x, next.y))
    for _ in 0 ..< 4 {
        pos = next
        leftright *= -1
        next = CGPoint(pos.x + areaHeight * leftright, pos.y verticalSpace)
        path.addCurve(to: CGPoint(next.x, next.y),
                      control1: CGPoint(pos.x, pos.y + 30),
                      control2: CGPoint(next.x, next.y - 30))
    }
    endPoint = next // 記錄最終點的位置.
    // 動畫1: 將圖層的 position 沿著曲線運動. 這個動畫添加到任何圖層上都適用.
    let anim1 = CAKeyframeAnimation(keyPath: #keyPa(CALayer.position))
    anim1.path = path
    anim1.calculationMode = kCAAnimationPaced
    return anim1
  }

第二個動畫: 船需要在轉彎的時候同時翻轉, 否則看起來就不正常了. 翻轉時直接沿著 Y 軸旋轉即可. 第二個動畫需要和第一個動畫配合, 當第一個動畫中小船每次處于曲線的頂點時, 都需要對其進行翻轉, 如果遇到比較復雜的情況, 則需要定義 keyTimes 數組, 讓兩個動畫可以協作.

func createAnim2() -> CAAnimation {
    let revs = [0.0, .pi, 0.0, .pi]
    let anim2 = CAKeyframeAnimation(keyPath:#keyPath(CALayer.transform))
    anim2.values = revs
    anim2.valueFunction = CAValueFunction(name:kCAValueFunctionRotateY)
    anim2.calculationMode = kCAAnimationDiscrete
    return anim2
}

第三個動畫: 小船的重復震動效果, 模擬的是風雨飄搖.

func createAnim3() -> CAAnimation {
    let pitches = [0.0, .pi/60.0, 0.0, -.pi/60.0, 0.0]
    let anim3 = CAKeyframeAnimation(keyPath:#keyPath(CALayer.transform))
    anim3.values = pitches
    anim3.repeatCount = .infinity
    anim3.duration = 0.5
    anim3.isAdditive = true
    anim3.valueFunction = CAValueFunction(name:kCAValueFunctionRotateZ)
    return anim3
}

最后通過動畫組將三個動畫組合, 然后將動畫組應用到小船圖層上.

private func addAnimToBoatImageViewLayer() {
    let animGroup = createAnimGroup()
    boatImageView.layer.add(animGroup, forKey: nil)
    CATransaction.setDisableActions(true)
    boatImageView.layer.position = endPoint ?? .zero
}

利用 CAAnimationGroup 可以實現許多復雜的動畫效果, 這里看到的小船動畫就是一個.

10 關于動畫凍結

可以不把動畫取消掉, 而是在某個位置將動畫凍結, 這樣在未來的某個時候, 可以手動繼續開始動畫.

由于 CALayer 有一個 speed 屬性, 如果將它改為 0, 就可以把動畫凍結. 另外還有一個 timeOffset 屬性, 可以控制顯示動畫的任意一幀. 這兩個屬性結合后, 就可以實現動畫按 timeOffset 的值來動態控制顯示了.

    func increase() {
        guard shape.timeOffset + 0.1 <= 1 else { return }
        shape.timeOffset += 0.1
    }

    func decrease() {
        guard shape.timeOffset - 0.1 >= 0 else { return }
        shape.timeOffset -= 0.1
    }

    func setupAnims() {
        shape.frame = bounds
        layer.addSublayer(shape)
        shape.fillColor = UIColor.clear.cgColor
        shape.strokeColor = UIColor.red.cgColor

        let path = CGPath(ellipseIn: CGRect(10, 10, 50, 50), transform: nil)
        shape.path = path

        let path2 = CGPath(rect: CGRect(10, 10, 50, 50), transform: nil)

        let basicAnim = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.path))
        basicAnim.duration = 1.0
        basicAnim.fromValue = path
        basicAnim.toValue = path2
        shape.speed = 0
        shape.timeOffset = 0
        shape.add(basicAnim, forKey: nil)
    }

11 關于圖層的轉變

圖層的轉變(transition) 指的是被轉變的圖層擁有兩個"拷貝", 通過第二個替換第一個, 從而實現一些轉變的效果.

主要是設置轉變的類型和子類型來達到效果, 這里由于歷史原因, 轉變中的 Bottom 和 Top 正好和手機方向是相反的.

    private func rootLayerTransition() {
        let transition = CATransition()
        transition.type = kCATransitionPush
        transition.subtype = kCATransitionFromBottom
        transition.duration = 2.0
        CATransaction.setDisableActions(true)
        layer.contents = UIImage(named: "img_highlighted")?.cgImage
        layer.add(transition, forKey: nil)
    }

這樣的動畫應用場景主要是父圖層的 maskToBounds 屬性是 true, 然后子圖層從范圍外移動到范圍內的情況, 這樣就可以達到動畫效果, 且在父圖層外不會看大子圖層的移動效果.

12 動畫列表

為了了解動畫的內部原理, 需要首先看看什么是 Animation List.

顯式動畫是通過 CALayer 的 add 方法添加到圖層上的. 動畫對象(CAAnimation)改變的是圖層的繪制方式. 當動畫添加到圖層上后, 剩下的工作都是由圖層的繪制機制來完成的.

在圖層中維護了一張當前正在或需要進行的動畫的列表, 動畫通過 add 方法添加到列表中, 當動畫時機到達時, 圖層就根據動畫列表中的所有動畫來決定如何將自己繪制出來, 且按照一定的順序進行, 而繪制時候要進行的任務在文檔中稱為 rendering tree.

動畫的添加順序就是它們在繪制時候的執行順序.

動畫列表中如果存在某個 key, 當另外一個擁有相同 key 的動畫添加進來時, 之前的相同 key 的動畫會被移除掉.

如果添加動畫時指定 key 為 nil, 則不受 key 值的影響, 即可以多次添加 nil key 的動畫.

但是蘋果對 keyPath 和 key 搞了一個小動作, 即如果在創建動畫時將 keyPath 設置為 nil, 則動畫默認指向的屬性就是 add 方法中 key 對應的屬性, 這個不得不說是個蛋疼的地方. 有時這樣的結合規則會被誤用.

所以一定要清楚地認識這兩個方法參數的作用:

  • 創建動畫時一定要指定動畫的 keyPath
  • 添加動畫時候的 key 一定只是作為不同動畫的標記

這樣的話, 在某些情況下就可以通過相同 key 的動畫將之前的那個替換掉.

另外 CATransition 添加的動畫的 key 一直是 "transition"(也就是 kCATransition 代表的字符串), 故同一個 layer 同時只能添加一個 CATransition 動畫.

如果沒有特殊處理的話, 當動畫結束后, 動畫就會從動畫列表中移除掉. 當然可以設置動畫的 isRemovedOnCompletion 來將它保留在動畫列表中, 下次動畫時機時就會再次被執行.

這里有一個經典的錯誤實現:

很多例子都是使用 isRemovedOnCompletion 結合 fillMode 設置來把圖層動畫的最終狀態保留在動畫的最后狀態上, 但這樣的錯誤的, 因為這個時候的圖層只是 "看起來" 在最終位置.

而正確的解決辦法是: 將圖層對應的屬性修改為動畫的終態一致, 然后進行動畫即可, 而非設置 fillMode.

而 kCAFillMode 的用途是在動畫組(Animation Group)中, 和子動畫相關的.

在代碼中無法直接訪問動畫列表的所有動畫, 只能利用 animation(forKey:) 方法來獲取某個 key 對應的動畫. 且動畫完成回調調用時, 該動畫已經完成, 故就已經從動畫列表中移除了, 所以在完成回調中, 是獲取不到該動畫的.

可以使用 removeAnimation(forKey:) 或者 removeAllAnimations 移除動畫, 當移除 nil key 的動畫時, 只有通過 removeAllAnimations 才可以辦到.

如果 APP 被暫停(suspended)的時候, 系統會自動在所有圖層上調用 removeAllAnimations 方法.

當手動將進行中的動畫被移除時, 它會直接停止. 但停止的時機是在下一個重繪時機到達時. 如果想直接停止動畫, 需要在 transaction 塊中寫.

待續...

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,578評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,701評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,691評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,974評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,694評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,026評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,015評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,193評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,719評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,442評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,668評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,151評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,846評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,255評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,592評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,394評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容

  • 在iOS中隨處都可以看到絢麗的動畫效果,實現這些動畫的過程并不復雜,今天將帶大家一窺ios動畫全貌。在這里你可以看...
    每天刷兩次牙閱讀 8,551評論 6 30
  • 在iOS中隨處都可以看到絢麗的動畫效果,實現這些動畫的過程并不復雜,今天將帶大家一窺iOS動畫全貌。在這里你可以看...
    F麥子閱讀 5,140評論 5 13
  • 如果想讓事情變得順利,只有靠自己--夏爾·紀堯姆 上一章介紹了隱式動畫的概念。隱式動畫是在iOS平臺創建動態用戶界...
    夜空下最亮的亮點閱讀 1,976評論 0 1
  • 書寫的很好,翻譯的也棒!感謝譯者,感謝感謝! iOS-Core-Animation-Advanced-Techni...
    錢噓噓閱讀 2,314評論 0 6
  • 把玫瑰獻給青春的火, 把月桂獻給盛年的人; 就折一段常春藤給我, 這未老先衰的身。 把紫羅蘭獻給青春的墳, 把桂冠...
    威廉_愛德華茲閱讀 212評論 0 0