mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-08 19:10:53 +00:00
Apply patch
This commit is contained in:
parent
187e260374
commit
59050c71a4
@ -11,7 +11,6 @@
|
||||
@protocol TGModernConversationInputMicButtonDecoration <NSObject>
|
||||
|
||||
- (void)updateLevel:(CGFloat)level;
|
||||
- (void)tick:(CGFloat)level;
|
||||
- (void)setColor:(UIColor *)color;
|
||||
- (void)stopAnimating;
|
||||
- (void)startAnimating;
|
||||
|
@ -846,7 +846,6 @@ static const CGFloat outerCircleMinScale = innerCircleRadius / outerCircleRadius
|
||||
NSTimeInterval t = CACurrentMediaTime();
|
||||
|
||||
_currentLevel = _currentLevel * 0.9f + _inputLevel * 0.1f;
|
||||
[_decoration tick:_currentLevel];
|
||||
|
||||
_currentTranslation = MIN(0.0, _currentTranslation * 0.7f + _targetTranslation * 0.3f);
|
||||
_cancelTranslation = MIN(0.0, _cancelTranslation * 0.7f + _cancelTargetTranslation * 0.3f);
|
||||
|
@ -3,53 +3,81 @@ import UIKit
|
||||
import Display
|
||||
import LegacyComponents
|
||||
|
||||
private enum Constants {
|
||||
|
||||
static let maxLevel: CGFloat = 4
|
||||
}
|
||||
|
||||
final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDecoration {
|
||||
|
||||
private let smallBlob = BlobView(
|
||||
pointsCount: 8,
|
||||
minRandomness: 0.1,
|
||||
maxRandomness: 0.5,
|
||||
minSpeed: 0.2,
|
||||
maxSpeed: 0.6,
|
||||
minScale: 0.45,
|
||||
maxScale: 0.55,
|
||||
scaleSpeed: 0.2,
|
||||
isCircle: true
|
||||
)
|
||||
private let mediumBlob = BlobView(
|
||||
pointsCount: 8,
|
||||
minRandomness: 1,
|
||||
maxRandomness: 1,
|
||||
minSpeed: 1.5,
|
||||
maxSpeed: 7,
|
||||
minScale: 0.52,
|
||||
maxScale: 0.87,
|
||||
scaleSpeed: 0.2,
|
||||
isCircle: false
|
||||
)
|
||||
private let bigBlob = BlobView(
|
||||
pointsCount: 8,
|
||||
minRandomness: 1,
|
||||
maxRandomness: 1,
|
||||
minSpeed: 1.5,
|
||||
maxSpeed: 7,
|
||||
minScale: 0.57,
|
||||
maxScale: 1,
|
||||
scaleSpeed: 0.2,
|
||||
isCircle: false
|
||||
)
|
||||
private let smallBlob: BlobView
|
||||
private let mediumBlob: BlobView
|
||||
private let bigBlob: BlobView
|
||||
|
||||
override init(frame: CGRect) {
|
||||
private let maxLevel: CGFloat
|
||||
|
||||
private var displayLinkAnimator: ConstantDisplayLinkAnimator?
|
||||
|
||||
private var audioLevel: CGFloat = 0
|
||||
private var presentationAudioLevel: CGFloat = 0
|
||||
|
||||
private(set) var isAnimating = false
|
||||
|
||||
typealias BlobRange = (min: CGFloat, max: CGFloat)
|
||||
|
||||
init(
|
||||
frame: CGRect,
|
||||
maxLevel: CGFloat,
|
||||
smallBlobRange: BlobRange,
|
||||
mediumBlobRange: BlobRange,
|
||||
bigBlobRange: BlobRange
|
||||
) {
|
||||
self.maxLevel = maxLevel
|
||||
|
||||
self.smallBlob = BlobView(
|
||||
pointsCount: 8,
|
||||
minRandomness: 0.1,
|
||||
maxRandomness: 0.5,
|
||||
minSpeed: 0.2,
|
||||
maxSpeed: 0.6,
|
||||
minScale: smallBlobRange.min,
|
||||
maxScale: smallBlobRange.max,
|
||||
scaleSpeed: 0.2,
|
||||
isCircle: true
|
||||
)
|
||||
self.mediumBlob = BlobView(
|
||||
pointsCount: 8,
|
||||
minRandomness: 1,
|
||||
maxRandomness: 1,
|
||||
minSpeed: 1.5,
|
||||
maxSpeed: 7,
|
||||
minScale: mediumBlobRange.min,
|
||||
maxScale: mediumBlobRange.max,
|
||||
scaleSpeed: 0.2,
|
||||
isCircle: false
|
||||
)
|
||||
self.bigBlob = BlobView(
|
||||
pointsCount: 8,
|
||||
minRandomness: 1,
|
||||
maxRandomness: 1,
|
||||
minSpeed: 1.5,
|
||||
maxSpeed: 7,
|
||||
minScale: bigBlobRange.min,
|
||||
maxScale: bigBlobRange.max,
|
||||
scaleSpeed: 0.2,
|
||||
isCircle: false
|
||||
)
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
addSubview(bigBlob)
|
||||
addSubview(mediumBlob)
|
||||
addSubview(smallBlob)
|
||||
|
||||
displayLinkAnimator = ConstantDisplayLinkAnimator() { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
|
||||
strongSelf.presentationAudioLevel = strongSelf.presentationAudioLevel * 0.9 + strongSelf.audioLevel * 0.1
|
||||
|
||||
strongSelf.smallBlob.level = strongSelf.presentationAudioLevel
|
||||
strongSelf.mediumBlob.level = strongSelf.presentationAudioLevel
|
||||
strongSelf.bigBlob.level = strongSelf.presentationAudioLevel
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@ -63,45 +91,61 @@ final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDecoration
|
||||
}
|
||||
|
||||
func updateLevel(_ level: CGFloat) {
|
||||
let normalizedLevel = min(1, max(level / Constants.maxLevel, 0))
|
||||
let normalizedLevel = min(1, max(level / maxLevel, 0))
|
||||
|
||||
smallBlob.updateSpeedLevel(to: normalizedLevel)
|
||||
mediumBlob.updateSpeedLevel(to: normalizedLevel)
|
||||
bigBlob.updateSpeedLevel(to: normalizedLevel)
|
||||
}
|
||||
|
||||
func tick(_ level: CGFloat) {
|
||||
let normalizedLevel = min(1, max(level / Constants.maxLevel, 0))
|
||||
|
||||
smallBlob.level = normalizedLevel
|
||||
mediumBlob.level = normalizedLevel
|
||||
bigBlob.level = normalizedLevel
|
||||
audioLevel = normalizedLevel
|
||||
}
|
||||
|
||||
func startAnimating() {
|
||||
mediumBlob.layer.animateScale(from: 0.5, to: 1, duration: 0.1, removeOnCompletion: false)
|
||||
bigBlob.layer.animateScale(from: 0.5, to: 1, duration: 0.1, removeOnCompletion: false)
|
||||
guard !isAnimating else { return }
|
||||
isAnimating = true
|
||||
|
||||
mediumBlob.layer.animateScale(from: 0.5, to: 1, duration: 0.15, removeOnCompletion: false)
|
||||
bigBlob.layer.animateScale(from: 0.5, to: 1, duration: 0.15, removeOnCompletion: false)
|
||||
|
||||
updateBlobsState()
|
||||
|
||||
displayLinkAnimator?.isPaused = false
|
||||
}
|
||||
|
||||
func stopAnimating() {
|
||||
mediumBlob.layer.animateScale(from: 1.0, to: 0.5, duration: 0.1, removeOnCompletion: false)
|
||||
bigBlob.layer.animateScale(from: 1.0, to: 0.5, duration: 0.1, removeOnCompletion: false)
|
||||
guard isAnimating else { return }
|
||||
isAnimating = false
|
||||
|
||||
mediumBlob.layer.animateScale(from: 1.0, to: 0.5, duration: 0.15, removeOnCompletion: false)
|
||||
bigBlob.layer.animateScale(from: 1.0, to: 0.5, duration: 0.15, removeOnCompletion: false)
|
||||
|
||||
updateBlobsState()
|
||||
|
||||
displayLinkAnimator?.isPaused = true
|
||||
}
|
||||
|
||||
private func updateBlobsState() {
|
||||
if isAnimating {
|
||||
if smallBlob.frame.size != .zero {
|
||||
smallBlob.startAnimating()
|
||||
mediumBlob.startAnimating()
|
||||
bigBlob.startAnimating()
|
||||
}
|
||||
} else {
|
||||
smallBlob.stopAnimating()
|
||||
mediumBlob.stopAnimating()
|
||||
bigBlob.stopAnimating()
|
||||
}
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
let isInitial = smallBlob.frame == .zero
|
||||
|
||||
smallBlob.frame = bounds
|
||||
mediumBlob.frame = bounds
|
||||
bigBlob.frame = bounds
|
||||
|
||||
if isInitial {
|
||||
smallBlob.startAnimating()
|
||||
mediumBlob.startAnimating()
|
||||
bigBlob.startAnimating()
|
||||
}
|
||||
updateBlobsState()
|
||||
}
|
||||
}
|
||||
|
||||
@ -221,40 +265,19 @@ final class BlobView: UIView {
|
||||
animateToNewShape()
|
||||
}
|
||||
|
||||
func animateToNewScale() {
|
||||
let scaleLevelForAnimation: CGFloat = {
|
||||
if scaleLevelsToBalance.isEmpty {
|
||||
return 0
|
||||
}
|
||||
return scaleLevelsToBalance.reduce(0, +) / CGFloat(scaleLevelsToBalance.count)
|
||||
}()
|
||||
let isDownscale = lastScaleLevel > scaleLevelForAnimation
|
||||
lastScaleLevel = scaleLevelForAnimation
|
||||
|
||||
shapeLayer.pop_removeAnimation(forKey: "scale")
|
||||
|
||||
let currentScale = minScale + (maxScale - minScale) * scaleLevelForAnimation
|
||||
let scaleAnimation = POPBasicAnimation(propertyNamed: kPOPLayerScaleXY)!
|
||||
scaleAnimation.toValue = CGPoint(x: currentScale, y: currentScale)
|
||||
scaleAnimation.duration = isDownscale ? 0.45 : CFTimeInterval(scaleSpeed)
|
||||
scaleAnimation.completionBlock = { [weak self] animation, finished in
|
||||
if finished {
|
||||
self?.animateToNewScale()
|
||||
}
|
||||
}
|
||||
shapeLayer.pop_add(scaleAnimation, forKey: "scale")
|
||||
|
||||
scaleLevel = 0
|
||||
scaleLevelsToBalance.removeAll()
|
||||
func stopAnimating() {
|
||||
fromPoints = currentPoints
|
||||
toPoints = nil
|
||||
pop_removeAnimation(forKey: "blob")
|
||||
}
|
||||
|
||||
func animateToNewShape() {
|
||||
private func animateToNewShape() {
|
||||
guard !isCircle else { return }
|
||||
|
||||
if pop_animation(forKey: "blob") != nil {
|
||||
fromPoints = currentPoints
|
||||
toPoints = nil
|
||||
shapeLayer.pop_removeAnimation(forKey: "blob")
|
||||
pop_removeAnimation(forKey: "blob")
|
||||
}
|
||||
|
||||
if fromPoints == nil {
|
||||
|
@ -34,7 +34,6 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
private var iconNode: TransformImageNode?
|
||||
private var statusNode: SemanticStatusNode?
|
||||
private var playbackAudioLevelView: VoiceBlobView?
|
||||
private var displayLinkAnimator: ConstantDisplayLinkAnimator?
|
||||
private var streamingStatusNode: RadialStatusNode?
|
||||
private var tapRecognizer: UITapGestureRecognizer?
|
||||
|
||||
@ -63,21 +62,10 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
|
||||
var visibility: Bool = false {
|
||||
didSet {
|
||||
if self.visibility != oldValue {
|
||||
if self.visibility {
|
||||
if self.displayLinkAnimator == nil {
|
||||
self.displayLinkAnimator = ConstantDisplayLinkAnimator(update: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.currentAudioLevel = strongSelf.currentAudioLevel * 0.9 + strongSelf.inputAudioLevel * 0.1
|
||||
strongSelf.playbackAudioLevelView?.tick(strongSelf.currentAudioLevel)
|
||||
})
|
||||
}
|
||||
self.displayLinkAnimator?.isPaused = false
|
||||
} else {
|
||||
self.displayLinkAnimator?.isPaused = true
|
||||
}
|
||||
guard self.visibility != oldValue else { return }
|
||||
|
||||
if !self.visibility {
|
||||
self.playbackAudioLevelView?.stopAnimating()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -449,8 +437,13 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
if hasThumbnail {
|
||||
minLayoutWidth = max(titleLayout.size.width, descriptionMaxWidth) + 86.0
|
||||
} else if isVoice {
|
||||
var descriptionAndStatusWidth = descriptionLayout.size.width
|
||||
if let statusSize = statusSize {
|
||||
descriptionAndStatusWidth += 6 + statusSize.width
|
||||
}
|
||||
let calcDuration = max(minVoiceLength, min(maxVoiceLength, CGFloat(audioDuration)))
|
||||
minLayoutWidth = minVoiceWidth + (maxVoiceWidth - minVoiceWidth) * (calcDuration - minVoiceLength) / (maxVoiceLength - minVoiceLength)
|
||||
minLayoutWidth = max(descriptionAndStatusWidth + 56, minLayoutWidth)
|
||||
} else {
|
||||
minLayoutWidth = max(titleLayout.size.width, descriptionMaxWidth) + 44.0 + 8.0
|
||||
}
|
||||
@ -477,7 +470,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
return (minLayoutWidth, { boundingWidth in
|
||||
let progressDiameter: CGFloat = (isVoice && !hasThumbnail) ? 37.0 : 44.0
|
||||
let progressDiameter: CGFloat = 44.0
|
||||
|
||||
var iconFrame: CGRect?
|
||||
let progressFrame: CGRect
|
||||
@ -487,10 +480,19 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
if hasThumbnail {
|
||||
let currentIconFrame = CGRect(origin: CGPoint(x: -1.0, y: -7.0), size: CGSize(width: 74.0, height: 74.0))
|
||||
iconFrame = currentIconFrame
|
||||
progressFrame = CGRect(origin: CGPoint(x: currentIconFrame.minX + floor((currentIconFrame.size.width - progressDiameter) / 2.0), y: currentIconFrame.minY + floor((currentIconFrame.size.height - progressDiameter) / 2.0)), size: CGSize(width: progressDiameter, height: progressDiameter))
|
||||
progressFrame = CGRect(
|
||||
origin: CGPoint(
|
||||
x: currentIconFrame.minX + floor((currentIconFrame.size.width - progressDiameter) / 2.0),
|
||||
y: currentIconFrame.minY + floor((currentIconFrame.size.height - progressDiameter) / 2.0)
|
||||
),
|
||||
size: CGSize(width: progressDiameter, height: progressDiameter)
|
||||
)
|
||||
controlAreaWidth = 86.0
|
||||
} else {
|
||||
progressFrame = CGRect(origin: CGPoint(x: 0.0, y: isVoice ? -5.0 : 0.0), size: CGSize(width: progressDiameter, height: progressDiameter))
|
||||
progressFrame = CGRect(
|
||||
origin: CGPoint(x: 3.0, y: -3.0),
|
||||
size: CGSize(width: progressDiameter, height: progressDiameter)
|
||||
)
|
||||
controlAreaWidth = progressFrame.maxX + 8.0
|
||||
}
|
||||
|
||||
@ -506,7 +508,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
|
||||
let descriptionFrame: CGRect
|
||||
if isVoice {
|
||||
descriptionFrame = CGRect(origin: CGPoint(x: 43.0, y: 19.0), size: descriptionLayout.size)
|
||||
descriptionFrame = CGRect(origin: CGPoint(x: 56.0, y: 22.0), size: descriptionLayout.size)
|
||||
} else {
|
||||
descriptionFrame = CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY - 1.0), size: descriptionLayout.size)
|
||||
}
|
||||
@ -516,7 +518,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
let textSizes = titleFrame.union(descriptionFrame).size
|
||||
fittedLayoutSize = CGSize(width: textSizes.width + controlAreaWidth, height: 59.0)
|
||||
} else if isVoice {
|
||||
fittedLayoutSize = CGSize(width: minLayoutWidth, height: 27.0)
|
||||
fittedLayoutSize = CGSize(width: minLayoutWidth, height: 38.0)
|
||||
} else {
|
||||
let unionSize = titleFrame.union(descriptionFrame).union(progressFrame).size
|
||||
fittedLayoutSize = CGSize(width: unionSize.width, height: unionSize.height + 6.0)
|
||||
@ -529,8 +531,9 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
if let statusFrameValue = statusFrame, descriptionFrame.intersects(statusFrameValue) {
|
||||
fittedLayoutSize.height += statusFrameValue.height
|
||||
statusFrame = statusFrameValue.offsetBy(dx: 0.0, dy: statusFrameValue.height)
|
||||
let intersection = descriptionFrame.intersection(statusFrameValue)
|
||||
let addedWidth = intersection.width + 20
|
||||
fittedLayoutSize.width += addedWidth
|
||||
}
|
||||
if let statusFrameValue = statusFrame, let iconFrame = iconFrame, iconFrame.intersects(statusFrameValue) {
|
||||
fittedLayoutSize.height += 15.0
|
||||
@ -598,7 +601,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
strongSelf.waveformScrubbingNode = waveformScrubbingNode
|
||||
strongSelf.addSubnode(waveformScrubbingNode)
|
||||
}
|
||||
strongSelf.waveformScrubbingNode?.frame = CGRect(origin: CGPoint(x: 43.0, y: -1.0), size: CGSize(width: boundingWidth - 41.0, height: 12.0))
|
||||
strongSelf.waveformScrubbingNode?.frame = CGRect(origin: CGPoint(x: 57.0, y: 1.0), size: CGSize(width: boundingWidth - 60.0, height: 15.0))
|
||||
let waveformColor: UIColor
|
||||
if incoming {
|
||||
if consumableContentIcon != nil {
|
||||
@ -679,7 +682,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
strongSelf.waveformNode.displaysAsynchronously = !presentationData.isPreview
|
||||
strongSelf.statusNode?.displaysAsynchronously = !presentationData.isPreview
|
||||
strongSelf.statusNode?.frame = progressFrame
|
||||
strongSelf.playbackAudioLevelView?.frame = progressFrame.insetBy(dx: -20.0, dy: -20.0)
|
||||
strongSelf.playbackAudioLevelView?.frame = progressFrame.insetBy(dx: -12.0, dy: -12.0)
|
||||
strongSelf.progressFrame = progressFrame
|
||||
strongSelf.streamingCacheStatusFrame = streamingCacheStatusFrame
|
||||
strongSelf.fileIconImage = fileIconImage
|
||||
@ -860,19 +863,35 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
let statusNode = SemanticStatusNode(backgroundNodeColor: backgroundNodeColor, foregroundNodeColor: foregroundNodeColor)
|
||||
self.statusNode = statusNode
|
||||
statusNode.frame = progressFrame
|
||||
|
||||
if self.playbackAudioLevelView == nil, false {
|
||||
let playbackAudioLevelView = VoiceBlobView(frame: progressFrame.insetBy(dx: -20.0, dy: -20.0))
|
||||
playbackAudioLevelView.setColor(presentationData.theme.theme.chat.inputPanel.actionControlFillColor)
|
||||
self.playbackAudioLevelView = playbackAudioLevelView
|
||||
self.view.addSubview(playbackAudioLevelView)
|
||||
}
|
||||
|
||||
self.addSubnode(statusNode)
|
||||
} else if let statusNode = self.statusNode {
|
||||
statusNode.backgroundNodeColor = backgroundNodeColor
|
||||
}
|
||||
|
||||
if state != .none && isVoice && self.playbackAudioLevelView == nil {
|
||||
let blobFrame = progressFrame.insetBy(dx: -12.0, dy: -12.0)
|
||||
let playbackAudioLevelView = VoiceBlobView(
|
||||
frame: blobFrame,
|
||||
maxLevel: 0.3,
|
||||
smallBlobRange: (0, 0),
|
||||
mediumBlobRange: (0.7, 0.8),
|
||||
bigBlobRange: (0.8, 0.9)
|
||||
)
|
||||
self.playbackAudioLevelView = playbackAudioLevelView
|
||||
self.view.addSubview(playbackAudioLevelView)
|
||||
|
||||
let maskRect = CGRect(origin: .zero, size: blobFrame.size)
|
||||
let playbackMaskLayer = CAShapeLayer()
|
||||
playbackMaskLayer.frame = maskRect
|
||||
playbackMaskLayer.fillRule = .evenOdd
|
||||
let maskPath = UIBezierPath()
|
||||
maskPath.append(UIBezierPath(roundedRect: maskRect.insetBy(dx: 12, dy: 12), cornerRadius: 22))
|
||||
maskPath.append(UIBezierPath(rect: maskRect))
|
||||
playbackMaskLayer.path = maskPath.cgPath
|
||||
playbackAudioLevelView.layer.mask = playbackMaskLayer
|
||||
}
|
||||
self.playbackAudioLevelView?.setColor(presentationData.theme.theme.chat.inputPanel.actionControlFillColor)
|
||||
|
||||
if streamingState != .none && self.streamingStatusNode == nil {
|
||||
let streamingStatusNode = RadialStatusNode(backgroundNodeColor: .clear)
|
||||
self.streamingStatusNode = streamingStatusNode
|
||||
@ -893,6 +912,13 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
statusNode?.removeFromSupernode()
|
||||
}
|
||||
})
|
||||
|
||||
switch state {
|
||||
case .pause:
|
||||
self.playbackAudioLevelView?.startAnimating()
|
||||
default:
|
||||
self.playbackAudioLevelView?.stopAnimating()
|
||||
}
|
||||
}
|
||||
|
||||
if let streamingStatusNode = self.streamingStatusNode {
|
||||
|
@ -238,7 +238,13 @@ final class ChatTextInputMediaRecordingButton: TGModernConversationInputMicButto
|
||||
}
|
||||
|
||||
private lazy var micDecoration: (UIView & TGModernConversationInputMicButtonDecoration) = {
|
||||
let blobView = VoiceBlobView(frame: CGRect(origin: CGPoint(), size: CGSize(width: 220.0, height: 220.0)))
|
||||
let blobView = VoiceBlobView(
|
||||
frame: CGRect(origin: CGPoint(), size: CGSize(width: 220.0, height: 220.0)),
|
||||
maxLevel: 4,
|
||||
smallBlobRange: (0.45, 0.55),
|
||||
mediumBlobRange: (0.52, 0.87),
|
||||
bigBlobRange: (0.57, 1.00)
|
||||
)
|
||||
blobView.setColor(self.theme.chat.inputPanel.actionControlFillColor)
|
||||
return blobView
|
||||
}()
|
||||
|
Loading…
x
Reference in New Issue
Block a user