From 6fcb817fb8d4600fa5efa826b3df0b8c61cfe394 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sat, 1 Jun 2019 16:19:09 +0900 Subject: [PATCH] Add the rubberbanding behavior for top & bottom buffer (#144) * Add sample code * Fix updateInteractiveTopConstraint() * {min,max}Y variables are confusing because it's not a value of coordinate Y, but a constant value from the `interactiveTopConstraint`. --- Examples/Samples/Sources/ViewController.swift | 21 ++++++++++- Framework/Sources/FloatingPanel.swift | 3 +- Framework/Sources/FloatingPanelBehavior.swift | 12 +++++- Framework/Sources/FloatingPanelLayout.swift | 37 ++++++++++++++----- 4 files changed, 61 insertions(+), 12 deletions(-) diff --git a/Examples/Samples/Sources/ViewController.swift b/Examples/Samples/Sources/ViewController.swift index b07b97d9..3826c0e3 100644 --- a/Examples/Samples/Sources/ViewController.swift +++ b/Examples/Samples/Sources/ViewController.swift @@ -956,6 +956,15 @@ extension TabBarContentViewController: FloatingPanelControllerDelegate { } } + func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior? { + switch self.tabBarItem.tag { + case 1: + return TwoTabBarPanelBehavior() + default: + return nil + } + } + func floatingPanelDidMove(_ vc: FloatingPanelController) { guard self.tabBarItem.tag == 2 else { return } @@ -1076,19 +1085,29 @@ class TwoTabBarPanelLayout: FloatingPanelLayout { var supportedPositions: Set { return [.full, .half] } + var topInteractionBuffer: CGFloat { + return 100.0 + } var bottomInteractionBuffer: CGFloat { return 261.0 - 22.0 } func insetFor(position: FloatingPanelPosition) -> CGFloat? { switch position { - case .full: return 16.0 + case .full: return 100.0 case .half: return 261.0 default: return nil } } } +class TwoTabBarPanelBehavior: FloatingPanelBehavior { + func allowsRubberBanding(for edge: UIRectEdge) -> Bool { + return (edge == .bottom || edge == .top) + } +} + + class ThreeTabBarPanelLayout: FloatingPanelFullScreenLayout { weak var parentVC: UIViewController! diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index 52b9c19a..1ca1f520 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -427,7 +427,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate let dy = translation.y - initialTranslationY layoutAdapter.updateInteractiveTopConstraint(diff: dy, - allowsTopBuffer: allowsTopBuffer(for: dy)) + allowsTopBuffer: allowsTopBuffer(for: dy), + with: behavior) backdropView.alpha = getBackdropAlpha(with: translation) preserveContentVCLayoutIfNeeded() diff --git a/Framework/Sources/FloatingPanelBehavior.swift b/Framework/Sources/FloatingPanelBehavior.swift index 4bbcdb5f..d9e55717 100644 --- a/Framework/Sources/FloatingPanelBehavior.swift +++ b/Framework/Sources/FloatingPanelBehavior.swift @@ -6,7 +6,7 @@ import UIKit public protocol FloatingPanelBehavior { - /// Asks the behavior object if the floating panel should project a momentum of a user interaction to move the proposed position. + /// Asks the behavior if the floating panel should project a momentum of a user interaction to move the proposed position. /// /// The default implementation of this method returns true. This method is called for a layout to support all positions(tip, half and full). /// Therefore, `proposedTargetPosition` can only be `FloatingPanelPosition.tip` or `FloatingPanelPosition.full`. @@ -57,6 +57,12 @@ public protocol FloatingPanelBehavior { /// /// Default is a spring animator with 1.0 damping ratio. This method is called when FloatingPanelController.isRemovalInteractionEnabled is true. func removalInteractionAnimator(_ fpc: FloatingPanelController, with velocity: CGVector) -> UIViewPropertyAnimator + + + /// Asks the behavior whether the rubber band effect is enabled in moving over a given edge of the surface view. + /// + /// This method allows the behavior to activate the rubber band effect to a given edge of the surface view. By default, the effect is disabled. + func allowsRubberBanding(for edge: UIRectEdge) -> Bool } public extension FloatingPanelBehavior { @@ -114,6 +120,10 @@ public extension FloatingPanelBehavior { initialVelocity: velocity) return UIViewPropertyAnimator(duration: 0, timingParameters: timing) } + + func allowsRubberBanding(for edge: UIRectEdge) -> Bool { + return false + } } private let defaultBehavior = FloatingPanelDefaultBehavior() diff --git a/Framework/Sources/FloatingPanelLayout.swift b/Framework/Sources/FloatingPanelLayout.swift index c0774766..4fa6fd2f 100644 --- a/Framework/Sources/FloatingPanelLayout.swift +++ b/Framework/Sources/FloatingPanelLayout.swift @@ -412,12 +412,12 @@ class FloatingPanelLayoutAdapter { } } - func updateInteractiveTopConstraint(diff: CGFloat, allowsTopBuffer: Bool) { + func updateInteractiveTopConstraint(diff: CGFloat, allowsTopBuffer: Bool, with behavior: FloatingPanelBehavior) { defer { surfaceView.superview!.layoutIfNeeded() // MUST call here to update `surfaceView.frame` } - let minY: CGFloat = { + let topMostConst: CGFloat = { var ret: CGFloat = 0.0 switch layout { case is FloatingPanelIntrinsicLayout, is FloatingPanelFullScreenLayout: @@ -425,12 +425,9 @@ class FloatingPanelLayoutAdapter { default: ret = topY - safeAreaInsets.top } - if allowsTopBuffer { - ret -= layout.topInteractionBuffer - } return max(ret, 0.0) // The top boundary is equal to the related topAnchor. }() - let maxY: CGFloat = { + let bottomMostConst: CGFloat = { var ret: CGFloat = 0.0 switch layout { case is FloatingPanelIntrinsicLayout, is FloatingPanelFullScreenLayout: @@ -438,12 +435,34 @@ class FloatingPanelLayoutAdapter { default: ret = bottomY - safeAreaInsets.top } - ret += layout.bottomInteractionBuffer return min(ret, bottomMaxY) }() - let const = initialConst + diff + let minConst = allowsTopBuffer ? topMostConst - layout.topInteractionBuffer : topMostConst + let maxConst = bottomMostConst + layout.bottomInteractionBuffer + + var const = initialConst + diff + + // Rubberbanding top buffer + if behavior.allowsRubberBanding(for: .top), const < topMostConst { + let buffer = topMostConst - const + const = topMostConst - rubberbandEffect(for: buffer, base: vc.view.bounds.height) + } + + // Rubberbanding bottom buffer + if behavior.allowsRubberBanding(for: .bottom), const > bottomMostConst { + let buffer = const - bottomMostConst + const = bottomMostConst + rubberbandEffect(for: buffer, base: vc.view.bounds.height) + } + + interactiveTopConstraint?.constant = max(minConst, min(maxConst, const)) + } - interactiveTopConstraint?.constant = max(minY, min(maxY, const)) + // According to @chpwn's tweet: https://twitter.com/chpwn/status/285540192096497664 + // x = distance from the edge + // c = constant value, UIScrollView uses 0.55 + // d = dimension, either width or height + private func rubberbandEffect(for buffer: CGFloat, base: CGFloat) -> CGFloat { + return (1.0 - (1.0 / ((buffer * 0.55 / base) + 1.0))) * base } func activateLayout(of state: FloatingPanelPosition) {