Skip to content

Commit 76b3054

Browse files
committed
Connect drawer: the content picks up the fling on the fling's own clock
The carried momentum started the moment the finger lifted, ahead of the sheet, and on Android the same carry-over read as a phantom flick once the sheet had stopped. Time the pick-up like one fling instead: the content starts at the moment the fling would have covered the sheet's remaining travel (closed form on UIScrollView's exponential deceleration), or when the sheet arrives if that comes first, and continues the curve from there. The momentum driver takes the delay and holds until the display link reaches it.
1 parent d342042 commit 76b3054

3 files changed

Lines changed: 38 additions & 4 deletions

File tree

app/network/Main/Connect/ConnectSheetMomentum.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,23 @@ enum ConnectSheetMomentum {
5353
return speed / 1000 * decelerationRate / (1 - decelerationRate)
5454
}
5555

56+
/// How long, in seconds on the fling's own clock, a fling of `speed`
57+
/// takes to cover `travelled` points; nil when it would stop short.
58+
static func travelTime(speed: CGFloat, travelled: CGFloat, decelerationRate: CGFloat) -> TimeInterval? {
59+
guard speed > 0, 0 < decelerationRate, decelerationRate < 1 else {
60+
return nil
61+
}
62+
guard travelled > 0 else {
63+
return 0
64+
}
65+
let total = projectedDistance(speed: speed, decelerationRate: decelerationRate)
66+
guard travelled < total else {
67+
return nil
68+
}
69+
// distance(t) = total * (1 - rate^t) with t in milliseconds
70+
return log(1 - travelled / total) / log(decelerationRate) / 1000
71+
}
72+
5673
/// The speed a fling of `speed` has left after covering `travelled`
5774
/// points; zero when it would have stopped within that distance.
5875
static func residualSpeed(speed: CGFloat, travelled: CGFloat, decelerationRate: CGFloat) -> CGFloat {

app/network/Main/Connect/ConnectView-iOS.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ struct ConnectView_iOS: View {
8484
// and the top of the tab bar at the collapsed peek — the same 12pt on
8585
// every device, matching the Android drawer
8686
private let sheetFoldGap: CGFloat = 12
87+
// how long the sheet takes to reach its expanded position after a
88+
// release (the spring below settles visibly within this)
89+
private let sheetOpenDuration: TimeInterval = 0.3
8790

8891

8992
init(
@@ -630,11 +633,17 @@ struct ConnectView_iOS: View {
630633
}
631634
}
632635

636+
// The content picks up the fling on the fling's own clock: at the moment
637+
// the fling would have covered the sheet's remaining travel, or when the
638+
// sheet arrives if that comes first, so it never sets off on its own
639+
// after the sheet has visibly stopped.
633640
private func carryFlingIntoSheetContent(speed: CGFloat, afterTravelling travelled: CGFloat, maxHeight: CGFloat) {
634641
guard let scrollView = sheetScrollRef.scrollView else { return }
635642
let rate = UIScrollView.DecelerationRate.normal.rawValue
636643
let residual = ConnectSheetMomentum.residualSpeed(speed: speed, travelled: travelled, decelerationRate: rate)
637644
guard residual > 0 else { return }
645+
let travelTime = ConnectSheetMomentum.travelTime(speed: speed, travelled: travelled, decelerationRate: rate) ?? 0
646+
let delay = min(travelTime, sheetOpenDuration)
638647
// the sheet is still growing to its expanded height, so the far end
639648
// of the scroll is measured against the expanded content area, not
640649
// the bounds of this instant
@@ -644,7 +653,8 @@ struct ConnectView_iOS: View {
644653
scrollView: scrollView,
645654
speed: residual,
646655
maxOffset: maxOffset,
647-
decelerationRate: rate
656+
decelerationRate: rate,
657+
delay: delay
648658
)
649659
}
650660

app/network/Shared/Views/Components/SheetScrollMomentum.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,17 +79,21 @@ final class ScrollMomentumDriver {
7979
private var maxOffset: CGFloat = 0
8080
private var decelerationRate: CGFloat = UIScrollView.DecelerationRate.normal.rawValue
8181
private var lastTimestamp: CFTimeInterval = 0
82+
private var startAt: CFTimeInterval = 0
8283

83-
/// Starts scrolling `scrollView` downward at `speed` points per second,
84-
/// never past `maxOffset`.
85-
func start(scrollView: UIScrollView, speed: CGFloat, maxOffset: CGFloat, decelerationRate: CGFloat) {
84+
/// Starts scrolling `scrollView` downward at `speed` points per second
85+
/// after `delay` seconds, never past `maxOffset`. The delay lets the
86+
/// content pick up a fling exactly where the fling's own clock says the
87+
/// sheet's travel ends, so the motion reads as one scroll.
88+
func start(scrollView: UIScrollView, speed: CGFloat, maxOffset: CGFloat, decelerationRate: CGFloat, delay: TimeInterval = 0) {
8689
stop()
8790
guard speed > 0, maxOffset > scrollView.contentOffset.y else { return }
8891
self.scrollView = scrollView
8992
self.speed = speed
9093
self.maxOffset = maxOffset
9194
self.decelerationRate = decelerationRate
9295
lastTimestamp = 0
96+
startAt = CACurrentMediaTime() + max(0, delay)
9397
let link = CADisplayLink(target: self, selector: #selector(step(_:)))
9498
link.add(to: .main, forMode: .common)
9599
displayLink = link
@@ -107,6 +111,9 @@ final class ScrollMomentumDriver {
107111
stop()
108112
return
109113
}
114+
if link.timestamp < startAt {
115+
return
116+
}
110117
if lastTimestamp == 0 {
111118
lastTimestamp = link.timestamp
112119
return

0 commit comments

Comments
 (0)