不要弹出! iOS中的可中断过渡

您是否也对应用程序中的弹出窗口感到生气? 在本文中,我将展示如何交互地隐藏和显示弹出窗口,使动画可中断并且不会激怒我的客户。



在上一篇文章中,我研究了如何对新控制器的显示进行动画处理。


我们认为viewController可以动画显示和隐藏:



现在,我们将教他应对隐藏的手势。


互动过渡


添加近距离手势


要教控制器以交互方式关闭,您需要添加一个手势并对其进行处理。 所有工作将在TransitionDriver类中进行:


 class TransitionDriver: UIPercentDrivenInteractiveTransition { func link(to controller: UIViewController) { presentedController = controller panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handle(recognizer:))) presentedController?.view.addGestureRecognizer(panRecognizer!) } private var presentedController: UIViewController? private var panRecognizer: UIPanGestureRecognizer? } 

您可以在PanelTransition内部的DimmPresentationController的位置附加一个处理程序:


 private let driver = TransitionDriver() func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { driver.link(to: presented) let presentationController = DimmPresentationController(presentedViewController: presented, presenting: presenting) return presentationController } 

同时,您需要表明隐藏已变得可管理(我们在上一篇文章中已经做到这一点):


 // PanelTransition.swift func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { return driver } 

处理手势


让我们从闭合手势开始:如果向下拖动面板,则闭合动画将开始,手指的移动会影响闭合程度。
UIPercentDrivenInteractiveTransition允许UIPercentDrivenInteractiveTransition捕获过渡动画并对其进行手动控制。 它具有updatefinishcancel方法。 在其子类中方便进行手势处理。


