【问题标题】:UINavigationBar color animation synchronized with push animationUINavigationBar 彩色动画与推送动画同步
【发布时间】:2018-01-26 04:31:13
【问题描述】:

我想在具有不同UINavigationBar 背景颜色的视图之间实现平滑动画。嵌入式视图具有与UINavigationBar 相同的背景颜色,我想模仿推/弹出过渡动画,例如:

我已经准备好自定义过渡:

class CustomTransition: NSObject, UIViewControllerAnimatedTransitioning {

    private let duration: TimeInterval
    private let isPresenting: Bool

    init(duration: TimeInterval = 1.0, isPresenting: Bool) {
        self.duration = duration
        self.isPresenting = isPresenting
    }

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return duration
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        let container = transitionContext.containerView
        guard
            let toVC = transitionContext.viewController(forKey: .to),
            let fromVC = transitionContext.viewController(forKey: .from),
            let toView = transitionContext.view(forKey: .to),
            let fromView = transitionContext.view(forKey: .from)
        else {
            return
        }

        let rightTranslation = CGAffineTransform(translationX: container.frame.width, y: 0)
        let leftTranslation = CGAffineTransform(translationX: -container.frame.width, y: 0)

        toView.transform = isPresenting ? rightTranslation : leftTranslation

        container.addSubview(toView)
        container.addSubview(fromView)

        fromVC.navigationController?.navigationBar.backgroundColor = .clear
        fromVC.navigationController?.navigationBar.setBackgroundImage(UIImage.fromColor(color: .clear), for: .default)

        UIView.animate(
            withDuration: self.duration,
            animations: {
                fromVC.view.transform = self.isPresenting ? leftTranslation :rightTranslation
                toVC.view.transform = .identity
            },
            completion: { _ in
                fromView.transform = .identity
                toVC.navigationController?.navigationBar.setBackgroundImage(
                    UIImage.fromColor(color: self.isPresenting ? .yellow : .lightGray),
                    for: .default
                )
                transitionContext.completeTransition(true)
            }
        )
    }
}

并在UINavigationControllerDelegate方法实现中返回:

func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    return CustomTransition(isPresenting: operation == .push)
}

虽然 push 动画效果很好,但 pop 却不行。

问题:

  1. 为什么在弹出动画之前清除 NavBar 颜色后仍然是黄色?
  2. 有没有更好的方法来实现我的目标? (导航栏不能一直透明,因为它只是流程的一部分)

这是我在 GitHub 上的测试项目的 link

编辑

这是一个 gif,展示了所讨论问题的全貌和预期效果:

【问题讨论】:

  • 你觉得这个库怎么样:github.com/MoZhouqi/KMNavigationBarTransition
  • 嗨..您是否尝试在每个控制器的 viewWillAppear 中更改导航颜色?
  • @TomaszRejdych:我正在努力避免这种依赖:)
  • @CodeHunterr:是的,试过了,但没有得到肯定的结果。
  • hi @Fayer ..apple 默认动画对我的上述方法很有用。只需删除您的自定义动画并添加简单的 pushViewController 并返回 popViewController。它可能会帮助你取得好成绩。

标签: ios swift uinavigationbar custom-transition


【解决方案1】:

这些组件总是很难定制。我认为,Apple 希望系统组件在每个应用程序中的外观和行为都相同,因为它允许在整个 iOS 环境中保持共享的用户体验。

有时,从头开始实现自己的组件比尝试自定义系统组件更容易。定制通常会很棘手,因为您不确定内部组件是如何设计的。因此,您必须处理大量边缘情况并处理不必要的副作用。

不过,我相信我有适合您情况的解决方案。我已经分叉了您的项目并实施了您描述的行为。 你可以在GitHub 上找到我的实现。请参阅animation-implementation 分支。


UINavigationBar

