This commit is contained in:
Isaac 2025-12-26 20:29:30 +08:00
parent 7ed08c0141
commit 4a13c3830a
234 changed files with 1442 additions and 1764 deletions

View file

@ -210,7 +210,6 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel
private final var displayLink: CADisplayLink!
private final var needsAnimations = false
public final var dynamicBounceEnabled = true
public final var rotated = false
public final var experimentalSnapScrollToItem = false
public final var useMainQueueTransactions = false
@ -265,6 +264,9 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel
public final var addContentOffset: ((CGFloat, ListViewItemNode?) -> Void)?
public final var shouldStopScrolling: ((CGFloat) -> Bool)?
public final var onContentsUpdated: ((ContainedViewLayoutTransition) -> Void)?
public private(set) final var edgeEffectExtension: CGFloat = 0.0
public final var onEdgeEffectExtensionUpdated: ((ContainedViewLayoutTransition) -> Void)?
public final var updateScrollingIndicator: ((ScrollingIndicatorState?, ContainedViewLayoutTransition) -> Void)?
@ -1044,41 +1046,10 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel
self.enqueueUpdateVisibleItems(synchronous: false)
}
var useScrollDynamics = false
let anchor: CGFloat
if self.isTracking {
anchor = self.touchesPosition.y
} else if deltaY < 0.0 {
anchor = self.visibleSize.height
} else {
anchor = 0.0
}
self.didScrollWithOffset?(deltaY, .immediate, nil, self.isTrackingOrDecelerating)
for itemNode in self.itemNodes {
itemNode.updateFrame(itemNode.frame.offsetBy(dx: 0.0, dy: -deltaY), within: self.visibleSize)
if self.dynamicBounceEnabled && itemNode.wantsScrollDynamics {
useScrollDynamics = true
var distance: CGFloat
let itemFrame = itemNode.apparentFrame
if anchor < itemFrame.origin.y {
distance = abs(itemFrame.origin.y - anchor)
} else if anchor > itemFrame.origin.y + itemFrame.size.height {
distance = abs(anchor - (itemFrame.origin.y + itemFrame.size.height))
} else {
distance = 0.0
}
let factor: CGFloat = max(0.08, abs(distance) / self.visibleSize.height)
let resistance: CGFloat = testSpringFreeResistance
itemNode.addScrollingOffset(deltaY * factor * resistance)
}
}
if !self.snapToBounds(snapTopItem: false, stackFromBottom: self.stackFromBottom, insetDeltaOffsetFix: 0.0).offset.isZero {
@ -1088,38 +1059,10 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel
self.updateItemHeaders(leftInset: self.insets.left, rightInset: self.insets.right, synchronousLoad: false)
for (_, headerNode) in self.itemHeaderNodes {
if self.dynamicBounceEnabled && headerNode.wantsScrollDynamics {
useScrollDynamics = true
var distance: CGFloat
let itemFrame = headerNode.frame
if anchor < itemFrame.origin.y {
distance = abs(itemFrame.origin.y - anchor)
} else if anchor > itemFrame.origin.y + itemFrame.size.height {
distance = abs(anchor - (itemFrame.origin.y + itemFrame.size.height))
} else {
distance = 0.0
}
let factor: CGFloat = max(0.08, abs(distance) / self.visibleSize.height)
let resistance: CGFloat = testSpringFreeResistance
headerNode.addScrollingOffset(deltaY * factor * resistance)
}
}
if useScrollDynamics {
self.setNeedsAnimations()
}
self.updateVisibleContentOffset()
self.updateVisibleItemRange()
self.updateItemNodesVisibilities(onlyPositive: false)
self.onContentsUpdated?(.immediate)
//CATransaction.commit()
}
private func calculateAdditionalTopInverseInset() -> CGFloat {
@ -3914,6 +3857,8 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel
let flashing = self.headerItemsAreFlashing()
var maxEdgeEffectExtension: CGFloat = 0.0
func addHeader(id: VisibleHeaderNodeId, upperBound: CGFloat, upperIndex: Int, upperBoundEdge: CGFloat, lowerBound: CGFloat, lowerIndex: Int, item: ListViewItemHeader, hasValidNodes: Bool) {
let itemHeaderHeight: CGFloat = item.height
@ -4096,6 +4041,11 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel
}
headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, distance: stickLocationDistance, transition: .immediate)
}
if headerNode.contributesToEdgeEffect && stickLocationDistance > 0.0 {
maxEdgeEffectExtension = max(maxEdgeEffectExtension, upperDisplayBound + headerFrame.height + 8.0)
}
headerNode.offsetByHeaderNodeId = offsetByHeaderNodeId
headerNode.naturalOriginY = naturalY
var maxIntersectionHeight: (CGFloat, Int)?
@ -4228,6 +4178,11 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel
}
}
}
if self.edgeEffectExtension != maxEdgeEffectExtension {
self.edgeEffectExtension = maxEdgeEffectExtension
self.onEdgeEffectExtensionUpdated?(transition.0)
}
}
private func updateItemNodesVisibilities(onlyPositive: Bool) {

View file

@ -22,13 +22,12 @@ public protocol ListViewItemHeader: AnyObject {
}
open class ListViewItemHeaderNode: ASDisplayNode {
private final var spring: ListViewItemSpring?
let wantsScrollDynamics: Bool
let isRotated: Bool
final private(set) var internalStickLocationDistanceFactor: CGFloat = 0.0
final var internalStickLocationDistance: CGFloat = 0.0
private var isFlashingOnScrolling = false
weak var attachedToItemNode: ListViewItemNode?
public var contributesToEdgeEffect: Bool = false
var offsetByHeaderNodeId: ListViewItemNode.HeaderId?
var naturalOriginY: CGFloat?
@ -53,12 +52,8 @@ open class ListViewItemHeaderNode: ASDisplayNode {
return self.alpha
}
public init(layerBacked: Bool = false, dynamicBounce: Bool = false, isRotated: Bool = false, seeThrough: Bool = false) {
self.wantsScrollDynamics = dynamicBounce
public init(layerBacked: Bool = false, isRotated: Bool = false, seeThrough: Bool = false) {
self.isRotated = isRotated
if dynamicBounce {
self.spring = ListViewItemSpring(stiffness: -280.0, damping: -24.0, mass: 0.85)
}
super.init()
@ -69,54 +64,10 @@ open class ListViewItemHeaderNode: ASDisplayNode {
}
final func addScrollingOffset(_ scrollingOffset: CGFloat) {
if self.spring != nil && internalStickLocationDistanceFactor.isZero {
let bounds = self.bounds
self.bounds = CGRect(origin: CGPoint(x: 0.0, y: bounds.origin.y + scrollingOffset), size: bounds.size)
}
}
public func animate(_ timestamp: Double) -> Bool {
var continueAnimations = false
if let _ = self.spring {
let bounds = self.bounds
var offset = bounds.origin.y
let currentOffset = offset
let frictionConstant: CGFloat = testSpringFriction
let springConstant: CGFloat = testSpringConstant
let time: CGFloat = 1.0 / 60.0
// friction force = velocity * friction constant
let frictionForce = self.spring!.velocity * frictionConstant
// spring force = (target point - current position) * spring constant
let springForce = -currentOffset * springConstant
// force = spring force - friction force
let force = springForce - frictionForce
// velocity = current velocity + force * time / mass
self.spring!.velocity = self.spring!.velocity + force * time
// position = current position + velocity * time
offset = currentOffset + self.spring!.velocity * time
offset = offset.isNaN ? 0.0 : offset
let epsilon: CGFloat = 0.1
if abs(offset) < epsilon && abs(self.spring!.velocity) < epsilon {
offset = 0.0
self.spring!.velocity = 0.0
} else {
continueAnimations = true
}
if abs(offset) > 250.0 {
offset = offset < 0.0 ? -250.0 : 250.0
}
self.bounds = CGRect(origin: CGPoint(x: 0.0, y: offset), size: bounds.size)
}
return continueAnimations
return false
}
open func animateRemoved(duration: Double) {

View file

@ -129,7 +129,6 @@ open class ListViewItemNode: ASDisplayNode, AccessibilityFocusableNode {
return nil
}
private final var spring: ListViewItemSpring?
private final var animations: [(String, ListViewAnimation)] = []
private final var pendingControlledTransitions: [ControlledTransition] = []
private final var controlledTransitions: [ControlledTransitionContext] = []
@ -142,8 +141,6 @@ open class ListViewItemNode: ASDisplayNode, AccessibilityFocusableNode {
open func attachedHeaderNodesUpdated() {
}
final let wantsScrollDynamics: Bool
open var preferredAnimationCurve: (CGFloat) -> CGFloat {
return listViewAnimationCurveSystem
}
@ -236,12 +233,7 @@ open class ListViewItemNode: ASDisplayNode, AccessibilityFocusableNode {
return .complete()
}
public init(layerBacked: Bool, dynamicBounce: Bool = true, rotated: Bool = false, seeThrough: Bool = false) {
if dynamicBounce {
self.spring = ListViewItemSpring(stiffness: -280.0, damping: -24.0, mass: 0.85)
}
self.wantsScrollDynamics = dynamicBounce
public init(layerBacked: Bool, rotated: Bool = false, seeThrough: Bool = false) {
self.rotated = rotated
super.init()
@ -338,56 +330,14 @@ open class ListViewItemNode: ASDisplayNode, AccessibilityFocusableNode {
}
final func addScrollingOffset(_ scrollingOffset: CGFloat) {
if self.spring != nil {
self.contentOffset += scrollingOffset
}
}
func initializeDynamicsFromSibling(_ itemView: ListViewItemNode, additionalOffset: CGFloat) {
if let itemViewSpring = itemView.spring {
self.contentOffset = itemView.contentOffset + additionalOffset
self.spring?.velocity = itemViewSpring.velocity
}
}
public func animate(timestamp: Double, invertOffsetDirection: inout Bool) -> Bool {
var continueAnimations = false
if let _ = self.spring {
var offset = self.contentOffset
let frictionConstant: CGFloat = testSpringFriction
let springConstant: CGFloat = testSpringConstant
let time: CGFloat = 1.0 / 60.0
// friction force = velocity * friction constant
let frictionForce = self.spring!.velocity * frictionConstant
// spring force = (target point - current position) * spring constant
let springForce = -self.contentOffset * springConstant
// force = spring force - friction force
let force = springForce - frictionForce
// velocity = current velocity + force * time / mass
self.spring!.velocity = self.spring!.velocity + force * time
// position = current position + velocity * time
offset = self.contentOffset + self.spring!.velocity * time
offset = offset.isNaN ? 0.0 : offset
let epsilon: CGFloat = 0.1
if abs(offset) < epsilon && abs(self.spring!.velocity) < epsilon {
offset = 0.0
self.spring!.velocity = 0.0
} else {
continueAnimations = true
}
if abs(offset) > 250.0 {
offset = offset < 0.0 ? -250.0 : 250.0
}
self.contentOffset = offset
}
var i = 0
var animationCount = self.animations.count
while i < animationCount {

View file

@ -190,6 +190,8 @@ public protocol NavigationBar: ASDisplayNode {
var requestContainerLayout: ((ContainedViewLayoutTransition) -> Void)? { get set }
func updateLayout(size: CGSize, defaultHeight: CGFloat, additionalTopHeight: CGFloat, additionalContentHeight: CGFloat, additionalBackgroundHeight: CGFloat, additionalCutout: CGSize?, leftInset: CGFloat, rightInset: CGFloat, appearsHidden: Bool, isLandscape: Bool, transition: ContainedViewLayoutTransition)
func updateEdgeEffectExtension(value: CGFloat, transition: ContainedViewLayoutTransition)
}
public var defaultNavigationBarImpl: ((NavigationBarPresentationData) -> NavigationBar)?

View file

@ -249,6 +249,16 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject {
open var interactiveNavivationGestureEdgeWidth: InteractiveTransitionGestureRecognizerEdgeWidth? {
return nil
}
open var navigationEdgeEffectExtension: CGFloat {
return 0.0
}
public func updateNavigationEdgeEffectExtension(transition: ContainedViewLayoutTransition) {
if let navigationBar = self.navigationBar {
navigationBar.updateEdgeEffectExtension(value: max(0.0, self.navigationEdgeEffectExtension - navigationBar.frame.maxY), transition: transition)
}
}
open func navigationLayout(layout: ContainerViewLayout) -> NavigationLayout {
let statusBarHeight: CGFloat = layout.statusBarHeight ?? 0.0