UINavigationBar 彩色动画与推送动画同步
UINavigationBar color animation synchronized with push animation
我想在具有不同 UINavigationBar
背景颜色的视图之间实现流畅的动画效果。嵌入式视图具有与 UINavigationBar
相同的背景颜色,我想模仿 push/pop 过渡动画,例如:
我已经准备好自定义过渡:
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)
}
虽然推送动画效果很好,但弹出动画效果不佳。
问题:
- 为什么在弹出动画之前清除 NavBar 颜色后它仍然是黄色?
- 有没有更好的方法来实现我的目标? (导航栏不能一直透明,因为它只是流程的一部分)
这是我在 GitHub 上的测试项目的 link。
编辑
这是展示所讨论问题的全貌和预期效果的 gif:
从您的项目中删除此代码:
toVC.navigationController?.navigationBar.setBackgroundImage(
UIImage.fromColor(color: self.isPresenting ? .yellow : .lightGray),
for: .default
)
我的方法是让导航控制器完全透明。这样,包含的视图控制器的动画应该可以提供您想要的效果。
编辑:您还可以通过将 containerView 限制在导航栏下方来获得 "white content"。在示例代码中,我这样做了。推送随机选择一种颜色,并随机给容器视图白色或透明。你会看到你的 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
这些组件总是很难定制。我认为,Apple 希望系统组件在每个应用程序中的外观和行为都一样,因为它允许在整个 iOS 环境中保持共享的用户体验。
有时,从头开始实现自己的组件比尝试自定义系统组件更容易。定制通常会很棘手,因为您不确定内部组件是如何设计的。因此,您必须处理大量边缘情况并处理不必要的副作用。
尽管如此,我相信我有适合您情况的解决方案。我已经分叉了您的项目并实施了您描述的行为。
您可以在 GitHub 上找到我的实现。请参阅 animation-implementation
分支。
UINavigationBar
弹出动画不能正常工作的根本原因是UINavigationBar
有它自己的内部动画逻辑。当 UINavigationController's
堆栈改变时,UINavigationController
告诉 UINavigationBar
改变 UINavigationItems
。因此,首先,我们需要为 UINavigationItems
禁用系统动画。可以通过 subclassing 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
创建自定义过渡动画对象并使用 CoreAnimation
CATransaction
.
自定义过渡
您对过渡动画的实现近乎完美,但从我的角度来看,几乎没有遗漏任何细节。在文章 Customizing the Transition Animations 中,您可以找到更多信息。另外,请注意UIViewControllerContextTransitioning
协议中的方法注释。
所以,我的推送动画版本如下所示:
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动画实现差不多。 fromValue
和 toValue
属性的 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 对其进行探索)。确切的 class 在我们的例子中并不重要,它是 UIView
的继承者就足够了。
最后,我们可以直接在 _UIBarBackground
的视图层上添加我们的动画过渡:
let backgroundView = toVC.navigationController?.navigationBar.subviews[0]
backgroundView?.layer.add(transition, forKey: nil)
我想指出,我们正在预测第一个子视图是背景视图。将来可能会更改视图层次结构,请记住这一点。
将两个动画添加到一个 CATransaction
中很重要,因为在这种情况下,这些动画将同时 运行。
您可以在每个视图控制器的 viewWillAppear
方法中设置 UINavigationBar
背景颜色。
这是最终动画的样子:
希望对您有所帮助。
我想在具有不同 UINavigationBar
背景颜色的视图之间实现流畅的动画效果。嵌入式视图具有与 UINavigationBar
相同的背景颜色,我想模仿 push/pop 过渡动画,例如:
我已经准备好自定义过渡:
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)
}
虽然推送动画效果很好,但弹出动画效果不佳。
问题:
- 为什么在弹出动画之前清除 NavBar 颜色后它仍然是黄色?
- 有没有更好的方法来实现我的目标? (导航栏不能一直透明,因为它只是流程的一部分)
这是我在 GitHub 上的测试项目的 link。
编辑
这是展示所讨论问题的全貌和预期效果的 gif:
从您的项目中删除此代码:
toVC.navigationController?.navigationBar.setBackgroundImage(
UIImage.fromColor(color: self.isPresenting ? .yellow : .lightGray),
for: .default
)
我的方法是让导航控制器完全透明。这样,包含的视图控制器的动画应该可以提供您想要的效果。
编辑:您还可以通过将 containerView 限制在导航栏下方来获得 "white content"。在示例代码中,我这样做了。推送随机选择一种颜色,并随机给容器视图白色或透明。你会看到你的 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
这些组件总是很难定制。我认为,Apple 希望系统组件在每个应用程序中的外观和行为都一样,因为它允许在整个 iOS 环境中保持共享的用户体验。
有时,从头开始实现自己的组件比尝试自定义系统组件更容易。定制通常会很棘手,因为您不确定内部组件是如何设计的。因此,您必须处理大量边缘情况并处理不必要的副作用。
尽管如此,我相信我有适合您情况的解决方案。我已经分叉了您的项目并实施了您描述的行为。
您可以在 GitHub 上找到我的实现。请参阅 animation-implementation
分支。
UINavigationBar
弹出动画不能正常工作的根本原因是UINavigationBar
有它自己的内部动画逻辑。当 UINavigationController's
堆栈改变时,UINavigationController
告诉 UINavigationBar
改变 UINavigationItems
。因此,首先,我们需要为 UINavigationItems
禁用系统动画。可以通过 subclassing 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
创建自定义过渡动画对象并使用 CoreAnimation
CATransaction
.
自定义过渡
您对过渡动画的实现近乎完美,但从我的角度来看,几乎没有遗漏任何细节。在文章 Customizing the Transition Animations 中,您可以找到更多信息。另外,请注意UIViewControllerContextTransitioning
协议中的方法注释。
所以,我的推送动画版本如下所示:
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动画实现差不多。 fromValue
和 toValue
属性的 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 对其进行探索)。确切的 class 在我们的例子中并不重要,它是 UIView
的继承者就足够了。
最后,我们可以直接在 _UIBarBackground
的视图层上添加我们的动画过渡:
let backgroundView = toVC.navigationController?.navigationBar.subviews[0]
backgroundView?.layer.add(transition, forKey: nil)
我想指出,我们正在预测第一个子视图是背景视图。将来可能会更改视图层次结构,请记住这一点。
将两个动画添加到一个 CATransaction
中很重要,因为在这种情况下,这些动画将同时 运行。
您可以在每个视图控制器的 viewWillAppear
方法中设置 UINavigationBar
背景颜色。
这是最终动画的样子:
希望对您有所帮助。