深入了解動態設計:高級 iOS 過渡

已發表: 2020-11-09

創造力帶來了不斷給用戶留下深刻印象的願望。 幾個世紀以來,人類一直試圖操縱可用的手段來重建最自然的互動,以自然為例。

人在發現世界時,對周圍世界的微妙細節越來越敏感,這讓他們本能地分辨出人造物和生物。 這條線隨著技術的發展而變得模糊,該軟件旨在創建一個環境,​​在該環境中,用戶將他在人工創造的世界中的體驗描述為自然。

在應用程序設計中呼應自然

本文將以大多數iOS應用中日常元素交互動畫中的形狀變換為例,介紹模糊邊框的過程。 模仿自然的一種方法是通過對像在時間上的位置的各種變換。 下面給出動畫時間的示例性函數。

結合通過添加幾何變換對時間的正確使用,我們可以獲得無限多的效果。 為了展示當今設計和技術的可能性,我們創建了 Motion Patterns 應用程序,其中包括由我們的軟件開發公司開發的流行解決方案。 由於我不是作家,而是程序員,沒有什麼比活生生的例子更能說明問題了,我只好邀請你來到這個美好的世界!

樣品發現

讓我們看看運動設計如何將普通設計轉變為非凡的設計! 在下面的示例中,左側有一個僅使用基本 iOS 動畫的應用程序,而右側有一個相同應用程序的版本,並進行了一些改進。

“潛得更深”的效果

這是在視圖的兩種狀態之間使用轉換的轉換。 建立在集合的基礎上,在選擇特定單元格後,通過轉換其各個元素 * 轉換到元素的詳細信息。 另一個解決方案是使用交互式轉換,這有助於應用程序的使用。

*實際上是在臨時視圖上複製/映射數據元素,參與過渡,在它的開始和結束之間……但我將在本文後面解釋這一點……

“窺視邊緣”效果

在其動作中使用滾動視圖動畫可將圖像以3D 形式轉換為立方體效果。 影響效果的主要因素是滾動視圖的偏移量。

“連接點”效果

這是場景之間的過渡,將微型物體變成了整個畫面。 用於此目的的集合同時工作,一個屏幕上的變化對應於另一個屏幕上的變化。 此外,當您在背景中輸入微縮模型時,在場景之間滑動時會出現視差效果。

“改變形狀”效果

最後一種動畫是使用 Lottie 庫的簡單動畫。 它是動畫圖標最常見的用途。 在這種情況下,這些是標籤欄上的圖標。 此外,通過更改適當的選項卡,使用特定方向的過渡動畫來進一步強化交互效果。

深入研究:我們的第一個動作設計模式

現在是時候進入正題了……我們需要更深入地研究控制這些示例的機制的結構。

在本文中,我將向您介紹第一個動作設計模式,我們將其命名為“Dive Deeper”,並對其使用進行了抽象描述,但不涉及具體細節。 我們計劃在未來不受任何限制地向所有人提供準確的代碼和整個存儲庫。

項目的架構和應用嚴格的編程設計模式目前不是優先事項——我們專注於動畫和過渡。

在本文中,我們將使用兩組特性來管理場景轉換期間的視圖。 因此,我想指出,這篇文章是為那些比較熟悉 UIKit 和 Swift 語法的人準備的。

https://developer.apple.com/documentation/uikit/uiviewcontrolleranimatedtransitioning

https://developer.apple.com/documentation/uikit/uipercentdriveninteractivetransition

https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/CustomizingtheTransitionAnimations.html

一:結構

對於實現給定解決方案的基本版本,將需要幾個幫助類,負責提供有關轉換中涉及的視圖的必要信息,並控制轉換本身和交互。

Swift 中的過渡

負責管理過渡的基本類,即代理,將是 TransitionAnimation。 它決定過渡的方式,並涵蓋執行 Apple 團隊提供的操作所需的標準功能。

 /// 這是過渡的基本類,在指定的持續時間內呈現和關閉具有不同的行為。
