深入了解动态设计:高级 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 帐户提供——我们邀请您关注我们!

感谢您的关注,我们很快再见!