mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 22:55:00 +00:00
Various Improvements
This commit is contained in:
@@ -9,15 +9,35 @@ import LegacyComponents
|
||||
private final class InstantVideoRadialStatusNodeParameters: NSObject {
|
||||
let color: UIColor
|
||||
let progress: CGFloat
|
||||
let dimProgress: CGFloat
|
||||
let playProgress: CGFloat
|
||||
|
||||
init(color: UIColor, progress: CGFloat) {
|
||||
init(color: UIColor, progress: CGFloat, dimProgress: CGFloat, playProgress: CGFloat) {
|
||||
self.color = color
|
||||
self.progress = progress
|
||||
self.dimProgress = dimProgress
|
||||
self.playProgress = playProgress
|
||||
}
|
||||
}
|
||||
|
||||
final class InstantVideoRadialStatusNode: ASDisplayNode {
|
||||
private extension CGFloat {
|
||||
var degrees: CGFloat {
|
||||
return self * CGFloat(180) / .pi
|
||||
}
|
||||
}
|
||||
|
||||
private extension CGPoint {
|
||||
func angle(to otherPoint: CGPoint) -> CGFloat {
|
||||
let originX = otherPoint.x - x
|
||||
let originY = otherPoint.y - y
|
||||
let bearingRadians = atan2f(Float(originY), Float(originX))
|
||||
return CGFloat(bearingRadians)
|
||||
}
|
||||
}
|
||||
|
||||
final class InstantVideoRadialStatusNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
private let color: UIColor
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
private var effectiveProgress: CGFloat = 0.0 {
|
||||
didSet {
|
||||
@@ -25,6 +45,22 @@ final class InstantVideoRadialStatusNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
private var seeking = false
|
||||
private var seekingProgress: CGFloat?
|
||||
|
||||
private var dimmed = false
|
||||
private var effectiveDimProgress: CGFloat = 0.0 {
|
||||
didSet {
|
||||
self.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
private var effectivePlayProgress: CGFloat = 0.0 {
|
||||
didSet {
|
||||
self.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
private var _statusValue: MediaPlayerStatus?
|
||||
private var statusValue: MediaPlayerStatus? {
|
||||
get {
|
||||
@@ -58,6 +94,11 @@ final class InstantVideoRadialStatusNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
var tapGestureRecognizer: UITapGestureRecognizer?
|
||||
var panGestureRecognizer: UIPanGestureRecognizer?
|
||||
|
||||
var seekTo: ((Double, Bool) -> Void)?
|
||||
|
||||
init(color: UIColor) {
|
||||
self.color = color
|
||||
|
||||
@@ -66,19 +107,113 @@ final class InstantVideoRadialStatusNode: ASDisplayNode {
|
||||
self.isOpaque = false
|
||||
|
||||
self.statusDisposable = (self.statusValuePromise.get()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] status in
|
||||
if let strongSelf = self {
|
||||
strongSelf.statusValue = status
|
||||
}
|
||||
})
|
||||
|> deliverOnMainQueue).start(next: { [weak self] status in
|
||||
if let strongSelf = self {
|
||||
strongSelf.statusValue = status
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.statusDisposable?.dispose()
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
|
||||
tapGestureRecognizer.delegate = self
|
||||
self.view.addGestureRecognizer(tapGestureRecognizer)
|
||||
|
||||
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
|
||||
panGestureRecognizer.delegate = self
|
||||
self.view.addGestureRecognizer(panGestureRecognizer)
|
||||
}
|
||||
|
||||
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if gestureRecognizer === self.tapGestureRecognizer || gestureRecognizer === self.panGestureRecognizer {
|
||||
let center = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0)
|
||||
let location = gestureRecognizer.location(in: self.view)
|
||||
let distanceFromCenter = location.distanceTo(center)
|
||||
if distanceFromCenter < self.bounds.width * 0.15 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if gestureRecognizer === self.panGestureRecognizer, let otherGestureRecognizer = otherGestureRecognizer as? UIPanGestureRecognizer {
|
||||
otherGestureRecognizer.isEnabled = false
|
||||
otherGestureRecognizer.isEnabled = true
|
||||
return true
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||
let center = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0)
|
||||
let location = gestureRecognizer.location(in: self.view)
|
||||
|
||||
var angle = center.angle(to: location) + CGFloat.pi / 2.0
|
||||
if angle < 0.0 {
|
||||
angle = CGFloat.pi * 2.0 + angle
|
||||
}
|
||||
let fraction = max(0.0, min(1.0, Double(angle / (2.0 * CGFloat.pi))))
|
||||
self.seekTo?(min(0.99, fraction), true)
|
||||
}
|
||||
|
||||
@objc private func panGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
let center = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0)
|
||||
let location = gestureRecognizer.location(in: self.view)
|
||||
var angle = center.angle(to: location) + CGFloat.pi / 2.0
|
||||
if angle < 0.0 {
|
||||
angle = CGFloat.pi * 2.0 + angle
|
||||
}
|
||||
let fraction = max(0.0, min(1.0, Double(angle / (2.0 * CGFloat.pi))))
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
self.seeking = true
|
||||
|
||||
let playAnimation = POPSpringAnimation()
|
||||
playAnimation.property = POPAnimatableProperty.property(withName: "playProgress", initializer: { property in
|
||||
property?.readBlock = { node, values in
|
||||
values?.pointee = (node as! InstantVideoRadialStatusNode).effectivePlayProgress
|
||||
}
|
||||
property?.writeBlock = { node, values in
|
||||
(node as! InstantVideoRadialStatusNode).effectivePlayProgress = values!.pointee
|
||||
}
|
||||
property?.threshold = 0.01
|
||||
}) as? POPAnimatableProperty
|
||||
playAnimation.fromValue = self.effectivePlayProgress as NSNumber
|
||||
playAnimation.toValue = 0.0 as NSNumber
|
||||
playAnimation.springSpeed = 20
|
||||
playAnimation.springBounciness = 8
|
||||
self.pop_add(playAnimation, forKey: "playProgress")
|
||||
case .changed:
|
||||
if let seekingProgress = self.seekingProgress {
|
||||
if seekingProgress > 0.98 && fraction > 0.0 && fraction < 0.05 {
|
||||
self.hapticFeedback.impact(.light)
|
||||
} else if seekingProgress > 0.0 && seekingProgress < 0.05 && fraction > 0.98 {
|
||||
self.hapticFeedback.impact(.light)
|
||||
}
|
||||
}
|
||||
self.seekTo?(min(0.99, fraction), false)
|
||||
self.seekingProgress = CGFloat(fraction)
|
||||
case .ended, .cancelled:
|
||||
self.seeking = false
|
||||
self.seekTo?(min(0.99, fraction), true)
|
||||
self.seekingProgress = nil
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
|
||||
return InstantVideoRadialStatusNodeParameters(color: self.color, progress: self.effectiveProgress)
|
||||
return InstantVideoRadialStatusNodeParameters(color: self.color, progress: self.effectiveProgress, dimProgress: self.effectiveDimProgress, playProgress: self.effectivePlayProgress)
|
||||
}
|
||||
|
||||
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
|
||||
@@ -93,20 +228,59 @@ final class InstantVideoRadialStatusNode: ASDisplayNode {
|
||||
if let parameters = parameters as? InstantVideoRadialStatusNodeParameters {
|
||||
context.setStrokeColor(parameters.color.cgColor)
|
||||
|
||||
if !parameters.dimProgress.isZero {
|
||||
context.setFillColor(UIColor(rgb: 0x000000, alpha: 0.35 * min(1.0, parameters.dimProgress)).cgColor)
|
||||
context.fillEllipse(in: bounds)
|
||||
}
|
||||
|
||||
context.setBlendMode(.normal)
|
||||
|
||||
var progress = parameters.progress
|
||||
let startAngle = -CGFloat.pi / 2.0
|
||||
let endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle
|
||||
|
||||
progress = min(1.0, progress)
|
||||
|
||||
let lineWidth: CGFloat = 4.0
|
||||
var lineWidth: CGFloat = 4.0
|
||||
lineWidth += 1.0 * parameters.dimProgress
|
||||
|
||||
let pathDiameter = bounds.size.width - lineWidth - 8.0
|
||||
var pathDiameter = bounds.size.width - lineWidth - 8.0
|
||||
pathDiameter -= (18.0 * 2.0) * parameters.dimProgress
|
||||
|
||||
if !parameters.dimProgress.isZero {
|
||||
context.setStrokeColor(parameters.color.withAlphaComponent(0.2 * parameters.dimProgress).cgColor)
|
||||
context.setLineWidth(lineWidth)
|
||||
context.strokeEllipse(in: CGRect(x: (bounds.size.width - pathDiameter) / 2.0 , y: (bounds.size.height - pathDiameter) / 2.0, width: pathDiameter, height: pathDiameter))
|
||||
|
||||
if !parameters.playProgress.isZero {
|
||||
context.saveGState()
|
||||
context.translateBy(x: bounds.width / 2.0, y: bounds.height / 2.0)
|
||||
context.scaleBy(x: 1.0 + 1.4 * parameters.playProgress, y: 1.0 + 1.4 * parameters.playProgress)
|
||||
context.translateBy(x: -bounds.width / 2.0, y: -bounds.height / 2.0)
|
||||
|
||||
let iconSize = CGSize(width: 15.0, height: 18.0)
|
||||
context.translateBy(x: (bounds.width - iconSize.width) / 2.0 + 2.0, y: (bounds.height - iconSize.height) / 2.0)
|
||||
|
||||
context.setFillColor(UIColor(rgb: 0xffffff).withAlphaComponent(min(1.0, parameters.playProgress)).cgColor)
|
||||
let _ = try? drawSvgPath(context, path: "M1.71891969,0.209353049 C0.769586558,-0.350676705 0,0.0908839327 0,1.18800046 L0,16.8564753 C0,17.9569971 0.750549162,18.357187 1.67393713,17.7519379 L14.1073836,9.60224049 C15.0318735,8.99626906 15.0094718,8.04970371 14.062401,7.49100858 L1.71891969,0.209353049 ")
|
||||
context.fillPath()
|
||||
|
||||
context.restoreGState()
|
||||
}
|
||||
}
|
||||
|
||||
context.setStrokeColor(parameters.color.cgColor)
|
||||
let path = UIBezierPath(arcCenter: CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0), radius: pathDiameter / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise:true)
|
||||
path.lineWidth = lineWidth
|
||||
path.lineCapStyle = .round
|
||||
path.stroke()
|
||||
|
||||
let handleSide = 16.0 * min(1.0, (parameters.dimProgress * 2.0))
|
||||
let handleSize = CGSize(width: handleSide, height: handleSide)
|
||||
let handlePosition = CGPoint(x: 0.5 * pathDiameter * cos(endAngle), y: 0.5 * pathDiameter * sin(endAngle)).offsetBy(dx: bounds.size.width / 2.0, dy: bounds.size.height / 2.0)
|
||||
let handleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(handlePosition.x - handleSize.width / 2.0), y: floorToScreenPixels(handlePosition.y - handleSize.height / 2.0)), size: handleSize)
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
context.fillEllipse(in: handleFrame)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +292,53 @@ final class InstantVideoRadialStatusNode: ASDisplayNode {
|
||||
timestampAndDuration = nil
|
||||
}
|
||||
|
||||
if let (timestamp, duration, baseRate) = timestampAndDuration, let statusValue = self.statusValue {
|
||||
var dimmed = false
|
||||
if let statusValue = self.statusValue {
|
||||
dimmed = statusValue.status == .paused
|
||||
}
|
||||
if self.seeking {
|
||||
dimmed = true
|
||||
}
|
||||
if dimmed != self.dimmed {
|
||||
self.dimmed = dimmed
|
||||
|
||||
let animation = POPSpringAnimation()
|
||||
animation.property = POPAnimatableProperty.property(withName: "dimProgress", initializer: { property in
|
||||
property?.readBlock = { node, values in
|
||||
values?.pointee = (node as! InstantVideoRadialStatusNode).effectiveDimProgress
|
||||
}
|
||||
property?.writeBlock = { node, values in
|
||||
(node as! InstantVideoRadialStatusNode).effectiveDimProgress = values!.pointee
|
||||
}
|
||||
property?.threshold = 0.01
|
||||
}) as? POPAnimatableProperty
|
||||
animation.fromValue = self.effectiveDimProgress as NSNumber
|
||||
animation.toValue = (dimmed ? 1.0 : 0.0) as NSNumber
|
||||
animation.springSpeed = 20
|
||||
animation.springBounciness = 8
|
||||
self.pop_add(animation, forKey: "dimProgress")
|
||||
|
||||
let playAnimation = POPSpringAnimation()
|
||||
playAnimation.property = POPAnimatableProperty.property(withName: "playProgress", initializer: { property in
|
||||
property?.readBlock = { node, values in
|
||||
values?.pointee = (node as! InstantVideoRadialStatusNode).effectivePlayProgress
|
||||
}
|
||||
property?.writeBlock = { node, values in
|
||||
(node as! InstantVideoRadialStatusNode).effectivePlayProgress = values!.pointee
|
||||
}
|
||||
property?.threshold = 0.01
|
||||
}) as? POPAnimatableProperty
|
||||
playAnimation.fromValue = self.effectivePlayProgress as NSNumber
|
||||
playAnimation.toValue = (dimmed ? 1.0 : 0.0) as NSNumber
|
||||
playAnimation.springSpeed = 20
|
||||
playAnimation.springBounciness = 8
|
||||
self.pop_add(playAnimation, forKey: "playProgress")
|
||||
}
|
||||
|
||||
if self.seeking, let progress = self.seekingProgress {
|
||||
self.pop_removeAnimation(forKey: "progress")
|
||||
self.effectiveProgress = progress
|
||||
} else if let (timestamp, duration, baseRate) = timestampAndDuration, let statusValue = self.statusValue {
|
||||
let progress = CGFloat(timestamp / duration)
|
||||
|
||||
if progress.isNaN || !progress.isFinite {
|
||||
@@ -148,6 +368,9 @@ final class InstantVideoRadialStatusNode: ASDisplayNode {
|
||||
self.pop_add(animation, forKey: "progress")
|
||||
}
|
||||
} else {
|
||||
self.pop_removeAnimation(forKey: "dimProgress")
|
||||
self.effectiveDimProgress = 0.0
|
||||
|
||||
self.pop_removeAnimation(forKey: "progress")
|
||||
self.effectiveProgress = 0.0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user