mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
270 lines
11 KiB
Swift
270 lines
11 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import MediaPlayer
|
|
import SwiftSignalKit
|
|
|
|
private let volumeNotificationKey = "AVSystemController_SystemVolumeDidChangeNotification"
|
|
private let volumeParameterKey = "AVSystemController_AudioVolumeNotificationParameter"
|
|
private let changeReasonParameterKey = "AVSystemController_AudioVolumeChangeReasonNotificationParameter"
|
|
private let explicitChangeReasonValue = "ExplicitVolumeChange"
|
|
|
|
private final class VolumeView: MPVolumeView {
|
|
@objc func _updateWirelessRouteStatus() {
|
|
}
|
|
}
|
|
|
|
final class VolumeControlStatusBar: UIView {
|
|
private let control: VolumeView
|
|
private var observer: Any?
|
|
private var currentValue: Float
|
|
|
|
var valueChanged: ((Float, Float) -> Void)?
|
|
|
|
private var disposable: Disposable?
|
|
private var ignoreAdjustmentOnce = false
|
|
|
|
init(frame: CGRect, shouldBeVisible: Signal<Bool, NoError>) {
|
|
self.control = VolumeView(frame: CGRect(origin: CGPoint(x: -100.0, y: -100.0), size: CGSize(width: 100.0, height: 20.0)))
|
|
self.control.alpha = 0.0001
|
|
self.currentValue = AVAudioSession.sharedInstance().outputVolume
|
|
|
|
super.init(frame: frame)
|
|
|
|
self.addSubview(self.control)
|
|
self.observer = NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: volumeNotificationKey), object: nil, queue: OperationQueue.main, using: { [weak self] notification in
|
|
if let strongSelf = self, let userInfo = notification.userInfo {
|
|
if let volume = userInfo[volumeParameterKey] as? Float {
|
|
let previous = strongSelf.currentValue
|
|
if !previous.isEqual(to: volume) {
|
|
strongSelf.currentValue = volume
|
|
if strongSelf.ignoreAdjustmentOnce {
|
|
strongSelf.ignoreAdjustmentOnce = false
|
|
} else {
|
|
if strongSelf.control.superview != nil {
|
|
if let reason = userInfo[changeReasonParameterKey], reason as? String != explicitChangeReasonValue {
|
|
return
|
|
}
|
|
strongSelf.valueChanged?(previous, volume)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
self.disposable = (shouldBeVisible
|
|
|> deliverOnMainQueue).start(next: { [weak self] value in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if value {
|
|
if strongSelf.control.superview == nil {
|
|
strongSelf.ignoreAdjustmentOnce = true
|
|
strongSelf.addSubview(strongSelf.control)
|
|
}
|
|
} else {
|
|
strongSelf.control.removeFromSuperview()
|
|
strongSelf.ignoreAdjustmentOnce = false
|
|
}
|
|
})
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
if let observer = self.observer {
|
|
NotificationCenter.default.removeObserver(observer)
|
|
}
|
|
self.disposable?.dispose()
|
|
}
|
|
}
|
|
|
|
final class VolumeControlStatusBarNode: ASDisplayNode {
|
|
var innerGraphics: (UIImage, UIImage, UIImage, Bool)?
|
|
var graphics: (UIImage, UIImage, UIImage)? = nil {
|
|
didSet {
|
|
if self.isDark {
|
|
self.innerGraphics = generateDarkGraphics(self.graphics)
|
|
} else {
|
|
if let graphics = self.graphics {
|
|
self.innerGraphics = (graphics.0, graphics.1, graphics.2, false)
|
|
} else {
|
|
self.innerGraphics = nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
private let outlineNode: ASImageNode
|
|
private let backgroundNode: ASImageNode
|
|
private let iconNode: ASImageNode
|
|
private let foregroundNode: ASImageNode
|
|
private let foregroundClippingNode: ASDisplayNode
|
|
|
|
private var validLayout: ContainerViewLayout?
|
|
|
|
var isDark: Bool = false {
|
|
didSet {
|
|
if self.isDark != oldValue {
|
|
if self.isDark {
|
|
self.outlineNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: UIColor(white: 0.0, alpha: 0.7))
|
|
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 4.0, color: UIColor(white: 0.6, alpha: 1.0))
|
|
self.foregroundNode.image = generateStretchableFilledCircleImage(diameter: 4.0, color: .white)
|
|
|
|
self.innerGraphics = generateDarkGraphics(self.graphics)
|
|
} else {
|
|
self.outlineNode.image = nil
|
|
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 4.0, color: UIColor(rgb: 0xc5c5c5))
|
|
self.foregroundNode.image = generateStretchableFilledCircleImage(diameter: 4.0, color: .black)
|
|
|
|
if let graphics = self.graphics {
|
|
self.innerGraphics = (graphics.0, graphics.1, graphics.2, false)
|
|
}
|
|
}
|
|
self.updateIcon()
|
|
}
|
|
}
|
|
}
|
|
private var value: CGFloat = 1.0
|
|
|
|
override init() {
|
|
self.outlineNode = ASImageNode()
|
|
self.outlineNode.isLayerBacked = true
|
|
self.outlineNode.displaysAsynchronously = false
|
|
|
|
self.backgroundNode = ASImageNode()
|
|
self.backgroundNode.isLayerBacked = true
|
|
self.backgroundNode.displaysAsynchronously = false
|
|
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 4.0, color: UIColor(rgb: 0xc5c5c5))
|
|
|
|
self.foregroundNode = ASImageNode()
|
|
self.foregroundNode.isLayerBacked = true
|
|
self.foregroundNode.displaysAsynchronously = false
|
|
self.foregroundNode.image = generateStretchableFilledCircleImage(diameter: 4.0, color: .black)
|
|
|
|
self.foregroundClippingNode = ASDisplayNode()
|
|
self.foregroundClippingNode.clipsToBounds = true
|
|
self.foregroundClippingNode.addSubnode(self.foregroundNode)
|
|
|
|
self.iconNode = ASImageNode()
|
|
self.iconNode.isLayerBacked = true
|
|
self.iconNode.displaysAsynchronously = false
|
|
|
|
super.init()
|
|
|
|
self.isUserInteractionEnabled = false
|
|
|
|
self.addSubnode(self.outlineNode)
|
|
self.addSubnode(self.backgroundNode)
|
|
self.addSubnode(self.foregroundClippingNode)
|
|
self.addSubnode(self.iconNode)
|
|
}
|
|
|
|
func generateDarkGraphics(_ graphics: (UIImage, UIImage, UIImage)?) -> (UIImage, UIImage, UIImage, Bool)? {
|
|
if var (offImage, halfImage, onImage) = graphics {
|
|
offImage = generateTintedImage(image: offImage, color: UIColor.white)!
|
|
halfImage = generateTintedImage(image: halfImage, color: UIColor.white)!
|
|
onImage = generateTintedImage(image: onImage, color: UIColor.white)!
|
|
return (offImage, halfImage, onImage, true)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func updateGraphics() {
|
|
if self.isDark {
|
|
self.outlineNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: UIColor(white: 0.0, alpha: 0.7))
|
|
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 4.0, color: UIColor(white: 0.6, alpha: 1.0))
|
|
self.foregroundNode.image = generateStretchableFilledCircleImage(diameter: 4.0, color: .white)
|
|
} else {
|
|
self.outlineNode.image = nil
|
|
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 4.0, color: UIColor(white: 0.6, alpha: 1.0))
|
|
self.foregroundNode.image = generateStretchableFilledCircleImage(diameter: 4.0, color: .black)
|
|
}
|
|
}
|
|
|
|
func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
|
self.validLayout = layout
|
|
|
|
let barHeight: CGFloat = 4.0
|
|
var barWidth: CGFloat
|
|
|
|
let statusBarHeight: CGFloat
|
|
var sideInset: CGFloat
|
|
if let actual = layout.statusBarHeight {
|
|
statusBarHeight = actual
|
|
} else {
|
|
statusBarHeight = 24.0
|
|
}
|
|
if layout.safeInsets.left.isZero && layout.safeInsets.top.isZero && layout.intrinsicInsets.left.isZero && layout.intrinsicInsets.top.isZero {
|
|
sideInset = 4.0
|
|
} else {
|
|
sideInset = 12.0
|
|
}
|
|
|
|
let iconRect = CGRect(x: sideInset + 4.0, y: 14.0, width: 21.0, height: 16.0)
|
|
if !layout.intrinsicInsets.bottom.isZero {
|
|
if layout.size.width > 375.0 {
|
|
barWidth = 88.0 - sideInset * 2.0
|
|
} else {
|
|
barWidth = 80.0 - sideInset * 2.0
|
|
}
|
|
if layout.size.width < layout.size.height {
|
|
self.outlineNode.isHidden = true
|
|
} else {
|
|
self.outlineNode.isHidden = false
|
|
}
|
|
if self.graphics != nil {
|
|
if layout.size.width < layout.size.height {
|
|
self.iconNode.isHidden = false
|
|
barWidth -= iconRect.width - 8.0
|
|
sideInset += iconRect.width + 8.0
|
|
} else {
|
|
sideInset += layout.safeInsets.left
|
|
self.iconNode.isHidden = true
|
|
}
|
|
}
|
|
} else {
|
|
self.iconNode.isHidden = true
|
|
barWidth = layout.size.width - sideInset * 2.0
|
|
}
|
|
|
|
let boundingRect = CGRect(origin: CGPoint(x: sideInset, y: floor((statusBarHeight - barHeight) / 2.0)), size: CGSize(width: barWidth, height: barHeight))
|
|
|
|
transition.updateFrame(node: self.iconNode, frame: iconRect)
|
|
transition.updateFrame(node: self.outlineNode, frame: boundingRect.insetBy(dx: -4.0, dy: -4.0))
|
|
transition.updateFrame(node: self.backgroundNode, frame: boundingRect)
|
|
transition.updateFrame(node: self.foregroundNode, frame: CGRect(origin: CGPoint(), size: boundingRect.size))
|
|
transition.updateFrame(node: self.foregroundClippingNode, frame: CGRect(origin: boundingRect.origin, size: CGSize(width: self.value * boundingRect.width, height: boundingRect.height)))
|
|
}
|
|
|
|
func updateValue(from fromValue: CGFloat, to toValue: CGFloat) {
|
|
if let layout = self.validLayout {
|
|
if self.foregroundClippingNode.layer.animation(forKey: "bounds") == nil {
|
|
self.value = fromValue
|
|
self.updateLayout(layout: layout, transition: .immediate)
|
|
}
|
|
self.value = toValue
|
|
self.updateLayout(layout: layout, transition: .animated(duration: 0.25, curve: .spring))
|
|
|
|
self.updateIcon()
|
|
} else {
|
|
self.value = toValue
|
|
}
|
|
}
|
|
|
|
private func updateIcon() {
|
|
if let graphics = self.innerGraphics {
|
|
if self.value > 0.5 {
|
|
self.iconNode.image = graphics.2
|
|
} else if self.value > 0.001 {
|
|
self.iconNode.image = graphics.1
|
|
} else {
|
|
self.iconNode.image = graphics.0
|
|
}
|
|
}
|
|
}
|
|
}
|