Swiftgram/submodules/Display/Source/ListViewItemNode.swift
2020-12-12 03:40:31 +04:00

570 lines
20 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
var testSpringFrictionLimits: (CGFloat, CGFloat) = (3.0, 60.0)
var testSpringFriction: CGFloat = 31.8211269378662
var testSpringConstantLimits: (CGFloat, CGFloat) = (3.0, 450.0)
var testSpringConstant: CGFloat = 443.704223632812
var testSpringResistanceFreeLimits: (CGFloat, CGFloat) = (0.05, 1.0)
var testSpringFreeResistance: CGFloat = 0.676197171211243
var testSpringResistanceScrollingLimits: (CGFloat, CGFloat) = (0.1, 1.0)
var testSpringScrollingResistance: CGFloat = 0.6721
struct ListViewItemSpring {
let stiffness: CGFloat
let damping: CGFloat
let mass: CGFloat
var velocity: CGFloat = 0.0
init(stiffness: CGFloat, damping: CGFloat, mass: CGFloat) {
self.stiffness = stiffness
self.damping = damping
self.mass = mass
}
}
public struct ListViewItemNodeLayout {
public let contentSize: CGSize
public let insets: UIEdgeInsets
public init() {
self.contentSize = CGSize()
self.insets = UIEdgeInsets()
}
public init(contentSize: CGSize, insets: UIEdgeInsets) {
self.contentSize = contentSize
self.insets = insets
}
public var size: CGSize {
return CGSize(width: self.contentSize.width + self.insets.left + self.insets.right, height: self.contentSize.height + self.insets.top + self.insets.bottom)
}
}
public enum ListViewItemNodeVisibility: Equatable {
case none
case visible(CGFloat)
public static func ==(lhs: ListViewItemNodeVisibility, rhs: ListViewItemNodeVisibility) -> Bool {
switch lhs {
case .none:
if case .none = rhs {
return true
} else {
return false
}
case let .visible(fraction):
if case .visible(fraction) = rhs {
return true
} else {
return false
}
}
}
}
public struct ListViewItemLayoutParams {
public let width: CGFloat
public let leftInset: CGFloat
public let rightInset: CGFloat
public let availableHeight: CGFloat
public init(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, availableHeight: CGFloat) {
self.width = width
self.leftInset = leftInset
self.rightInset = rightInset
self.availableHeight = availableHeight
}
}
open class ListViewItemNode: ASDisplayNode, AccessibilityFocusableNode {
let rotated: Bool
final var index: Int?
public var isHighlightedInOverlay: Bool = false
public private(set) var accessoryItemNode: ListViewAccessoryItemNode?
func setAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode?, leftInset: CGFloat, rightInset: CGFloat) {
self.accessoryItemNode = accessoryItemNode
if let accessoryItemNode = accessoryItemNode {
self.layoutAccessoryItemNode(accessoryItemNode, leftInset: leftInset, rightInset: rightInset)
}
}
open func addAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) {
self.addSubnode(accessoryItemNode)
}
final var headerAccessoryItemNode: ListViewAccessoryItemNode? {
didSet {
if let headerAccessoryItemNode = self.headerAccessoryItemNode {
self.layoutHeaderAccessoryItemNode(headerAccessoryItemNode)
}
}
}
open var extractedBackgroundNode: ASDisplayNode? {
return nil
}
private final var spring: ListViewItemSpring?
private final var animations: [(String, ListViewAnimation)] = []
final let wantsScrollDynamics: Bool
open var preferredAnimationCurve: (CGFloat) -> CGFloat {
return listViewAnimationCurveSystem
}
public final var wantsTrailingItemSpaceUpdates: Bool = false
public final var scrollPositioningInsets: UIEdgeInsets = UIEdgeInsets()
public final var canBeUsedAsScrollToItemAnchor: Bool = true
open var visibility: ListViewItemNodeVisibility = .none
open var canBeSelected: Bool {
return true
}
open var canBeLongTapped: Bool {
return false
}
open var preventsTouchesToOtherItems: Bool {
return false
}
open func touchesToOtherItemsPrevented() {
}
open func tapped() {
}
open func longTapped() {
}
public final var insets: UIEdgeInsets = UIEdgeInsets() {
didSet {
let effectiveInsets = self.insets
self.frame = CGRect(origin: self.frame.origin, size: CGSize(width: self.contentSize.width, height: self.contentSize.height + effectiveInsets.top + effectiveInsets.bottom))
let bounds = self.bounds
self.bounds = CGRect(origin: CGPoint(x: bounds.origin.x, y: -effectiveInsets.top + self.contentOffset + self.transitionOffset), size: bounds.size)
}
}
private final var _contentSize: CGSize = CGSize()
public final var contentSize: CGSize {
get {
return self._contentSize
} set(value) {
let effectiveInsets = self.insets
self.frame = CGRect(origin: self.frame.origin, size: CGSize(width: value.width, height: value.height + effectiveInsets.top + effectiveInsets.bottom))
}
}
private var contentOffset: CGFloat = 0.0 {
didSet {
let effectiveInsets = self.insets
let bounds = self.bounds
self.bounds = CGRect(origin: CGPoint(x: bounds.origin.x, y: -effectiveInsets.top + self.contentOffset + self.transitionOffset), size: bounds.size)
}
}
public var transitionOffset: CGFloat = 0.0 {
didSet {
let effectiveInsets = self.insets
let bounds = self.bounds
self.bounds = CGRect(origin: CGPoint(x: bounds.origin.x, y: -effectiveInsets.top + self.contentOffset + self.transitionOffset), size: bounds.size)
}
}
public var layout: ListViewItemNodeLayout {
var insets = self.insets
var contentSize = self.contentSize
if let animation = self.animationForKey("insets") {
insets = animation.to as! UIEdgeInsets
}
if let animation = self.animationForKey("apparentHeight") {
contentSize.height = (animation.to as! CGFloat) - insets.top - insets.bottom
}
return ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
}
public var displayResourcesReady: Signal<Void, NoError> {
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
self.rotated = rotated
super.init()
self.isLayerBacked = layerBacked
}
var apparentHeight: CGFloat = 0.0
private var _bounds: CGRect = CGRect()
private var _position: CGPoint = CGPoint()
open override var frame: CGRect {
get {
return CGRect(origin: CGPoint(x: self._position.x - self._bounds.width / 2.0, y: self._position.y - self._bounds.height / 2.0), size: self._bounds.size)
} set(value) {
let previousSize = self._bounds.size
super.frame = value
self._bounds.size = value.size
self._position = CGPoint(x: value.midX, y: value.midY)
let effectiveInsets = self.insets
self._contentSize = CGSize(width: value.size.width, height: value.size.height - effectiveInsets.top - effectiveInsets.bottom)
if previousSize != value.size {
if let headerAccessoryItemNode = self.headerAccessoryItemNode {
self.layoutHeaderAccessoryItemNode(headerAccessoryItemNode)
}
}
}
}
open override var bounds: CGRect {
get {
return self._bounds
} set(value) {
let previousSize = self._bounds.size
super.bounds = value
self._bounds = value
let effectiveInsets = self.insets
self._contentSize = CGSize(width: value.size.width, height: value.size.height - effectiveInsets.top - effectiveInsets.bottom)
if previousSize != value.size {
if let headerAccessoryItemNode = self.headerAccessoryItemNode {
self.layoutHeaderAccessoryItemNode(headerAccessoryItemNode)
}
}
}
}
public var contentBounds: CGRect {
let bounds = self.bounds
let effectiveInsets = self.insets
return CGRect(origin: CGPoint(x: 0.0, y: bounds.origin.y + effectiveInsets.top), size: CGSize(width: bounds.size.width, height: bounds.size.height - effectiveInsets.top - effectiveInsets.bottom))
}
open override var position: CGPoint {
get {
return self._position
} set(value) {
super.position = value
self._position = value
}
}
public final var apparentFrame: CGRect {
var frame = self.frame
frame.size.height = self.apparentHeight
return frame
}
public final var apparentContentFrame: CGRect {
var frame = self.frame
let insets = self.insets
frame.origin.y += insets.top
frame.size.height = self.apparentHeight - insets.top - insets.bottom
return frame
}
public final var apparentBounds: CGRect {
var bounds = self.bounds
bounds.size.height = self.apparentHeight
return bounds
}
open func layoutAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode, leftInset: CGFloat, rightInset: CGFloat) {
}
open func layoutHeaderAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) {
}
open func reuse() {
}
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) -> 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 {
let (_, animation) = self.animations[i]
animation.applyAt(timestamp)
if animation.completeAt(timestamp) {
animations.remove(at: i)
animationCount -= 1
i -= 1
} else {
continueAnimations = true
}
i += 1
}
if let accessoryItemNode = self.accessoryItemNode {
if (accessoryItemNode.animate(timestamp)) {
continueAnimations = true
}
}
return continueAnimations
}
open func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
}
public func animationForKey(_ key: String) -> ListViewAnimation? {
for (animationKey, animation) in self.animations {
if animationKey == key {
return animation
}
}
return nil
}
public final func setAnimationForKey(_ key: String, animation: ListViewAnimation?) {
for i in 0 ..< self.animations.count {
let (currentKey, currentAnimation) = self.animations[i]
if currentKey == key {
self.animations.remove(at: i)
currentAnimation.cancel()
break
}
}
if let animation = animation {
self.animations.append((key, animation))
}
}
public final func removeAllAnimations() {
let previousAnimations = self.animations
self.animations.removeAll()
for (_, animation) in previousAnimations {
animation.cancel()
}
self.accessoryItemNode?.removeAllAnimations()
}
public func addInsetsAnimationToValue(_ value: UIEdgeInsets, duration: Double, beginAt: Double) {
let animation = ListViewAnimation(from: self.insets, to: value, duration: duration, curve: listViewAnimationCurveSystem, beginAt: beginAt, update: { [weak self] _, currentValue in
if let strongSelf = self {
strongSelf.insets = currentValue
}
})
self.setAnimationForKey("insets", animation: animation)
}
public func addHeightAnimation(_ value: CGFloat, duration: Double, beginAt: Double, update: ((CGFloat, CGFloat) -> Void)? = nil) {
let animation = ListViewAnimation(from: self.bounds.height, to: value, duration: duration, curve: self.preferredAnimationCurve, beginAt: beginAt, update: { [weak self] progress, currentValue in
if let strongSelf = self {
let frame = strongSelf.frame
strongSelf.frame = CGRect(origin: frame.origin, size: CGSize(width: frame.width, height: currentValue))
if let update = update {
update(progress, currentValue)
}
}
})
self.setAnimationForKey("height", animation: animation)
}
func copyHeightAndApparentHeightAnimations(to otherNode: ListViewItemNode) {
if let animation = self.animationForKey("apparentHeight") {
let updatedAnimation = ListViewAnimation(copying: animation, update: { [weak otherNode] (progress: CGFloat, currentValue: CGFloat) -> Void in
if let strongSelf = otherNode {
let frame = strongSelf.frame
strongSelf.frame = CGRect(origin: frame.origin, size: CGSize(width: frame.width, height: currentValue))
}
})
otherNode.setAnimationForKey("height", animation: updatedAnimation)
}
if let animation = self.animationForKey("apparentHeight") {
let updatedAnimation = ListViewAnimation(copying: animation, update: { [weak otherNode] (progress: CGFloat, currentValue: CGFloat) -> Void in
if let strongSelf = otherNode {
strongSelf.apparentHeight = currentValue
}
})
otherNode.setAnimationForKey("apparentHeight", animation: updatedAnimation)
}
}
public func addApparentHeightAnimation(_ value: CGFloat, duration: Double, beginAt: Double, update: ((CGFloat, CGFloat) -> Void)? = nil) {
let animation = ListViewAnimation(from: self.apparentHeight, to: value, duration: duration, curve: self.preferredAnimationCurve, beginAt: beginAt, update: { [weak self] progress, currentValue in
if let strongSelf = self {
strongSelf.apparentHeight = currentValue
if let update = update {
update(progress, currentValue)
}
}
})
self.setAnimationForKey("apparentHeight", animation: animation)
}
public func modifyApparentHeightAnimation(_ value: CGFloat, beginAt: Double) {
if let previousAnimation = self.animationForKey("apparentHeight") {
var duration = previousAnimation.startTime + previousAnimation.duration - beginAt
if abs(self.apparentHeight - value) < CGFloat.ulpOfOne {
duration = 0.0
}
let animation = ListViewAnimation(from: self.apparentHeight, to: value, duration: duration, curve: listViewAnimationCurveSystem, beginAt: beginAt, update: { [weak self] _, currentValue in
if let strongSelf = self {
strongSelf.apparentHeight = currentValue
}
})
self.setAnimationForKey("apparentHeight", animation: animation)
}
}
public func removeApparentHeightAnimation() {
self.setAnimationForKey("apparentHeight", animation: nil)
}
public func addTransitionOffsetAnimation(_ value: CGFloat, duration: Double, beginAt: Double) {
let animation = ListViewAnimation(from: self.transitionOffset, to: value, duration: duration, curve: self.preferredAnimationCurve, beginAt: beginAt, update: { [weak self] _, currentValue in
if let strongSelf = self {
strongSelf.transitionOffset = currentValue
}
})
self.setAnimationForKey("transitionOffset", animation: animation)
}
open func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
}
open func animateAdded(_ currentTimestamp: Double, duration: Double) {
}
open func animateRemoved(_ currentTimestamp: Double, duration: Double) {
}
open func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
}
open func selected() {
}
open func isReorderable(at point: CGPoint) -> Bool {
return false
}
open func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) {
}
open func shouldAnimateHorizontalFrameTransition() -> Bool {
return false
}
open func header() -> ListViewItemHeader? {
return nil
}
open func updateTrailingItemSpace(_ height: CGFloat, transition: ContainedViewLayoutTransition) {
}
override open func accessibilityElementDidBecomeFocused() {
(self.supernode as? ListView)?.ensureItemNodeVisible(self, animated: false, overflow: 22.0)
}
public func updateFrame(_ frame: CGRect, within containerSize: CGSize) {
self.frame = frame
self.updateAbsoluteRect(frame, within: containerSize)
if let extractedBackgroundNode = self.extractedBackgroundNode {
extractedBackgroundNode.frame = frame.offsetBy(dx: 0.0, dy: -self.insets.top)
}
}
open func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
}
open func applyAbsoluteOffset(value: CGFloat, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) {
if let extractedBackgroundNode = self.extractedBackgroundNode {
let transition: ContainedViewLayoutTransition = .animated(duration: duration, curve: animationCurve)
transition.animatePositionAdditive(node: extractedBackgroundNode, offset: CGPoint(x: 0.0, y: -value))
}
}
open func snapshotForReordering() -> UIView? {
return self.view.snapshotContentTree(keepTransform: true)
}
}