打開類TransitionAnimator:NSObject,UIViewControllerAnimatedTransitioning {
    
    /// 指示它是呈現還是關閉過渡。
    var呈現:Bool = true
    
    /// 整個轉換發生的時間間隔。
    私人出租持續時間:TimeInterval
    
    /// 具有默認值的過渡動畫器的默認初始化器。
    /// - 參數持續時間:整個轉換發生的時間間隔。
    /// - 參數呈現:指示它是呈現還是解除過渡。
    公共初始化(持續時間:TimeInterval = 0.5,呈現:Bool = true){
        self.duration = 持續時間
        self.presenting = 呈現
        超級初始化()
    }
    
    /// 確定過渡的持續時間。
    /// - 參數transitionContext:當前轉換的上下文。
    /// - 返回:動畫器初始化時指定的持續時間。
    公共函數transitionDuration(使用transitionContext:UIViewControllerContextTransitioning?)-> TimeInterval {
        返回self.duration
    }
    
    /// 過渡動畫師的核心,在此函數中進行過渡。
    /// - 重要提示:在更具體的過渡類型中覆蓋此函數對於執行動畫至關重要。
    /// - 參數transitionContext:轉換的上下文。
    公共 func animateTransition(使用 transitionContext: UIViewControllerContextTransitioning) { }
    
}

基於TransitionAnimator,我們創建一個TransformTransition 文件,其任務是執行特定的transition,transition with transformation(parenting)

 /// 轉換轉換的實現。
