【问题标题】:How to animate Tab bar tab switch with a CrossDissolve slide transition?如何使用 CrossDissolve 幻灯片过渡为选项卡栏选项卡开关设置动画?
【发布时间】:2017-11-04 21:20:36
【问题描述】:

我正在尝试在UITabBarController 上创建一种类似于 Facebook 应用程序的过渡效果。我设法在标签切换上获得了“滚动效果”,但我似乎无法弄清楚如何交叉溶解(或者至少它不起作用)。

这是我当前的代码:

import UIKit

class ScrollingTabBarControllerDelegate: NSObject, UITabBarControllerDelegate {
    func tabBarController(_ tabBarController: UITabBarController, animationControllerForTransitionFrom fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {

        return ScrollingTransitionAnimator(tabBarController: tabBarController, lastIndex: tabBarController.selectedIndex)
    }
}

class ScrollingTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
    weak var transitionContext: UIViewControllerContextTransitioning?
    var tabBarController: UITabBarController!
    var lastIndex = 0

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

    init(tabBarController: UITabBarController, lastIndex: Int) {
        self.tabBarController = tabBarController
        self.lastIndex = lastIndex
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        self.transitionContext = transitionContext

        let containerView = transitionContext.containerView
        let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
        let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)

        containerView.addSubview(toViewController!.view)

        var viewWidth = toViewController!.view.bounds.width

        if tabBarController.selectedIndex < lastIndex {
            viewWidth = -viewWidth
        }

        toViewController!.view.transform = CGAffineTransform(translationX: viewWidth, y: 0)

        UIView.animate(withDuration: self.transitionDuration(using: (self.transitionContext)), delay: 0.0, usingSpringWithDamping: 1.2, initialSpringVelocity: 2.5, options: .transitionCrossDissolve, animations: {
            toViewController!.view.transform = CGAffineTransform.identity
            fromViewController!.view.transform = CGAffineTransform(translationX: -viewWidth, y: 0)
        }, completion: { _ in
            self.transitionContext?.completeTransition(!self.transitionContext!.transitionWasCancelled)
            fromViewController!.view.transform = CGAffineTransform.identity
        })
    }
}

如果有人知道如何让它工作,那就太好了,现在已经尝试了好几天没有进展......:/

编辑:我通过将 UIView.animate 块替换为:

UIView.transition(with: containerView, duration: 0.2, options: .transitionCrossDissolve, animations: {

    toViewController!.view.transform = CGAffineTransform.identity
    fromViewController!.view.transform = CGAffineTransform(translationX: -viewWidth, y: 0)

}, completion: { _ in

    self.transitionContext?.completeTransition(!self.transitionContext!.transitionWasCancelled)
    fromViewController!.view.transform = CGAffineTransform.identity

})

然而,动画真的很卡,不能用:(

编辑 2:对于尝试使用这些 sn-ps 的人,不要忘记为 UITabBarController 连接代理,否则不会发生任何事情。

编辑 3: 我找到了一个 Swift 库,它完全符合我的要求: https://github.com/Interactive-Studio/TransitionableTab

【问题讨论】:

  • 很高兴看到 CSS 和 JavaScript 的解决方案。

标签: ios swift uitabbarcontroller uitabbar


【解决方案1】:

有一种更简单的方法可以做到这一点。在标签栏委托中添加以下代码:

使用 Swift 2、3 和 4

class MySubclassedTabBarController: UITabBarController {

    override func viewDidLoad() {
      super.viewDidLoad()
      delegate = self
    }
}

extension MySubclassedTabBarController: UITabBarControllerDelegate  {
    func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {

        guard let fromView = selectedViewController?.view, let toView = viewController.view else {
          return false // Make sure you want this as false
        }

        if fromView != toView {
          UIView.transition(from: fromView, to: toView, duration: 0.3, options: [.transitionCrossDissolve], completion: nil)
        }

        return true
    }
}

编辑(2018 年 4 月 23 日) 由于这个答案越来越流行,我更新了代码以删除强制解包,这是一种不好的做法,并添加了保护语句。

编辑(2018 年 7 月 11 日) @AlbertoGarcía 是对的。如果您点击标签栏图标两次,您会看到一个空白屏幕。所以我加了一个额外的检查

【讨论】:

  • 谢谢,这行得通,而且代码更简洁,代码更少!有什么方法可以同时制作动画幻灯片? (我正在寻找两者的结合)
  • 使用此方法,除了.transitionCrossDissolve 之外,其他任何转换都不会产生太多效果。
  • @Alfi 只有看到您的代码才能帮助我们回答这个问题。确保您已设置此代码并且正在调用委托。在其中放置断点,看看它是否被调用。如果不是,则意味着您的代码中有问题。
  • 为了使这段代码完美,您需要检查新的视图控制器是否等于当前的视图控制器。如果你不这样做,你将有一个漂亮的黑屏:)
  • 不幸的是,如果您以编程方式在 tabBar 上设置 selectedIndex,这将不起作用。