pop动画不能正常工作的根本原因是UINavigationBar有自己的内部动画逻辑。当UINavigationController's 堆栈改变时,UINavigationController 告诉UINavigationBar 改变UINavigationItems。因此,首先,我们需要禁用UINavigationItems 的系统动画。可以通过子类化UINavigationBar来完成:

class CustomNavigationBar: UINavigationBar {
   override func pushItem(_ item: UINavigationItem, animated: Bool) {
     return super.pushItem(item, animated: false)
   }

   override func popItem(animated: Bool) -> UINavigationItem? {
     return super.popItem(animated: false)
   }
}

那么UINavigationController应该用CustomNavigationBar初始化:

let nc = UINavigationController(navigationBarClass: CustomNavigationBar.self, toolbarClass: nil)

UINavigationController


由于需要在UINavigationBar 和呈现UIViewController 之间保持动画流畅和同步,我们需要为UINavigationController 创建自定义过渡动画对象并将CoreAnimationCATransaction 一起使用。

自定义过渡

您对过渡动画师的实现几乎完美,但从我的角度来看,很少有细节被遗漏。在Customizing the Transition Animations 文章中,您可以找到更多信息。另外请注意UIViewControllerContextTransitioning协议中的方法cmets。

所以,我的推送动画版本如下:

func animatePush(_ transitionContext: UIViewControllerContextTransitioning) {
  let container = transitionContext.containerView

  guard let toVC = transitionContext.viewController(forKey: .to),
    let toView = transitionContext.view(forKey: .to) else {
      return
  }

  let toViewFinalFrame = transitionContext.finalFrame(for: toVC)
  toView.frame = toViewFinalFrame
  container.addSubview(toView)

  let viewTransition = CABasicAnimation(keyPath: "transform")
  viewTransition.duration = CFTimeInterval(self.duration)
  viewTransition.fromValue = CATransform3DTranslate(toView.layer.transform, container.layer.bounds.width, 0, 0)
  viewTransition.toValue = CATransform3DIdentity

  CATransaction.begin()
  CATransaction.setAnimationDuration(CFTimeInterval(self.duration))
  CATransaction.setCompletionBlock = {
      let cancelled = transitionContext.transitionWasCancelled
      if cancelled {
          toView.removeFromSuperview()
      }
      transitionContext.completeTransition(cancelled == false)
  }
  toView.layer.add(viewTransition, forKey: nil)
  CATransaction.commit()
}

Pop 动画的实现几乎相同。 fromValuetoValue 属性的 CABasicAnimation 值的唯一区别。

UINavigationBar 动画

为了对UINavigationBar 进行动画处理,我们必须在UINavigationBar 层上添加CATransition 动画:

let transition = CATransition()
transition.duration = CFTimeInterval(self.duration)
transition.type = kCATransitionPush
transition.subtype = self.isPresenting ? kCATransitionFromRight : kCATransitionFromLeft
toVC.navigationController?.navigationBar.layer.add(transition, forKey: nil)

上面的代码将动画整个UINavigationBar。为了只为UINavigationBar 的背景设置动画,我们需要从UINavigationBar 检索背景视图。诀窍是:UINavigationBar 的第一个子视图是 _UIBarBackground 视图(可以使用 Xcode Debug View Hierarchy 进行探索)。在我们的例子中,确切的类并不重要,它是UIView 的继承者就足够了。 最后我们可以直接在_UIBarBackground的视图层上添加我们的动画过渡:

let backgroundView = toVC.navigationController?.navigationBar.subviews[0]
backgroundView?.layer.add(transition, forKey: nil)

我想指出,我们正在预测第一个子视图是背景视图。将来可能会更改视图层次结构,请记住这一点。

将两个动画添加到一个CATransaction 中很重要,因为在这种情况下这些动画将同时运行。

您可以在每个视图控制器的viewWillAppear 方法中设置UINavigationBar 背景颜色。

这是最终动画的样子:

我希望这会有所帮助。