手势处理


 private func handleDismiss(recognizer r: UIPanGestureRecognizer) { switch r.state { case .began: pause() //   percentComplete   0 let isRunning = percentComplete != 0 if !isRunning { presentingController?.present(presentedController!, animated: true) } case .changed: update(percentComplete + r.incrementToBottom(maxTranslation: maxTranslation)) case .ended, .cancelled: if r.isProjectedToDownHalf(maxTranslation: maxTranslation) { finish() } else { cancel() } case .failed: cancel() default: break } } 

.begin
以最常见的方式开始解散。 我们在link(to:)方法中将link(to:)保存到控制器


.changed
计算增量并将其传递给update方法。 可接受的值可以在0到1之间变化,因此我们将通过interactionControllerForDismissal(using:)方法控制动画的完成程度。 计算是在手势扩展中进行的,因此代码变得更加清晰。


手势计算
 private extension UIPanGestureRecognizer { func incrementToBottom(maxTranslation: CGFloat) -> CGFloat { let translation = self.translation(in: view).y setTranslation(.zero, in: nil) let percentIncrement = translation / maxTranslation return percentIncrement } } 

计算基于maxTranslation ,我们将其计算为显示的控制器的高度:


 var maxTranslation: CGFloat { return presentedController?.view.frame.height ?? 0 } 

.end
我们看一下手势的完整性。 完成规则:如果超过一半已转移,则关闭。 在这种情况下,不仅必须通过当前坐标来考虑偏移量,还必须通过velocity来考虑偏移量。 因此,我们了解了用户的意图:他可能不会走到中间,但会非常向下滑动。 反之亦然:取下,但向上滑动即可返回。


ProjectedLocation计算
 private extension UIPanGestureRecognizer { func isProjectedToDownHalf(maxTranslation: CGFloat) -> Bool { let endLocation = projectedLocation(decelerationRate: .fast) let isPresentationCompleted = endLocation.y > maxTranslation / 2 return isPresentationCompleted } func projectedLocation(decelerationRate: UIScrollView.DecelerationRate) -> CGPoint { let velocityOffset = velocity(in: view).projectedOffset(decelerationRate: .normal) let projectedLocation = location(in: view!) + velocityOffset return projectedLocation } } extension CGPoint { func projectedOffset(decelerationRate: UIScrollView.DecelerationRate) -> CGPoint { return CGPoint(x: x.projectedOffset(decelerationRate: decelerationRate), y: y.projectedOffset(decelerationRate: decelerationRate)) } } extension CGFloat { // Velocity value func projectedOffset(decelerationRate: UIScrollView.DecelerationRate) -> CGFloat { // Magic formula from WWDC let multiplier = 1 / (1 - decelerationRate.rawValue) / 1000 return self * multiplier } } extension CGPoint { static func +(left: CGPoint, right: CGPoint) -> CGPoint { return CGPoint(x: left.x + right.x, y: left.y + right.y) } } 

.cancelled如果您锁定手机屏幕或他们打电话时会发生。 您可以将其作为.ended块处理或取消操作。
.failed如果该手势被另一个手势取消,则会发生。 因此,例如,拖动手势可以取消轻击手势。
.possible手势的初始状态,通常不需要太多工作。


现在也可以通过滑动关闭面板,但是dismiss按钮已dismiss 。 发生这种情况是因为TransitionDriver有一个wantsInteractiveStart属性,默认情况下为true 。 这对于刷卡是正常的,但是会阻止通常的dismiss


让我们根据手势状态来分解行为。 如果手势开始,则这是一个交互式关闭,如果手势没有开始,则通常是:


 override var wantsInteractiveStart: Bool { get { let gestureIsActive = panRecognizer?.state == .began return gestureIsActive } set { } } 

现在,用户可以控制隐藏了:



中断过渡


假设我们开始关闭信用卡,但改变了主意并想返回。 很简单:在.began状态下, .began调用pause()停止。


但是您需要分开两种情况:


  • 当我们开始隐藏手势时;
  • 当我们中断当前的。

为此,在停止之后,检查percentComplete:如果它为0,则我们开始手动关闭卡,另外我们需要调用dismiss 。 如果不为0,则表明隐藏已经开始,仅停止动画就足够了:


 case .began: pause() // Pause allows to detect percentComplete if percentComplete == 0 { presentedController?.dismiss(animated: true) } 

我按下按钮并立即向上滑动以取消隐藏:


停止显示控制器


相反的情况:该卡开始出现,但我们不需要它。 我们将其捕获并向下滑动即可发送。 您可以按照相同的步骤中断控制器显示的动画。


将驱动程序返回为交互式显示控制器:


 func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { return driver } 

处理手势,但具有反向偏差和完整性值:


 private func handlePresentation(recognizer r: UIPanGestureRecognizer) { switch r.state { case .began: pause() case .changed: let increment = -r.incrementToBottom(maxTranslation: maxTranslation) update(percentComplete + increment) case .ended, .cancelled: if r.isProjectedToDownHalf(maxTranslation: maxTranslation) { cancel() } else { finish() } case .failed: cancel() default: break } } 

为了分开显示和隐藏,我输入了当前动画方向的枚举:


 enum TransitionDirection { case present, dismiss } 

该属性存储在TransitionDriver并影响将使用的手势处理程序:


 var direction: TransitionDirection = .present @objc private func handle(recognizer r: UIPanGestureRecognizer) { switch direction { case .present: handlePresentation(recognizer: r) case .dismiss: handleDismiss(recognizer: r) } } 

它还会影响wantsInteractiveStart 。 我们不打算向控制器显示手势,因此我们为.present返回false


 override var wantsInteractiveStart: Bool { get { switch direction { case .present: return false case .dismiss: let gestureIsActive = panRecognizer?.state == .began return gestureIsActive } } set { } } 

好了,完全显示控制器后,仍然可以更改手势的方向。 最好的地方是PresentationController


 override func presentationTransitionDidEnd(_ completed: Bool) { super.presentationTransitionDidEnd(completed) if completed { driver.direction = .dismiss } } 

没有枚举有可能吗?

看来我们可以依赖于isBeingPresentedisBeingDismissed控制器的属性。 但是它们仅显示过程,我们还需要可能的方向:在交互式关闭的开始,两个值都将为false ,并且我们已经需要知道这是关闭的方向。 这可以通过检查控制器层次结构的其他条件来解决,但是通过enum进行显式分配似乎是一个更简单的解决方案。


现在您可以中断节目的动画。 我按下按钮并立即向下滑动:



手势显示


如果要为应用程序制作汉堡菜单,则很可能希望通过手势显示它。 这就像交互式隐藏一样,但以手势(而不是dismiss称之为present
让我们从头开始。 在handlePresentation(recognizer:)显示控制器:


 case .began: pause() let isRunning = percentComplete != 0 if !isRunning { presentingController?.present(presentedController!, animated: true) } 

让我们互动展示:


 override var wantsInteractiveStart: Bool { get { switch direction { case .present: let gestureIsActive = screenEdgePanRecognizer?.state == .began return gestureIsActive case .dismiss: … } 

为了使代码正常工作,没有足够的链接到presentingControllerpresentedController 。 创建手势时,我们将传递它们,并添加UIScreenEdgePanGestureRecognizer


 func linkPresentationGesture(to presentedController: UIViewController, presentingController: UIViewController) { self.presentedController = presentedController self.presentingController = presentingController //    panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handle(recognizer:))) presentedController.view.addGestureRecognizer(panRecognizer!) //    screenEdgePanRecognizer = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handlePresentation(recognizer:))) screenEdgePanRecognizer!.edges = .bottom presentingController.view.addGestureRecognizer(screenEdgePanRecognizer!) } 

您可以在创建PanelTransition时转移控制器:


 class PanelTransition: NSObject, UIViewControllerTransitioningDelegate { init(presented: UIViewController, presenting: UIViewController) { driver.linkPresentationGesture(to: presented, presentingController: presenting) } private let driver = TransitionDriver() } 

仍然可以PanelTransition创建PanelTransition


  1. 让我们在viewDidLoad创建一个child控制器,因为我们可能随时需要一个控制器。
  2. 创建PanelTransition 。 在他的构造函数中,手势已绑定到控制器。
  3. 放下子控制器的transitioningDelegate。
  4. 为了进行培训,我从下面滑动,但这与在iPhone X和控制中心上关闭应用程序冲突。 使用preferredScreenEdgesDeferringSystemGestures禁用了从下方滑动系统的功能。


     class ParentViewController: UIViewController { private var child: ChildViewController! private var transition: PanelTransition! override func viewDidLoad() { super.viewDidLoad() child = ChildViewController() // 1 transition = PanelTransition(presented: child, presenting: self) // 2 // Setup the child child.modalPresentationStyle = .custom child.transitioningDelegate = transition // 3 } override var preferredScreenEdgesDeferringSystemGestures: UIRectEdge { return .bottom // 4 } } 

    更改之后,事实证明是有问题的:第一次关闭面板后,它永远处于TransitionDirection.dismiss的状态。 将控制器隐藏在PresentationController后,设置正确的状态:


     override func dismissalTransitionDidEnd(_ completed: Bool) { super.dismissalTransitionDidEnd(completed) if completed { driver.direction = .present } } 

    交互式显示代码可以在单独的线程中查看。 看起来像这样:




结论


结果,我们可以向控制器显示动画中断,并且用户可以控制屏幕上发生的事情。 这样更好,因为动画不再阻塞接口,可以取消甚至加速动画。


可以在github上看到一个例子


订阅Dodo Pizza Mobile频道。

Source: https://habr.com/ru/post/zh-CN465073/


All Articles