【解决方案2】:

如果您想使用UIViewControllerAnimatedTransitioning 做一些比UIView.transition 更自定义的事情,请查看this gist

// MyTabController.swift

import UIKit

class MyTabBarController: UITabBarController {

    override func viewDidLoad() {
        super.viewDidLoad()
        delegate = self
    }
}

extension MyTabBarController: UITabBarControllerDelegate {

    func tabBarController(_ tabBarController: UITabBarController, animationControllerForTransitionFrom fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return MyTransition(viewControllers: tabBarController.viewControllers)
    }
}

class MyTransition: NSObject, UIViewControllerAnimatedTransitioning {

    let viewControllers: [UIViewController]?
    let transitionDuration: Double = 1

    init(viewControllers: [UIViewController]?) {
        self.viewControllers = viewControllers
    }

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

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

        guard
            let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
            let fromView = fromVC.view,
            let fromIndex = getIndex(forViewController: fromVC),
            let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to),
            let toView = toVC.view,
            let toIndex = getIndex(forViewController: toVC)
            else {
                transitionContext.completeTransition(false)
                return
        }

        let frame = transitionContext.initialFrame(for: fromVC)
        var fromFrameEnd = frame
        var toFrameStart = frame
        fromFrameEnd.origin.x = toIndex > fromIndex ? frame.origin.x - frame.width : frame.origin.x + frame.width
        toFrameStart.origin.x = toIndex > fromIndex ? frame.origin.x + frame.width : frame.origin.x - frame.width
        toView.frame = toFrameStart

        DispatchQueue.main.async {
            transitionContext.containerView.addSubview(toView)
            UIView.animate(withDuration: self.transitionDuration, animations: {
                fromView.frame = fromFrameEnd
                toView.frame = frame
            }, completion: {success in
                fromView.removeFromSuperview()
                transitionContext.completeTransition(success)
            })
        }
    }

    func getIndex(forViewController vc: UIViewController) -> Int? {
        guard let vcs = self.viewControllers else { return nil }
        for (index, thisVC) in vcs.enumerated() {
            if thisVC == vc { return index }
        }
        return nil
    }
}

【讨论】:

  • 两个观察: 1. 这只有在你有 4 个 viewControllers 时才有效,第五个是 UIMoreNavigationController,并且不会为它返回索引。 2.getIndex可以优化:return self.viewControllers?.firstIndex(of:vc)
  • 最佳解决方案兄弟 :))
  • 在 Swift 5 中工作。太棒了。
  • 它工作得很好,但我有一个问题。当过渡发生时,控制器的顶部栏颜色会发生变化,并会导致烦人的闪光效果。如何解决?请查看视频drive.google.com/file/d/15mlR9NfV_1C8gwi-4vfpV4CxpXbG5BUm/…
【解决方案3】:

我在用户点击和以编程方式调用 selectedIndex = X 时都在努力处理选项卡栏动画,因为在以编程方式设置所选选项卡时,接受的解决方案对我不起作用。

最后我设法通过UITabBarControllerDelegate 和自定义UIViewControllerAnimatedTransitioning 解决了这个问题,如下所示:

extension MainController: UITabBarControllerDelegate {

    public func tabBarController(
            _ tabBarController: UITabBarController,
            animationControllerForTransitionFrom fromVC: UIViewController,
            to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return FadePushAnimator()
    }
}

FadePushAnimator 如下所示:

class FadePushAnimator: NSObject, UIViewControllerAnimatedTransitioning {

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

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard
                let toViewController = transitionContext.viewController(forKey: .to)
                else {
            return
        }

        transitionContext.containerView.addSubview(toViewController.view)
        toViewController.view.alpha = 0

        let duration = self.transitionDuration(using: transitionContext)
        UIView.animate(withDuration: duration, animations: {
            toViewController.view.alpha = 1
        }, completion: { _ in
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        })

    }
}

这种方法支持任何类型的自定义动画,并且在用户点击和以编程方式设置选定选项卡时都有效。 在 Swift 5 上测试。

【讨论】:

  • 我们可以在这里区分以编程方式更改所选索引还是通过用户交互?我只想在代码中调用更改时为更改设置动画。
【解决方案4】:

扩展@gmogames 答案:https://stackoverflow.com/a/45362914/1993937

当通过代码选择标签栏索引时,我无法让它动画化,调用:

tabBarController.setSeletedIndex(0)

似乎没有经历相同的调用层次结构,它跳过了方法:

tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController)

完全没有动画。

在我的代码中,除了在某些情况下手动在代码中设置标签栏项目之外,我还希望为用户点击标签栏项目提供动画过渡。

这是我对上述解决方案的补充,它添加了一种不同的方法来通过将动画转换的代码设置选定的索引:

