Bounce experiment

This commit is contained in:
Isaac 2025-10-04 00:44:30 +08:00
parent 1ff56084fe
commit 4b04a6c69e
7 changed files with 165 additions and 19 deletions

View File

@ -322,8 +322,9 @@ public extension CALayer {
return animation
}
func animateSpring(from: AnyObject, to: AnyObject, keyPath: String, duration: Double, delay: Double = 0.0, initialVelocity: CGFloat = 0.0, damping: CGFloat = 88.0, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
func animateSpring(from: AnyObject, to: AnyObject, keyPath: String, duration: Double, delay: Double = 0.0, initialVelocity: CGFloat = 0.0, stiffness: CGFloat = 900.0, damping: CGFloat = 88.0, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
let animation = makeSpringBounceAnimation(keyPath, initialVelocity, damping)
animation.stiffness = stiffness
animation.fromValue = from
animation.toValue = to
animation.isRemovedOnCompletion = removeOnCompletion

View File

@ -138,6 +138,10 @@ private extension CALayer {
}
}
private func bounceParameters(duration: Double) -> (duration: Double, damping: CGFloat, stiffness: CGFloat) {
return (duration: duration * 1.25, damping: 88.0, stiffness: 750.0)
}
public extension ContainedViewLayoutTransition {
func animation() -> CABasicAnimation? {
switch self {
@ -469,6 +473,106 @@ public extension ContainedViewLayoutTransition {
}
}
func updatePositionSpring(layer: CALayer, position: CGPoint, completion: ((Bool) -> Void)? = nil) {
if layer.position.equalTo(position) {
completion?(true)
} else {
switch self {
case .immediate:
layer.removeAnimation(forKey: "position")
if let view = layer.delegate as? UIView {
view.center = position
} else {
layer.position = position
}
if let completion = completion {
completion(true)
}
case let .animated(duration, curve):
let _ = curve
let previousPosition = layer.position
if let view = layer.delegate as? UIView {
view.center = position
} else {
layer.position = position
}
let params = bounceParameters(duration: duration)
layer.animateSpring(from: NSValue(cgPoint: previousPosition), to: NSValue(cgPoint: position), keyPath: "position", duration: params.duration, stiffness: params.stiffness, damping: params.damping, completion: { flag in
if let completion {
completion(flag)
}
})
}
}
}
func updateScaleSpring(layer: CALayer, scale: CGFloat, completion: ((Bool) -> Void)? = nil) {
let t = layer.transform
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
if abs(CGFloat(currentScale) - scale) <= CGFloat(Float.ulpOfOne) {
completion?(true)
} else {
switch self {
case .immediate:
layer.removeAnimation(forKey: "transform.scale")
if let view = layer.delegate as? UIView {
view.transform = CGAffineTransformMakeScale(scale, scale)
} else {
layer.transform = CATransform3DMakeScale(scale, scale, 1.0)
}
if let completion = completion {
completion(true)
}
case let .animated(duration, curve):
let _ = curve
if let view = layer.delegate as? UIView {
view.transform = CGAffineTransformMakeScale(scale, scale)
} else {
layer.transform = CATransform3DMakeScale(scale, scale, 1.0)
}
let params = bounceParameters(duration: duration)
layer.animateSpring(from: currentScale as NSNumber, to: scale as NSNumber, keyPath: "transform.scale", duration: params.duration, stiffness: params.stiffness, damping: params.damping, completion: { flag in
if let completion {
completion(flag)
}
})
}
}
}
func updateBoundsSpring(layer: CALayer, bounds: CGRect, completion: ((Bool) -> Void)? = nil) {
if layer.bounds.equalTo(bounds) {
completion?(true)
} else {
switch self {
case .immediate:
layer.removeAnimation(forKey: "bounds")
if let view = layer.delegate as? UIView {
view.bounds = bounds
} else {
layer.bounds = bounds
}
if let completion = completion {
completion(true)
}
case let .animated(duration, curve):
let _ = curve
let previousBounds = layer.bounds
if let view = layer.delegate as? UIView {
view.bounds = bounds
} else {
layer.bounds = bounds
}
let params = bounceParameters(duration: duration)
layer.animateSpring(from: NSValue(cgRect: previousBounds), to: NSValue(cgRect: bounds), keyPath: "bounds", duration: params.duration, stiffness: params.stiffness, damping: params.damping, completion: { result in
if let completion = completion {
completion(result)
}
})
}
}
}
func updateAnchorPoint(layer: CALayer, anchorPoint: CGPoint, force: Bool = false, completion: ((Bool) -> Void)? = nil) {
if layer.anchorPoint.equalTo(anchorPoint) && !force {
completion?(true)

View File

@ -12,7 +12,7 @@ public func makeSpringAnimation(_ keyPath: String, duration: Double) -> CABasicA
return makeSpringAnimationImpl(keyPath, duration)
}
public func makeSpringBounceAnimation(_ keyPath: String, _ initialVelocity: CGFloat, _ damping: CGFloat) -> CABasicAnimation {
public func makeSpringBounceAnimation(_ keyPath: String, _ initialVelocity: CGFloat, _ damping: CGFloat) -> CASpringAnimation {
return makeSpringBounceAnimationImpl(keyPath, initialVelocity, damping)
}

View File

@ -288,6 +288,8 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
private var rightSlowModeInset: CGFloat = 0.0
private var currentTextInputBackgroundWidthOffset: CGFloat = 0.0
private var enableBounceAnimations: Bool = false
public var displayAttachmentMenu: () -> Void = { }
public var sendMessage: () -> Void = { }
public var paste: (ChatTextInputPanelPasteData) -> Void = { _ in }
@ -320,6 +322,8 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
private let hapticFeedback = HapticFeedback()
private var currentInputHasText: Bool = false
public var inputTextState: ChatTextInputState {
if let textInputNode = self.textInputNode {
let selectionRange: Range<Int> = textInputNode.selectedRange.location ..< (textInputNode.selectedRange.location + textInputNode.selectedRange.length)
@ -636,6 +640,11 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
self.context = context
self.enableBounceAnimations = true
if let data = context.currentAppConfiguration.with({ $0 }).data, data["ios_killswitch_input_bounce"] != nil {
self.enableBounceAnimations = false
}
self.addSubnode(self.clippingNode)
self.sendAsAvatarContainerNode.activated = { [weak self] gesture, _ in
@ -1416,6 +1425,16 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, attributedText.length != 0 {
inputHasText = true
}
let inputHadText = self.currentInputHasText
self.currentInputHasText = inputHasText
var useBounceAnimation = inputHasText && !inputHadText
if accessoryPanel != nil || self.accessoryPanel != nil {
useBounceAnimation = false
}
if !self.enableBounceAnimations {
useBounceAnimation = false
}
var hasMenuButton = false
var menuButtonExpanded = false
@ -1957,7 +1976,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
}
menuButtonTitleTransition.updateFrame(node: self.menuButtonTextNode, frame: CGRect(origin: CGPoint(x: 16.0, y: 11.0), size: menuTextSize))
transition.updateAlpha(node: self.menuButtonTextNode, alpha: menuButtonExpanded ? 1.0 : 0.0)
transition.updateFrame(node: self.menuButtonIconNode, frame: CGRect(x: 5.0, y: isSendAsButton ? 5.0 : (5.0 - UIScreenPixel), width: 30.0, height: 30.0))
transition.updateFrame(node: self.menuButtonIconNode, frame: CGRect(x: 7.0, y: 7.0, width: 26.0, height: 26.0))
transition.updateFrame(node: self.sendAsAvatarButtonNode, frame: menuButtonFrame)
transition.updateFrame(node: self.sendAsAvatarContainerNode, frame: CGRect(origin: CGPoint(), size: menuButtonFrame.size))
@ -2384,7 +2403,11 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
transition.updateFrame(view: self.accessoryPanelContainer, frame: CGRect(origin: CGPoint(), size: textInputContainerBackgroundFrame.size))
transition.updateFrame(view: self.textInputContainerBackgroundView, frame: CGRect(origin: CGPoint(), size: textInputContainerBackgroundFrame.size))
self.textInputContainerBackgroundView.update(size: textInputContainerBackgroundFrame.size, cornerRadius: floor(minimalInputHeight * 0.5), isDark: interfaceState.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: interfaceState.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), transition: ComponentTransition(transition))
var textInputContainerBackgroundTransition = ComponentTransition(transition)
if useBounceAnimation, case let .animated(_, curve) = transition, case .spring = curve {
textInputContainerBackgroundTransition = textInputContainerBackgroundTransition.withUserData(GlassBackgroundView.TransitionFlagBounce())
}
self.textInputContainerBackgroundView.update(size: textInputContainerBackgroundFrame.size, cornerRadius: floor(minimalInputHeight * 0.5), isDark: interfaceState.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: interfaceState.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), transition: textInputContainerBackgroundTransition)
transition.updateFrame(layer: self.textInputBackgroundNode.layer, frame: textInputContainerBackgroundFrame)
transition.updateAlpha(node: self.textInputBackgroundNode, alpha: audioRecordingItemsAlpha)
@ -2626,13 +2649,22 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
}
var sendActionButtonsFrame = CGRect(origin: CGPoint(x: textInputContainerBackgroundFrame.maxX - sendActionButtonsSize.width, y: textInputContainerBackgroundFrame.maxY - sendActionButtonsSize.height), size: sendActionButtonsSize)
let sendActionsScale: CGFloat
if inputHasText || hasMediaDraft || hasForward {
transition.updateTransformScale(node: self.sendActionButtons, scale: CGPoint(x: 1.0, y: 1.0))
sendActionsScale = 1.0
} else {
sendActionsScale = 0.001
sendActionButtonsFrame.origin.x += (sendActionButtonsSize.width - 3.0 * 2.0) * 0.5 - 3.0
transition.updateTransformScale(node: self.sendActionButtons, scale: CGPoint(x: 0.001, y: 0.001))
}
if useBounceAnimation, case let .animated(duration, curve) = transition, case .spring = curve {
ContainedViewLayoutTransition.animated(duration: duration, curve: curve).updateScaleSpring(layer: self.sendActionButtons.layer, scale: sendActionsScale)
ContainedViewLayoutTransition.animated(duration: duration, curve: curve).updatePositionSpring(layer: self.sendActionButtons.layer, position: sendActionButtonsFrame.center)
} else {
transition.updateTransformScale(node: self.sendActionButtons, scale: CGPoint(x: sendActionsScale, y: sendActionsScale))
transition.updatePosition(node: self.sendActionButtons, position: sendActionButtonsFrame.center)
}
transition.updateBounds(node: self.sendActionButtons, bounds: CGRect(origin: CGPoint(), size: sendActionButtonsFrame.size))
if let (rect, containerSize) = self.absoluteRect {
self.sendActionButtons.updateAbsoluteRect(CGRect(x: rect.origin.x + sendActionButtonsFrame.origin.x, y: rect.origin.y + sendActionButtonsFrame.origin.y, width: sendActionButtonsFrame.width, height: sendActionButtonsFrame.height), within: containerSize, transition: transition)
@ -3806,7 +3838,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
if self.sendActionButtons.sendContainerNode.alpha.isZero && self.rightSlowModeInset.isZero {
alphaTransition.updateAlpha(node: self.sendActionButtons.sendContainerNode, alpha: 1.0)
blurTransitionIn.animateBlur(layer: self.sendActionButtons.sendContainerNode.layer, fromRadius: sendButtonBlurOut, toRadius: 0.0)
transition.animatePositionAdditive(layer: self.sendActionButtons.sendButton.imageNode.layer, offset: CGPoint(x: -18.0, y: 14.0))
transition.animatePositionAdditive(layer: self.sendActionButtons.sendButton.imageNode.layer, offset: CGPoint(x: -22.0, y: 18.0))
if let sendButtonRadialStatusNode = self.sendActionButtons.sendButtonRadialStatusNode {
alphaTransition.updateAlpha(node: sendButtonRadialStatusNode, alpha: 1.0)
}

View File

@ -48,6 +48,11 @@ private final class ContentContainer: UIView {
}
public class GlassBackgroundView: UIView {
public final class TransitionFlagBounce {
public init() {
}
}
public protocol ContentView: UIView {
var tintMask: UIView { get }
}
@ -377,23 +382,27 @@ public class GlassBackgroundView: UIView {
public func update(size: CGSize, cornerRadius: CGFloat, isDark: Bool, tintColor: TintColor, isInteractive: Bool = false, transition: ComponentTransition) {
if let nativeContainerView = self.nativeContainerView, let nativeView = self.nativeView, nativeView.bounds.size != size {
//let previousFrame = nativeView.frame
if transition.animation.isImmediate {
nativeView.layer.cornerRadius = cornerRadius
nativeView.frame = CGRect(origin: CGPoint(), size: size)
nativeContainerView.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: max(size.height, 400.0)))
} else {
/*transition.containedViewLayoutTransition.animateView {
nativeView.layer.cornerRadius = cornerRadius
nativeView.frame = CGRect(origin: CGPoint(), size: size)
nativeContainerView.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: max(size.height, 400.0)))
}*/
nativeView.layer.cornerRadius = cornerRadius
transition.setFrame(view: nativeView, frame: CGRect(origin: CGPoint(), size: size))
transition.setFrame(view: nativeContainerView, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: max(size.height, 400.0))))
//nativeView.layer.animateFrame(from: previousFrame, to: CGRect(origin: CGPoint(), size: size), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
let nativeFrame = CGRect(origin: CGPoint(), size: size)
let nativeContainerFrame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: max(size.height, 400.0)))
if transition.userData(TransitionFlagBounce.self) != nil {
transition.containedViewLayoutTransition.updatePositionSpring(layer: nativeView.layer, position: nativeFrame.center)
transition.containedViewLayoutTransition.updateBoundsSpring(layer: nativeView.layer, bounds: CGRect(origin: CGPoint(), size: nativeFrame.size))
transition.containedViewLayoutTransition.updatePositionSpring(layer: nativeContainerView.layer, position: nativeContainerFrame.center)
transition.containedViewLayoutTransition.updateBoundsSpring(layer: nativeContainerView.layer, bounds: CGRect(origin: CGPoint(), size: nativeContainerFrame.size))
} else {
transition.setFrame(view: nativeView, frame: nativeFrame)
transition.setFrame(view: nativeContainerView, frame: nativeContainerFrame)
}
}
}
if let backgroundNode = self.backgroundNode {

View File

@ -5,7 +5,7 @@ double animationDurationFactorImpl();
CABasicAnimation * _Nonnull makeSpringAnimationImpl(NSString * _Nonnull keyPath, double duration);
CABasicAnimation * _Nonnull make26SpringAnimationImpl(NSString * _Nonnull keyPath, double duration);
CABasicAnimation * _Nonnull makeSpringBounceAnimationImpl(NSString * _Nonnull keyPath, CGFloat initialVelocity, CGFloat damping);
CASpringAnimation * _Nonnull makeSpringBounceAnimationImpl(NSString * _Nonnull keyPath, CGFloat initialVelocity, CGFloat damping);
CGFloat springAnimationValueAtImpl(CABasicAnimation * _Nonnull animation, CGFloat t);
UIBlurEffect * _Nonnull makeCustomZoomBlurEffectImpl(bool isLight);

View File

@ -83,7 +83,7 @@ CABasicAnimation * _Nonnull make26SpringAnimationImpl(NSString * _Nonnull keyPat
return springAnimation;
}
CABasicAnimation * _Nonnull makeSpringBounceAnimationImpl(NSString * _Nonnull keyPath, CGFloat initialVelocity, CGFloat damping) {
CASpringAnimation * _Nonnull makeSpringBounceAnimationImpl(NSString * _Nonnull keyPath, CGFloat initialVelocity, CGFloat damping) {
CASpringAnimation *springAnimation = [CASpringAnimation animationWithKeyPath:keyPath];
springAnimation.mass = 5.0f;
springAnimation.stiffness = 900.0f;