Skip to content

Commit

Permalink
Add the rubberbanding behavior for top & bottom buffer (#144)
Browse files Browse the repository at this point in the history
* 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`.
  • Loading branch information
scenee authored Jun 1, 2019
1 parent e2ebfd0 commit 6fcb817
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 12 deletions.
21 changes: 20 additions & 1 deletion Examples/Samples/Sources/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down Expand Up @@ -1076,19 +1085,29 @@ class TwoTabBarPanelLayout: FloatingPanelLayout {
var supportedPositions: Set<FloatingPanelPosition> {
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!

Expand Down
3 changes: 2 additions & 1 deletion Framework/Sources/FloatingPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
12 changes: 11 additions & 1 deletion Framework/Sources/FloatingPanelBehavior.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
Expand Down
37 changes: 28 additions & 9 deletions Framework/Sources/FloatingPanelLayout.swift
Original file line number Diff line number Diff line change
Expand Up @@ -412,38 +412,57 @@ 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:
ret = topY
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:
ret = bottomY
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) {
Expand Down

0 comments on commit 6fcb817

Please sign in to comment.