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

在上一篇文章中,我研究了如何对新控制器的显示进行动画处理。
我们认为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 } 
同时,您需要表明隐藏已变得可管理(我们在上一篇文章中已经做到这一点):
 
处理手势
让我们从闭合手势开始:如果向下拖动面板,则闭合动画将开始,手指的移动会影响闭合程度。
UIPercentDrivenInteractiveTransition允许UIPercentDrivenInteractiveTransition捕获过渡动画并对其进行手动控制。 它具有update , finish , cancel方法。 在其子类中方便进行手势处理。
手势处理
 private func handleDismiss(recognizer r: UIPanGestureRecognizer) { switch r.state { case .began: pause()  
.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 {  
 .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()  
我按下按钮并立即向上滑动以取消隐藏:

停止显示控制器
相反的情况:该卡开始出现,但我们不需要它。 我们将其捕获并向下滑动即可发送。 您可以按照相同的步骤中断控制器显示的动画。
将驱动程序返回为交互式显示控制器:
 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 } } 
没有枚举有可能吗?看来我们可以依赖于isBeingPresented和isBeingDismissed控制器的属性。 但是它们仅显示过程,我们还需要可能的方向:在交互式关闭的开始,两个值都将为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: … } 
为了使代码正常工作,没有足够的链接到presentingController和presentedController 。 创建手势时,我们将传递它们,并添加UIScreenEdgePanGestureRecognizer :
 func linkPresentationGesture(to presentedController: UIViewController, presentingController: UIViewController) { self.presentedController = presentedController self.presentingController = presentingController  
您可以在创建PanelTransition时转移控制器:
 class PanelTransition: NSObject, UIViewControllerTransitioningDelegate { init(presented: UIViewController, presenting: UIViewController) { driver.linkPresentationGesture(to: presented, presentingController: presenting) } private let driver = TransitionDriver() } 
仍然可以PanelTransition创建PanelTransition :
- 让我们在viewDidLoad创建一个child控制器,因为我们可能随时需要一个控制器。
- 创建PanelTransition。 在他的构造函数中,手势已绑定到控制器。
- 放下子控制器的transitioningDelegate。
- 为了进行培训,我从下面滑动,但这与在iPhone X和控制中心上关闭应用程序冲突。 使用- preferredScreenEdgesDeferringSystemGestures禁用了从下方滑动系统的功能。
 
 -  - class ParentViewController: UIViewController { private var child: ChildViewController! private var transition: PanelTransition! override func viewDidLoad() { super.viewDidLoad() child = ChildViewController() 
 
 - 更改之后,事实证明是有问题的:第一次关闭面板后,它永远处于- TransitionDirection.dismiss的状态。 将控制器隐藏在- PresentationController后,设置正确的状态:
 
 -  - override func dismissalTransitionDidEnd(_ completed: Bool) { super.dismissalTransitionDidEnd(completed) if completed { driver.direction = .present } }
 
 - 交互式显示代码可以在单独的线程中查看。 看起来像这样: 
 

结论
结果,我们可以向控制器显示动画中断,并且用户可以控制屏幕上发生的事情。 这样更好,因为动画不再阻塞接口,可以取消甚至加速动画。
可以在github上看到一个例子。
订阅Dodo Pizza Mobile频道。