import Foundation
import UIKit

@objc class CustomTabBarController: UITabBarController {
    override func viewDidLoad() {
        super.viewDidLoad()
        delegate = self
    }

    @objc func set(selectedIndex index : Int) {
        _ = self.tabBarController(self, shouldSelect: self.viewControllers![index])
    }
}

@objc extension CustomTabBarController: UITabBarControllerDelegate  {
    @objc func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {

        guard let fromView = selectedViewController?.view, let toView = viewController.view else {
            return false // Make sure you want this as false
        }

        if fromView != toView {

            UIView.transition(from: fromView, to: toView, duration: 0.3, options: [.transitionCrossDissolve], completion: { (true) in

            })

            self.selectedViewController = viewController
        }

        return true
    }
}

现在打电话

tabBarController.setSelectedWithIndex(1)   

用于代码内动画过渡!

我仍然认为不幸的是,要完成这项工作,我们必须重写一个不是 setter 的方法并在其中操作数据。如果这是我们需要重写以完成此操作的方法,它不会使标签栏控制器具有应有的可扩展性。

【讨论】:

  • 你刚刚救了我的命。谢谢!
【解决方案5】:

因此,几年后,经验更丰富,在针对相同行为重新审视我自己的问题后,我对 Derek 的回答有所改进。

我继承了他的大部分代码(因为这似乎是最好的解决方案)。

我改变了什么

  • 我通过添加toCoverViewfromCoverView 在幻灯片动画中添加了一个crossDissolve 动画(正如我最初想要的那样),这些是另一个视图的快照视图,将用于淡入/out 同时进行。
  • 将框架宽度更改为已经从 75% 开始,而不是必须翻译完整的 100% 宽度,现在它只翻译 25%,这让它感觉更快捷。
  • 添加了 SpringWithDamping 和 initialSpringVelocity 设置。

这些变化让我感觉它与 Facebook 的实施几乎一样接近,我个人对此非常满意。

这是改编后的答案(大部分功劳归功于 Derek,所以一定要支持他):

class MyTabBarController: UITabBarController {

    override func viewDidLoad() {
        super.viewDidLoad()
        delegate = self
    }
}

extension MyTabBarController: UITabBarControllerDelegate {

    func tabBarController(_ tabBarController: UITabBarController, animationControllerForTransitionFrom fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return MyTransition(viewControllers: tabBarController.viewControllers)
    }
}

class MyTransition: NSObject, UIViewControllerAnimatedTransitioning {

    let viewControllers: [UIViewController]?
    let transitionDuration: Double = 0.2

    init(viewControllers: [UIViewController]?) {
        self.viewControllers = viewControllers
    }

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

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

        guard
            let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
            let fromView = fromVC.view,
            let fromIndex = getIndex(forViewController: fromVC),
            let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to),
            let toView = toVC.view,
            let toIndex = getIndex(forViewController: toVC)
            else {
                transitionContext.completeTransition(false)
                return
        }

        let frame = transitionContext.initialFrame(for: fromVC)
        var fromFrameEnd = frame
        var toFrameStart = frame
        let quarterFrame = frame.width * 0.25
        fromFrameEnd.origin.x = toIndex > fromIndex ? frame.origin.x - quarterFrame : frame.origin.x + quarterFrame
        toFrameStart.origin.x = toIndex > fromIndex ? frame.origin.x + quarterFrame : frame.origin.x - quarterFrame
        toView.frame = toFrameStart

        let toCoverView = fromView.snapshotView(afterScreenUpdates: false)
        if let toCoverView = toCoverView {
            toView.addSubview(toCoverView)
        }
        let fromCoverView = toView.snapshotView(afterScreenUpdates: false)
        if let fromCoverView = fromCoverView {
            fromView.addSubview(fromCoverView)
        }

        DispatchQueue.main.async {
            transitionContext.containerView.addSubview(toView)
            UIView.animate(withDuration: self.transitionDuration, delay: 0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.8, options: [.curveEaseOut], animations: {
                fromView.frame = fromFrameEnd
                toView.frame = frame
                toCoverView?.alpha = 0
                fromCoverView?.alpha = 1
            }) { (success) in
                fromCoverView?.removeFromSuperview()
                toCoverView?.removeFromSuperview()
                fromView.removeFromSuperview()
                transitionContext.completeTransition(success)
            }
        }
    }

    func getIndex(forViewController vc: UIViewController) -> Int? {
        guard let vcs = self.viewControllers else { return nil }
        for (index, thisVC) in vcs.enumerated() {
            if thisVC == vc { return index }
        }
        return nil
    }
}

唯一我还没有弄清楚的是如何让它像 Facebook 那样“可中断”。我知道有一个 interruptibleAnimator 函数可以解决这个问题,但我还不能让它工作。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-09-08
    • 2014-09-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多