make the reply swipe reliable and add a telegram-style plate
- Lock the gesture into a reply swipe once it starts clearly horizontal (1.6 dominance, 12pt) and relax the ratio to 1.05 afterwards, so a slightly diagonal finish no longer cancels the swipe while vertical scrolls stay untouched. - Restyle the composer reply banner as a rounded telegram-like card with the reply header, close button and two-line quote preview. - Cover the lock/follow behaviour in MessageReplySwipeTests.
This commit is contained in:
1 parent
0ef77f7f6a
commit
2130a76735
4 files changed
+462
-169
No files matched your search
@@ -2,16 +2,30 @@ import CoreGraphics
|
||||
|
||||
struct MessageReplySwipePolicy {
|
||||
static let triggerDistance: CGFloat = 64
|
||||
/// Horizontal travel required before the reply indicator appears. Keeps
|
||||
/// the per-cell state untouched during ordinary vertical scrolling.
|
||||
/// Horizontal travel required before the indicator starts following the
|
||||
/// finger; keeps per-cell state untouched during ordinary scrolling.
|
||||
static let activationDistance: CGFloat = 24
|
||||
static let maximumOffset: CGFloat = 76
|
||||
/// The drag must be clearly horizontal: a mostly vertical timeline scroll
|
||||
/// (even a diagonal one) must never arm the reply swipe or move bubbles.
|
||||
static let horizontalDominance: CGFloat = 2.0
|
||||
/// Ratio required to lock the gesture into a reply swipe. Mostly vertical
|
||||
/// timeline scrolls (even diagonal ones) never reach it.
|
||||
static let lockDominance: CGFloat = 1.6
|
||||
/// Relaxed ratio once locked, so the finger can drift while finishing
|
||||
/// the swipe without it being cancelled by the scroll view.
|
||||
static let followDominance: CGFloat = 1.05
|
||||
|
||||
static func offset(for translation: CGSize) -> CGFloat {
|
||||
guard isHorizontal(translation) else { return 0 }
|
||||
/// Whether this translation can lock the gesture as a horizontal swipe.
|
||||
static func canLock(_ translation: CGSize) -> Bool {
|
||||
isHorizontal(translation, dominance: lockDominance)
|
||||
&& abs(translation.width) >= activationDistance * 0.5
|
||||
}
|
||||
|
||||
static func isHorizontal(_ translation: CGSize) -> Bool {
|
||||
isHorizontal(translation, dominance: lockDominance)
|
||||
}
|
||||
|
||||
static func offset(locked: Bool, translation: CGSize) -> CGFloat {
|
||||
let dominance = locked ? followDominance : lockDominance
|
||||
guard isHorizontal(translation, dominance: dominance) else { return 0 }
|
||||
let width = abs(translation.width)
|
||||
guard width >= activationDistance else { return 0 }
|
||||
let distance = min((width - activationDistance) * 0.72 + 12, maximumOffset)
|
||||
@@ -19,20 +33,24 @@ struct MessageReplySwipePolicy {
|
||||
}
|
||||
|
||||
static func shouldReply(
|
||||
locked: Bool,
|
||||
translation: CGSize,
|
||||
predictedEndTranslation: CGSize
|
||||
) -> Bool {
|
||||
let candidate = abs(predictedEndTranslation.width) > abs(translation.width)
|
||||
? predictedEndTranslation
|
||||
: translation
|
||||
return isHorizontal(candidate) && abs(candidate.width) >= triggerDistance
|
||||
let dominance = locked ? followDominance : lockDominance
|
||||
return isHorizontal(candidate, dominance: dominance)
|
||||
&& abs(candidate.width) >= triggerDistance
|
||||
}
|
||||
|
||||
static func progress(for offset: CGFloat) -> CGFloat {
|
||||
min(1, abs(offset) / triggerDistance)
|
||||
}
|
||||
|
||||
private static func isHorizontal(_ translation: CGSize) -> Bool {
|
||||
abs(translation.width) > abs(translation.height) * horizontalDominance
|
||||
private static func isHorizontal(_ translation: CGSize, dominance: CGFloat) -> Bool {
|
||||
abs(translation.width) > abs(translation.height) * dominance
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user