added more anomations
This commit is contained in:
1 parent
0d6c562abd
commit
3e990e7af9
4 files changed
+435
-1
No files matched your search
@@ -0,0 +1,82 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
/// Timing and geometry for the Messages-style chat animations.
|
||||
///
|
||||
/// Two effects live here: the staggered rise of the first visible page when a
|
||||
/// conversation opens, and the transition a freshly appended row uses while the
|
||||
/// user already watches the timeline (an outgoing bubble launches from the
|
||||
/// composer's send control, an incoming one springs from its own edge).
|
||||
enum ChatMessageEntrancePolicy {
|
||||
/// A row waits this long behind every newer row below it, so the visible
|
||||
/// page unfolds bottom-up the way Messages does.
|
||||
static let entranceStagger: TimeInterval = 0.035
|
||||
|
||||
/// Rows further from the bottom than this are left alone: past a screenful
|
||||
/// the cascade is invisible and the row may only appear on a later scroll.
|
||||
static let entranceCascadeLimit = 12
|
||||
|
||||
static let entranceRise: CGFloat = 16
|
||||
static let entranceScale: CGFloat = 0.94
|
||||
|
||||
static let sendScale: CGFloat = 0.2
|
||||
/// Launch point sits next to the composer's send control so the bubble
|
||||
/// reads as thrown out of the field instead of fading in place.
|
||||
static let sendRise: CGFloat = 12
|
||||
|
||||
static let receiveScale: CGFloat = 0.4
|
||||
static let receiveRise: CGFloat = 10
|
||||
|
||||
/// Upper bound of the spring used by both effects. The owning view keeps a
|
||||
/// row's entrance state alive for this long so a recycled row that reappears
|
||||
/// while scrolling plays nothing.
|
||||
static let entranceAnimationDuration: TimeInterval = 0.75
|
||||
|
||||
/// A burst larger than this is a history page arriving, not a conversation:
|
||||
/// animating every row of a page would flash the whole timeline.
|
||||
static let appendAnimationLimit = 4
|
||||
|
||||
/// Total time the opening cascade can still be pending for the last row.
|
||||
static var entranceCascadeDuration: TimeInterval {
|
||||
TimeInterval(entranceCascadeLimit - 1) * entranceStagger
|
||||
+ entranceAnimationDuration
|
||||
}
|
||||
|
||||
static var appendAnimation: Animation {
|
||||
.spring(response: 0.44, dampingFraction: 0.68)
|
||||
}
|
||||
|
||||
/// Per-row delays keyed by client ID. The newest row leads the cascade and
|
||||
/// every older row follows one stagger step behind it; rows outside the
|
||||
/// cascade get no delay and therefore no entrance at all.
|
||||
static func entranceDelays(forIDs ids: [String]) -> [String: TimeInterval] {
|
||||
let last = ids.count - 1
|
||||
guard last >= 0 else { return [:] }
|
||||
let firstAnimated = max(0, ids.count - entranceCascadeLimit)
|
||||
var delays: [String: TimeInterval] = [:]
|
||||
delays.reserveCapacity(ids.count - firstAnimated)
|
||||
for index in firstAnimated...last {
|
||||
delays[ids[index]] = TimeInterval(last - index) * entranceStagger
|
||||
}
|
||||
return delays
|
||||
}
|
||||
|
||||
/// Rows a query refresh added below everything already on screen.
|
||||
///
|
||||
/// A MAM page paged in above the viewport is deliberately ignored: replaying
|
||||
/// the entrance mid-scroll would yank the timeline the user is reading. A
|
||||
/// burst of rows is treated as the same kind of archive delivery.
|
||||
static func appendedLaunchIDs(
|
||||
in ids: [String],
|
||||
after previous: Set<String>
|
||||
) -> [String] {
|
||||
guard !previous.isEmpty else { return [] }
|
||||
guard let lastKnownIndex = ids.lastIndex(where: previous.contains)
|
||||
else { return [] }
|
||||
let appended = ids.dropFirst(lastKnownIndex + 1).filter {
|
||||
!previous.contains($0)
|
||||
}
|
||||
guard appended.count <= appendAnimationLimit else { return [] }
|
||||
return Array(appended)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user