打開類TransformTransition:TransitionAnimator {
    
    /// 包含所有需要的轉換規範的視圖模型。
    私有變量 viewModel:TransformViewModel
    
    /// 轉換轉換的默認初始化器。
    /// - 參數viewModel:變換轉換的視圖模型。
    /// - 參數持續時間:過渡的持續時間。
    初始化(viewModel:TransformViewModel,持續時間:TimeInterval){
        self.viewModel = viewModel
        super.init(持續時間:持續時間,呈現:viewModel.presenting)
    }

TransformTransition 類的組成包括TransformViewModel,顧名思義,它通知了該轉換將應用於哪些視圖模型的機制。

 /// 轉換轉換的視圖模型,其中包含有關它的基本信息。
最終類 TransformViewModel {
    
    /// 指示轉換轉換是呈現還是關閉視圖。
    讓呈現:布爾
    /// 具有關於每個視圖的變換規範的模型數組。
    讓模型:[TransformModel]
    
    /// 轉換視圖模型的默認初始化器。
    /// - 參數呈現:指示它是呈現還是關閉變換過渡。
    /// - 參數模型:模型數組,帶有關於每個視圖的變換的規範。
    初始化(呈現:布爾,模型:[TransformModel]){
        self.presenting = 呈現
        self.models = 模型
    }
    
}

轉換模型是一個輔助類,描述了位於父級的轉換中涉及的視圖的特定元素,通常是可以轉換的控制器視圖。

在轉換的情況下,這是一個必要的步驟,因為這種轉換包含給定狀態之間特定視圖的操作。

二:執行

我們使用 Transformable 擴展了我們開始轉換的視圖模型,這迫使我們實現一個準備所有必要元素的函數。 這個函數的大小可以很快增長,所以我建議你把它分解成更小的部分,例如每個元素。

 /// 想要執行轉換轉換的類的協議。
協議可轉換:ViewModel {
    
    /// 準備轉換中涉及的視圖模型。
    /// - 參數fromView:轉換開始的視圖
    /// - 參數 toView:轉換到的視圖。
    /// - 參數呈現:指示它是呈現還是關閉。
    /// - 返回:結構數組,其中包含準備好為每個視圖轉換轉換所需的所有信息。
    func prepareTransitionModels(fromView: UIView, toView: UIView, presenting: Bool) -> [TransformModel]
    
}

假設並不是說如何搜索參與轉換的視圖的數據。 在我的示例中,我使用了代表給定視圖的標籤。 您可以在這部分實施中自由發揮。

特定視圖變換的模型(TransformModel)是整個列表中最小的模型。 它們由開始視圖、過渡視圖、開始幀、結束幀、開始中心、結束中心、並發動畫和結束操作等關鍵變換信息組成。 大多數參數在轉換過程中不需要使用,因此它們有自己的默認值。 為了獲得最小的結果,只使用那些需要的就足夠了。

 /// 具有默認值的轉換模型的默認初始化器。
    /// - 參數initialView:轉換開始的視圖。
    /// - 參數 phantomView:在變換過渡期間呈現的視圖。
    /// - 參數initialFrame:開始變換轉換的視圖框架。
    /// - 參數finalFrame:將在變換過渡結束時呈現的視圖框架。
    /// - 參數initialCenter:當初始中心點與初始視圖中心點不同時需要。
    /// - 參數finalCenter:當最終中心點與最終視圖中心點不同時需要。
    /// - 參數parallelAnimation:在變換過渡期間執行的視圖的附加動畫。
    /// - 參數完成:轉換轉換後觸發的代碼塊。
    /// - 注意:只需要初始視圖來執行最簡約版本的轉換轉換。
    初始化(初始視圖:UIView,
         幻影視圖:UIView = UIView(),
         初始幀:CGRect = CGRect(),
         finalFrame: CGRect = CGRect(),
         初始中心:CGPoint? = 無,
         finalCenter:CGPoint? = 無,
         並行動畫:(()->無效)? = 無,
         完成:(()->無效)? =無){
        self.initialView = 初始視圖
        self.phantomView = phantomView
        self.initialFrame = 初始幀
        self.finalFrame = finalFrame
        self.parallelAnimation = 並行動畫
        self.completion = 完成
        self.initialCenter = 初始中心 ?? CGPoint(x: initialFrame.midX, y: initialFrame.midY)
        self.finalCenter = finalCenter ?? CGPoint(x: finalFrame.midX, y: finalFrame.midY)
    }

您的注意力可能已被幻影視圖所吸引。 現在我將解釋 iOS 轉換的工作流程。 以盡可能短的形式……

iOS 過渡的工作流程

當用戶希望移動到下一個場景時,iOS 通過將開始(藍色)和目標(綠色)控制器複製到內存來準備特定的控制器。 接下來,通過包含容器的轉換協調器創建轉換上下文,這是一個不包含任何特殊功能的“愚蠢”視圖,除了模擬兩個場景之間的轉換視圖。

使用轉換的關鍵原則是不要將任何真實視圖添加到轉換上下文中,因為在轉換結束時,所有上下文以及添加到容器中的視圖都會被釋放。 這些視圖僅在過渡期間存在,然後被刪除。

因此,使用作為真實視圖副本的幻像視圖是這種轉換的重要解決方案。

在這種情況下,我們有一個過渡,通過改變其形狀和大小將一個視圖轉換為另一個視圖。 為此,在過渡開始時,我創建給定元素的 PhantomView 並將其添加到容器中。 FadeView 是一個輔助視圖,用於為整體過渡添加柔和度。

 /// 轉換轉換的核心,轉換執行的地方。 覆蓋`TransitionAnimator.animateTransition(...)`。
    /// - 參數transitionContext:當前變換轉換的上下文。
    覆蓋打開 func animateTransition(使用 transitionContext: UIViewControllerContextTransitioning) {
        守衛讓 toViewController = transitionContext.view(forKey: .to),
            讓 fromViewController = transitionContext.view(forKey: .from) else {
                返回 Log.unexpectedState()
        }
        讓 containerView = transitionContext.containerView
        讓持續時間 = transitionDuration(使用:transitionContext)
        let fadeView = toViewController.makeFadeView(opacity: (!presenting).cgFloatValue)
        讓模型 = viewModel.models
        讓呈現視圖 = 呈現? toViewController : fromViewController
        
        models.forEach { $0.initialView.isHidden = true }
        呈現視圖.isHidden = true
        containerView.addSubview(toViewController)
        如果呈現{
            containerView.insertSubview(fadeView, belowSubview: toViewController)
        } 別的 {
            containerView.addSubview(fadeView)
        }
        containerView.addSubviews(viewModel.models.map { $0.phantomView })

在下一步中,我通過轉換將其轉換為目標形狀,並根據它是演示還是召回,它執行額外的操作來清理特定的視圖——這就是這個轉換的全部秘訣。

 讓動畫: () -> Void = { [weak self] in
            守衛讓 self = self else { return Log.unexpectedState() }
            fadeView.alpha = self.presenting.cgFloatValue
            模型.forEach {
                讓 center = self.presenting ? $0.finalCenter : $0.initialCenter
                讓變換=自我呈現? $0.presentTransform : $0.dismissTransform
                $0.phantomView.setTransformAndCenter(變換,中心)
            }
            models.compactMap { $0.parallelAnimation }.forEach { $0() }
        }
        
        讓完成:(布爾)-> Void = { _ in
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
            presentView.isHidden = false
            模型.compactMap { $0.completion }.forEach { $0() }
            models.forEach { $0.initialView.isHidden = false }
            如果 !self.presenting && transitionContext.transitionWasCancelled {
                toViewController.removeFromSuperview()
                fadeView.removeFromSuperview()
            }
        }
        
        UIView.animate(withDuration:持續時間,
                       延遲:0,
                       使用SpringWithDamping:1,
                       初始SpringVelocity:0.5,
                       選項:.curveEaseOut,
                       動畫:動畫,
                       完成:完成)

三:特殊成分

將所有函數、類和協議放在一起後,結果應如下所示:

我們過渡的最後一個組成部分將是它的完全交互性。 為此,我們將使用在控制器視圖中添加的平移手勢,TransitionInteractor...

 /// 處理交互轉換的中介。
最終類TransitionInteractor:UIPercentDrivenInteractiveTransition {
    
    /// 指示轉換是否已經開始。
    var hasStarted = false
    /// 指示轉換是否應該完成。
    var shouldFinish = false

}

…我們也在控制器主體中初始化。

 /// 處理集合視圖項上的平移手勢,並管理轉換。
    @objc func handlePanGesture(_gestureRecognizer: UIPanGestureRecognizer) {
        讓百分比閾值:CGFloat = 0.1
        讓翻譯 = gestureRecognizer.translation(in: view)
        讓verticalMovement = translation.y / view.bounds.height
        讓upupMovement = fminf(Float(verticalMovement), 0.0)
        讓upwardMovementPercent = fminf(abs(upwardMovement), 0.9)
        讓進度 = CGFloat(upwardMovementPercent)
        守衛讓交互器=交互控制器否則{返回}
        切換手勢識別器.state {
        案例.開始:
            interactor.hasStarted = true
            讓 tapPosition = gestureRecognizer.location(in: collectionView)
            showDetailViewControllerFrom(位置:tapPosition)
        案例.更改:
            interactor.shouldFinish = 進度 > percentThreshold
            交互器.更新(進度)
        案例.取消:
            interactor.hasStarted = false
            交互器.取消()
        案例結束:
            interactor.hasStarted = false
            交互者.shouldFinish
                ? 交互器.finish()
                :interactor.cancel()
        默認:
            休息
        }
    }

準備好的交互應該如下:

如果一切按計劃進行,我們的應用程序將在用戶眼中獲得更多。

只發現了一座冰山的山峰

下一次,我將在後面的版本中解釋運動設計相關問題的實現。

應用程序、設計和源代碼是 Miquido 的財產,由才華橫溢的設計師和程序員熱情地創建,我們不負責在我們的實現中使用這些內容。 詳細的源代碼將在未來通過我們的 github 帳戶提供——我們邀請您關注我們!

感謝您的關注,我們很快再見!