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

在上一篇文章中,我研究了如何对新控制器的显示进行动画处理。
我们认为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频道。