自定义交互过渡动画
custom interactive transition animation
我想实现两个视图控制器之间的交互转换。我希望它是模态或现在的过渡。
- 我希望应用程序从第一个视图控制器开始,并允许用户向下滑动以引入第二个视图控制器
- 第二个视图控制器应该进入并覆盖当前(第一个视图控制器)而不是将其移开
我知道我需要使用以下内容。
transitioningDelegate
animationController(forPresented:presenting:Source:)
interactionControllerForPresentation(Using:)
UIPercentDrivenInteractiveTransition
我不知道如何实现这一切。我似乎在 swift 中找不到任何有用的东西或任何工作示例 3. 现在我创建了一个简单的单视图应用程序,它有两个视图控制器 VC1(蓝色背景)和 VC2(黄色背景)来轻松测试任何可能的解决方案。
请参阅 WWDC 2013 视频 Custom Transitions Using View Controllers for discussion of the transition delegate, animation controller, and interaction controller. See WWDC 2014 videos View Controller Advancements in iOS 8 and A Look Inside Presentation Controllers 了解演示控制器(您也应该使用)。
基本思想是创建一个转换委托对象,该对象标识将用于自定义转换的动画控制器、交互控制器和演示控制器:
class TransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate {
/// Interaction controller
///
/// If gesture triggers transition, it will set will manage its own
/// `UIPercentDrivenInteractiveTransition`, but it must set this
/// reference to that interaction controller here, so that this
/// knows whether it's interactive or not.
weak var interactionController: UIPercentDrivenInteractiveTransition?
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return PullDownAnimationController(transitionType: .presenting)
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return PullDownAnimationController(transitionType: .dismissing)
}
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return PresentationController(presentedViewController: presented, presenting: presenting)
}
func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactionController
}
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactionController
}
}
然后您只需指定正在使用的自定义转换以及应使用的转换委托。您可以在实例化目标视图控制器时执行此操作,或者您可以将其指定为目标视图控制器的 init
的一部分,例如:
class SecondViewController: UIViewController {
let customTransitionDelegate = TransitioningDelegate()
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
modalPresentationStyle = .custom
transitioningDelegate = customTransitionDelegate
}
...
}
动画控制器指定动画的详细信息(如何设置动画、non-interactive 转换所用的持续时间等):
class PullDownAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
enum TransitionType {
case presenting
case dismissing
}
let transitionType: TransitionType
init(transitionType: TransitionType) {
self.transitionType = transitionType
super.init()
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let inView = transitionContext.containerView
let toView = transitionContext.view(forKey: .to)!
let fromView = transitionContext.view(forKey: .from)!
var frame = inView.bounds
switch transitionType {
case .presenting:
frame.origin.y = -frame.size.height
toView.frame = frame
inView.addSubview(toView)
UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
toView.frame = inView.bounds
}, completion: { finished in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
case .dismissing:
toView.frame = frame
inView.insertSubview(toView, belowSubview: fromView)
UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
frame.origin.y = -frame.size.height
fromView.frame = frame
}, completion: { finished in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.5
}
}
上面的动画控制器处理呈现和解散,但如果感觉太复杂,理论上你可以将它分成两个 classes,一个用于呈现,另一个用于解散。但我不喜欢两个不同的 class 如此紧密地耦合,所以我将承担 animateTransition
的轻微复杂性的成本,以确保它们都很好地封装在一个 class.
无论如何,我们想要的下一个对象是呈现控制器。在这种情况下,表示控制器告诉我们从视图层次结构中删除原始视图控制器的视图。 (在这种情况下,我们这样做是因为您要过渡到的场景恰好占据了整个屏幕,因此无需在视图层次结构中保留旧视图。)如果您要添加任何其他额外的 chrome(例如添加 dimming/blurring 视图等),这将属于演示控制器。
无论如何,在这种情况下,呈现控制器非常简单:
class PresentationController: UIPresentationController {
override var shouldRemovePresentersView: Bool { return true }
}
最后,您可能想要一个手势识别器:
- 实例化
UIPercentDrivenInteractiveTransition
;
- 自己启动转换;
- 随着手势的进行更新
UIPercentDrivenInteractiveTransition
;
- 在手势完成时取消或结束交互过渡;
- 完成后删除
UIPercentDrivenInteractiveTransition
(以确保它不会挥之不去,因此它不会干扰您稍后可能想做的任何 non-interactive 转换......这是一个微妙的容易被忽视的小点)。
所以 "presenting" 视图控制器可能有一个手势识别器,可以做类似的事情:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let panDown = UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
view.addGestureRecognizer(panDown)
}
var interactionController: UIPercentDrivenInteractiveTransition?
// pan down transitions to next view controller
func handleGesture(_ gesture: UIPanGestureRecognizer) {
let translate = gesture.translation(in: gesture.view)
let percent = translate.y / gesture.view!.bounds.size.height
if gesture.state == .began {
let controller = storyboard!.instantiateViewController(withIdentifier: "SecondViewController") as! SecondViewController
interactionController = UIPercentDrivenInteractiveTransition()
controller.customTransitionDelegate.interactionController = interactionController
show(controller, sender: self)
} else if gesture.state == .changed {
interactionController?.update(percent)
} else if gesture.state == .ended || gesture.state == .cancelled {
let velocity = gesture.velocity(in: gesture.view)
if (percent > 0.5 && velocity.y == 0) || velocity.y > 0 {
interactionController?.finish()
} else {
interactionController?.cancel()
}
interactionController = nil
}
}
}
您可能还想更改它,使其只识别向下的手势(而不是任何旧的平底锅),但希望这能说明这个想法。
并且您可能希望 "presented" 视图控制器具有用于关闭场景的手势识别器:
class SecondViewController: UIViewController {
let customTransitionDelegate = TransitioningDelegate()
required init?(coder aDecoder: NSCoder) {
// as shown above
}
override func viewDidLoad() {
super.viewDidLoad()
let panUp = UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
view.addGestureRecognizer(panUp)
}
// pan up transitions back to the presenting view controller
var interactionController: UIPercentDrivenInteractiveTransition?
func handleGesture(_ gesture: UIPanGestureRecognizer) {
let translate = gesture.translation(in: gesture.view)
let percent = -translate.y / gesture.view!.bounds.size.height
if gesture.state == .began {
interactionController = UIPercentDrivenInteractiveTransition()
customTransitionDelegate.interactionController = interactionController
dismiss(animated: true)
} else if gesture.state == .changed {
interactionController?.update(percent)
} else if gesture.state == .ended {
let velocity = gesture.velocity(in: gesture.view)
if (percent > 0.5 && velocity.y == 0) || velocity.y < 0 {
interactionController?.finish()
} else {
interactionController?.cancel()
}
interactionController = nil
}
}
@IBAction func didTapButton(_ sender: UIButton) {
dismiss(animated: true)
}
}
有关上述代码的演示,请参阅 https://github.com/robertmryan/SwiftCustomTransitions。
看起来像:
但是,归根结底,自定义转场有点复杂,所以我再次向您推荐那些原始视频。在发布任何进一步的问题之前,请确保您仔细观察了它们。您的大部分问题可能会在这些视频中得到解答。
我想实现两个视图控制器之间的交互转换。我希望它是模态或现在的过渡。
- 我希望应用程序从第一个视图控制器开始,并允许用户向下滑动以引入第二个视图控制器
- 第二个视图控制器应该进入并覆盖当前(第一个视图控制器)而不是将其移开
我知道我需要使用以下内容。
transitioningDelegate
animationController(forPresented:presenting:Source:)
interactionControllerForPresentation(Using:)
UIPercentDrivenInteractiveTransition
我不知道如何实现这一切。我似乎在 swift 中找不到任何有用的东西或任何工作示例 3. 现在我创建了一个简单的单视图应用程序,它有两个视图控制器 VC1(蓝色背景)和 VC2(黄色背景)来轻松测试任何可能的解决方案。
请参阅 WWDC 2013 视频 Custom Transitions Using View Controllers for discussion of the transition delegate, animation controller, and interaction controller. See WWDC 2014 videos View Controller Advancements in iOS 8 and A Look Inside Presentation Controllers 了解演示控制器(您也应该使用)。
基本思想是创建一个转换委托对象,该对象标识将用于自定义转换的动画控制器、交互控制器和演示控制器:
class TransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate {
/// Interaction controller
///
/// If gesture triggers transition, it will set will manage its own
/// `UIPercentDrivenInteractiveTransition`, but it must set this
/// reference to that interaction controller here, so that this
/// knows whether it's interactive or not.
weak var interactionController: UIPercentDrivenInteractiveTransition?
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return PullDownAnimationController(transitionType: .presenting)
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return PullDownAnimationController(transitionType: .dismissing)
}
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return PresentationController(presentedViewController: presented, presenting: presenting)
}
func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactionController
}
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactionController
}
}
然后您只需指定正在使用的自定义转换以及应使用的转换委托。您可以在实例化目标视图控制器时执行此操作,或者您可以将其指定为目标视图控制器的 init
的一部分,例如:
class SecondViewController: UIViewController {
let customTransitionDelegate = TransitioningDelegate()
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
modalPresentationStyle = .custom
transitioningDelegate = customTransitionDelegate
}
...
}
动画控制器指定动画的详细信息(如何设置动画、non-interactive 转换所用的持续时间等):
class PullDownAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
enum TransitionType {
case presenting
case dismissing
}
let transitionType: TransitionType
init(transitionType: TransitionType) {
self.transitionType = transitionType
super.init()
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let inView = transitionContext.containerView
let toView = transitionContext.view(forKey: .to)!
let fromView = transitionContext.view(forKey: .from)!
var frame = inView.bounds
switch transitionType {
case .presenting:
frame.origin.y = -frame.size.height
toView.frame = frame
inView.addSubview(toView)
UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
toView.frame = inView.bounds
}, completion: { finished in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
case .dismissing:
toView.frame = frame
inView.insertSubview(toView, belowSubview: fromView)
UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
frame.origin.y = -frame.size.height
fromView.frame = frame
}, completion: { finished in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.5
}
}
上面的动画控制器处理呈现和解散,但如果感觉太复杂,理论上你可以将它分成两个 classes,一个用于呈现,另一个用于解散。但我不喜欢两个不同的 class 如此紧密地耦合,所以我将承担 animateTransition
的轻微复杂性的成本,以确保它们都很好地封装在一个 class.
无论如何,我们想要的下一个对象是呈现控制器。在这种情况下,表示控制器告诉我们从视图层次结构中删除原始视图控制器的视图。 (在这种情况下,我们这样做是因为您要过渡到的场景恰好占据了整个屏幕,因此无需在视图层次结构中保留旧视图。)如果您要添加任何其他额外的 chrome(例如添加 dimming/blurring 视图等),这将属于演示控制器。
无论如何,在这种情况下,呈现控制器非常简单:
class PresentationController: UIPresentationController {
override var shouldRemovePresentersView: Bool { return true }
}
最后,您可能想要一个手势识别器:
- 实例化
UIPercentDrivenInteractiveTransition
; - 自己启动转换;
- 随着手势的进行更新
UIPercentDrivenInteractiveTransition
; - 在手势完成时取消或结束交互过渡;
- 完成后删除
UIPercentDrivenInteractiveTransition
(以确保它不会挥之不去,因此它不会干扰您稍后可能想做的任何 non-interactive 转换......这是一个微妙的容易被忽视的小点)。
所以 "presenting" 视图控制器可能有一个手势识别器,可以做类似的事情:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let panDown = UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
view.addGestureRecognizer(panDown)
}
var interactionController: UIPercentDrivenInteractiveTransition?
// pan down transitions to next view controller
func handleGesture(_ gesture: UIPanGestureRecognizer) {
let translate = gesture.translation(in: gesture.view)
let percent = translate.y / gesture.view!.bounds.size.height
if gesture.state == .began {
let controller = storyboard!.instantiateViewController(withIdentifier: "SecondViewController") as! SecondViewController
interactionController = UIPercentDrivenInteractiveTransition()
controller.customTransitionDelegate.interactionController = interactionController
show(controller, sender: self)
} else if gesture.state == .changed {
interactionController?.update(percent)
} else if gesture.state == .ended || gesture.state == .cancelled {
let velocity = gesture.velocity(in: gesture.view)
if (percent > 0.5 && velocity.y == 0) || velocity.y > 0 {
interactionController?.finish()
} else {
interactionController?.cancel()
}
interactionController = nil
}
}
}
您可能还想更改它,使其只识别向下的手势(而不是任何旧的平底锅),但希望这能说明这个想法。
并且您可能希望 "presented" 视图控制器具有用于关闭场景的手势识别器:
class SecondViewController: UIViewController {
let customTransitionDelegate = TransitioningDelegate()
required init?(coder aDecoder: NSCoder) {
// as shown above
}
override func viewDidLoad() {
super.viewDidLoad()
let panUp = UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
view.addGestureRecognizer(panUp)
}
// pan up transitions back to the presenting view controller
var interactionController: UIPercentDrivenInteractiveTransition?
func handleGesture(_ gesture: UIPanGestureRecognizer) {
let translate = gesture.translation(in: gesture.view)
let percent = -translate.y / gesture.view!.bounds.size.height
if gesture.state == .began {
interactionController = UIPercentDrivenInteractiveTransition()
customTransitionDelegate.interactionController = interactionController
dismiss(animated: true)
} else if gesture.state == .changed {
interactionController?.update(percent)
} else if gesture.state == .ended {
let velocity = gesture.velocity(in: gesture.view)
if (percent > 0.5 && velocity.y == 0) || velocity.y < 0 {
interactionController?.finish()
} else {
interactionController?.cancel()
}
interactionController = nil
}
}
@IBAction func didTapButton(_ sender: UIButton) {
dismiss(animated: true)
}
}
有关上述代码的演示,请参阅 https://github.com/robertmryan/SwiftCustomTransitions。
看起来像:
但是,归根结底,自定义转场有点复杂,所以我再次向您推荐那些原始视频。在发布任何进一步的问题之前,请确保您仔细观察了它们。您的大部分问题可能会在这些视频中得到解答。