【讨论】:

  • 您的回答非常有用,非常感谢!我已经测试了你的解决方案,它几乎可以工作了......弹出动画有一个小故障,当你在设备上测试时它是可见的(iPhone 7,iOS 11.2)。将深入细节并尝试查找原因。
  • @Fayer 我很高兴它有帮助!我已经在设备(iPhone 6s、iOS 11.2.5)上启动了该项目,并且故障确实存在。它也存在于 iOS 模拟器上,但几乎不可见。根本原因是我错过了为视图设置最终转换值。我修复了该错误并将更新推送到遥控器。请检查它是否有效。
【解决方案2】:

我的做法是让导航控制器完全透明。这样,包含的视图控制器的动画应该会产生您想要的效果。

编辑:您还可以通过将 containerView 限制在导航栏下方来获得“白色内容”。在示例代码中我做到了。推送随机选择一种颜色并随机赋予容器视图白色或透明。您将看到 gif 中的所有场景都包含在此示例中。

在操场上试试这个:

import UIKit
import PlaygroundSupport

class MyViewController : UIViewController {
  var containerView: UIView!

  override func loadView() {
    let view = UIView()
    view.backgroundColor = .white

    containerView = UIView()
    containerView.backgroundColor = .white
    containerView.translatesAutoresizingMaskIntoConstraints = false

    view.addSubview(containerView)
    containerView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor).isActive = true
    containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
    containerView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
    containerView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true

    let button = UIButton()
    button.frame = CGRect(x: 150, y: 200, width: 200, height: 20)
    button.setTitle("push", for: .normal)
    button.setTitleColor(.black, for: .normal)
    button.addTarget(self, action: #selector(push), for: .touchUpInside)
    containerView.addSubview(button)
    self.view = view
  }

  @objc func push() {
    let colors: [UIColor] = [.yellow, .red, .blue, .purple, .gray, .darkGray, .green]
    let controller = MyViewController()
    controller.title = "Second"

    let randColor = Int(arc4random()%UInt32(colors.count))
    controller.view.backgroundColor = colors[randColor]

    let clearColor: Bool = (arc4random()%2) == 1
    controller.containerView.backgroundColor = clearColor ? .clear: .white
    navigationController?.pushViewController(controller, animated: true)
  }
}
// Present the view controller in the Live View window

let controller = MyViewController()
controller.view.backgroundColor = .white
let navController = UINavigationController(rootViewController: controller)
navController.navigationBar.setBackgroundImage(UIImage(), for: .default)
controller.title = "First"


PlaygroundPage.current.liveView = navController

【讨论】:

  • 正如我之前提到的,导航栏不可能一直完全透明。检查我的编辑,我希望它有助于澄清问题。
  • 您可以让导航栏始终透明,并获得与 gif 相同的结果。您只需要一个具有顶部约束的容器视图 = 导航栏底部以白色作为背景,而主视图控制器背景具有您希望在导航栏下看到的颜色。
  • 我在答案中编辑了代码。我用我之前评论中描述的方法获得了你想要的效果。在操场上试试,让我知道你的想法。 :)
  • 我试过你的游乐场,不得不承认它有效,谢谢! TBH 由于该项目的潜在开销,我并不完全相信,但我会尝试将其付诸实践。
  • 您关心什么?只有一种观点不会影响表演。这实际上是苹果自己广泛使用的一种方法。要说服自己,只需将视图检查器与导航控制器或 UITabViewController 一起使用。你会看到很多容器。
【解决方案3】:

从您的项目中删除此代码:

toVC.navigationController?.navigationBar.setBackgroundImage(
        UIImage.fromColor(color: self.isPresenting ? .yellow : .lightGray),
        for: .default
)

【讨论】:

  • 在删除这部分代码后,导航栏将保持透明,而它必须相应地为yellowlightGray,因为它是更大流程的一部分,所有以前的和连续的视图都呈现在同一个视图上UINavigationController.
猜你喜欢
  • 2011-07-15
  • 2012-04-05
  • 1970-01-01
  • 1970-01-01
  • 2011-03-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-30
相关资源
最近更新 更多