mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
1095 lines
55 KiB
Swift
1095 lines
55 KiB
Swift
import Foundation
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import SwiftSignalKit
|
|
import RangeSet
|
|
import TextFormat
|
|
|
|
public enum MediaPlayerScrubbingNodeCap {
|
|
case square
|
|
case round
|
|
}
|
|
|
|
private func generateHandleBackground(color: UIColor) -> UIImage? {
|
|
return generateImage(CGSize(width: 2.0, height: 4.0), contextGenerator: { size, context in
|
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
|
context.setFillColor(color.cgColor)
|
|
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: 1.5, height: 1.5)))
|
|
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: size.height - 1.5), size: CGSize(width: 1.5, height: 1.5)))
|
|
context.fill(CGRect(origin: CGPoint(x: 0.0, y: 1.5 / 2.0), size: CGSize(width: 1.5, height: size.height - 1.5)))
|
|
})?.stretchableImage(withLeftCapWidth: 0, topCapHeight: 2)
|
|
}
|
|
|
|
public struct MediaPlayerScrubbingChapter: Equatable {
|
|
public let title: String
|
|
public let start: Double
|
|
|
|
public init(title: String, start: Double) {
|
|
self.title = title
|
|
self.start = start
|
|
}
|
|
}
|
|
|
|
public func parseMediaPlayerChapters(_ string: NSAttributedString) -> [MediaPlayerScrubbingChapter] {
|
|
var existingTimecodes = Set<Double>()
|
|
var timecodeRanges: [(NSRange, TelegramTimecode)] = []
|
|
var lineRanges: [NSRange] = []
|
|
string.enumerateAttributes(in: NSMakeRange(0, string.length), options: [], using: { attributes, range, _ in
|
|
if let timecode = attributes[NSAttributedString.Key(TelegramTextAttributes.Timecode)] as? TelegramTimecode {
|
|
if !existingTimecodes.contains(timecode.time) {
|
|
timecodeRanges.append((range, timecode))
|
|
existingTimecodes.insert(timecode.time)
|
|
}
|
|
}
|
|
})
|
|
(string.string as NSString).enumerateSubstrings(in: NSMakeRange(0, string.length), options: .byLines, using: { _, range, _, _ in
|
|
lineRanges.append(range)
|
|
})
|
|
|
|
var chapters: [MediaPlayerScrubbingChapter] = []
|
|
for (timecodeRange, timecode) in timecodeRanges {
|
|
inner: for lineRange in lineRanges {
|
|
if lineRange.contains(timecodeRange.location) {
|
|
if lineRange.length > timecodeRange.length && timecodeRange.location < lineRange.location + 4 {
|
|
var title = ((string.string as NSString).substring(with: lineRange) as NSString).replacingCharacters(in: NSMakeRange(timecodeRange.location - lineRange.location, timecodeRange.length), with: "")
|
|
title = title.trimmingCharacters(in: .whitespacesAndNewlines).trimmingCharacters(in: .punctuationCharacters)
|
|
chapters.append(MediaPlayerScrubbingChapter(title: title, start: timecode.time))
|
|
}
|
|
break inner
|
|
}
|
|
}
|
|
}
|
|
|
|
return chapters
|
|
}
|
|
|
|
private final class MediaPlayerScrubbingNodeButton: ASDisplayNode, ASGestureRecognizerDelegate {
|
|
var beginScrubbing: (() -> Void)?
|
|
var endScrubbing: ((Bool) -> Void)?
|
|
var updateScrubbing: ((CGFloat, Double) -> Void)?
|
|
var updateMultiplier: ((Double) -> Void)?
|
|
|
|
var highlighted: ((Bool) -> Void)?
|
|
|
|
var verticalPanEnabled = false
|
|
private let hapticFeedback = HapticFeedback()
|
|
|
|
private var scrubbingMultiplier: Double = 1.0
|
|
private var scrubbingStartLocation: CGPoint?
|
|
|
|
override func didLoad() {
|
|
super.didLoad()
|
|
|
|
self.view.disablesInteractiveTransitionGestureRecognizer = true
|
|
|
|
let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
|
|
gestureRecognizer.delegate = self.wrappedGestureRecognizerDelegate
|
|
self.view.addGestureRecognizer(gestureRecognizer)
|
|
}
|
|
|
|
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
guard let _ = gestureRecognizer as? UIPanGestureRecognizer else {
|
|
return !self.verticalPanEnabled
|
|
}
|
|
return true
|
|
}
|
|
|
|
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
super.touchesBegan(touches, with: event)
|
|
|
|
self.highlighted?(true)
|
|
}
|
|
|
|
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
super.touchesEnded(touches, with: event)
|
|
|
|
if self.scrubbingStartLocation == nil {
|
|
self.highlighted?(false)
|
|
}
|
|
}
|
|
|
|
override func touchesCancelled(_ touches: Set<UITouch>?, with event: UIEvent?) {
|
|
super.touchesCancelled(touches, with: event)
|
|
|
|
if self.scrubbingStartLocation == nil {
|
|
self.highlighted?(false)
|
|
}
|
|
}
|
|
|
|
@objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
|
var location = recognizer.location(in: self.view)
|
|
location.x -= self.bounds.minX
|
|
switch recognizer.state {
|
|
case .began:
|
|
self.scrubbingStartLocation = location
|
|
self.beginScrubbing?()
|
|
case .changed:
|
|
if let scrubbingStartLocation = self.scrubbingStartLocation {
|
|
let delta = location.x - scrubbingStartLocation.x
|
|
var multiplier: Double = 1.0
|
|
var skipUpdate = false
|
|
if self.verticalPanEnabled, location.y > scrubbingStartLocation.y {
|
|
let verticalDelta = abs(location.y - scrubbingStartLocation.y)
|
|
if verticalDelta > 150.0 {
|
|
multiplier = 0.01
|
|
} else if verticalDelta > 100.0 {
|
|
multiplier = 0.25
|
|
} else if verticalDelta > 50.0 {
|
|
multiplier = 0.5
|
|
}
|
|
if multiplier != self.scrubbingMultiplier {
|
|
skipUpdate = true
|
|
self.scrubbingMultiplier = multiplier
|
|
self.scrubbingStartLocation = CGPoint(x: location.x, y: scrubbingStartLocation.y)
|
|
self.updateMultiplier?(multiplier)
|
|
|
|
self.hapticFeedback.impact()
|
|
}
|
|
}
|
|
if !skipUpdate {
|
|
self.updateScrubbing?(delta / self.bounds.size.width, multiplier)
|
|
}
|
|
}
|
|
case .ended, .cancelled:
|
|
if let scrubbingStartLocation = self.scrubbingStartLocation {
|
|
self.scrubbingStartLocation = nil
|
|
let delta = location.x - scrubbingStartLocation.x
|
|
self.updateScrubbing?(delta / self.bounds.size.width, self.scrubbingMultiplier)
|
|
self.endScrubbing?(recognizer.state == .ended)
|
|
self.highlighted?(false)
|
|
self.scrubbingMultiplier = 1.0
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class MediaPlayerScrubbingForegroundNode: ASDisplayNode {
|
|
var onEnterHierarchy: (() -> Void)?
|
|
var onExitHierarchy: (() -> Void)?
|
|
|
|
override func willEnterHierarchy() {
|
|
super.willEnterHierarchy()
|
|
|
|
self.onEnterHierarchy?()
|
|
}
|
|
|
|
override func didExitHierarchy() {
|
|
super.didExitHierarchy()
|
|
|
|
self.onExitHierarchy?()
|
|
}
|
|
}
|
|
|
|
public enum MediaPlayerScrubbingNodeHandle {
|
|
case none
|
|
case line
|
|
case circle
|
|
}
|
|
|
|
public enum MediaPlayerScrubbingNodeContent {
|
|
case standard(lineHeight: CGFloat, lineCap: MediaPlayerScrubbingNodeCap, scrubberHandle: MediaPlayerScrubbingNodeHandle, backgroundColor: UIColor, foregroundColor: UIColor, bufferingColor: UIColor, chapters: [MediaPlayerScrubbingChapter])
|
|
case custom(backgroundNode: CustomMediaPlayerScrubbingForegroundNode, foregroundContentNode: CustomMediaPlayerScrubbingForegroundNode)
|
|
}
|
|
|
|
private final class StandardMediaPlayerScrubbingNodeContentNode {
|
|
let lineHeight: CGFloat
|
|
let lineCap: MediaPlayerScrubbingNodeCap
|
|
let containerNode: ASDisplayNode
|
|
let backgroundNode: ASImageNode
|
|
let bufferingNode: MediaPlayerScrubbingBufferingNode
|
|
let foregroundContentNode: ASImageNode
|
|
let foregroundNode: MediaPlayerScrubbingForegroundNode
|
|
let chapterNodesContainer: ASDisplayNode?
|
|
let chapterNodes: [(MediaPlayerScrubbingChapter, ASDisplayNode)]
|
|
let handle: MediaPlayerScrubbingNodeHandle
|
|
let handleNode: ASDisplayNode?
|
|
let highlightedHandleNode: ASDisplayNode?
|
|
let handleNodeContainer: MediaPlayerScrubbingNodeButton?
|
|
|
|
init(lineHeight: CGFloat, lineCap: MediaPlayerScrubbingNodeCap, containerNode: ASDisplayNode, backgroundNode: ASImageNode, bufferingNode: MediaPlayerScrubbingBufferingNode, foregroundContentNode: ASImageNode, foregroundNode: MediaPlayerScrubbingForegroundNode, chapterNodesContainer: ASDisplayNode?, chapterNodes: [(MediaPlayerScrubbingChapter, ASDisplayNode)], handle: MediaPlayerScrubbingNodeHandle, handleNode: ASDisplayNode?, highlightedHandleNode: ASDisplayNode?, handleNodeContainer: MediaPlayerScrubbingNodeButton?) {
|
|
self.lineHeight = lineHeight
|
|
self.lineCap = lineCap
|
|
self.containerNode = containerNode
|
|
self.backgroundNode = backgroundNode
|
|
self.bufferingNode = bufferingNode
|
|
self.foregroundContentNode = foregroundContentNode
|
|
self.foregroundNode = foregroundNode
|
|
self.chapterNodesContainer = chapterNodesContainer
|
|
self.chapterNodes = chapterNodes
|
|
self.handle = handle
|
|
self.handleNode = handleNode
|
|
self.highlightedHandleNode = highlightedHandleNode
|
|
self.handleNodeContainer = handleNodeContainer
|
|
}
|
|
}
|
|
|
|
public protocol CustomMediaPlayerScrubbingForegroundNode: ASDisplayNode {
|
|
var progress: CGFloat? { get set }
|
|
}
|
|
|
|
private final class CustomMediaPlayerScrubbingNodeContentNode {
|
|
let backgroundNode: CustomMediaPlayerScrubbingForegroundNode
|
|
let foregroundContentNode: CustomMediaPlayerScrubbingForegroundNode
|
|
let foregroundNode: MediaPlayerScrubbingForegroundNode
|
|
let handleNodeContainer: MediaPlayerScrubbingNodeButton?
|
|
|
|
init(backgroundNode: CustomMediaPlayerScrubbingForegroundNode, foregroundContentNode: CustomMediaPlayerScrubbingForegroundNode, foregroundNode: MediaPlayerScrubbingForegroundNode, handleNodeContainer: MediaPlayerScrubbingNodeButton?) {
|
|
self.backgroundNode = backgroundNode
|
|
self.foregroundContentNode = foregroundContentNode
|
|
self.foregroundNode = foregroundNode
|
|
self.handleNodeContainer = handleNodeContainer
|
|
}
|
|
}
|
|
|
|
private enum MediaPlayerScrubbingNodeContentNodes {
|
|
case standard(StandardMediaPlayerScrubbingNodeContentNode)
|
|
case custom(CustomMediaPlayerScrubbingNodeContentNode)
|
|
}
|
|
|
|
private final class MediaPlayerScrubbingBufferingNode: ASDisplayNode {
|
|
private let color: UIColor
|
|
private let containerNode: ASDisplayNode
|
|
private let foregroundNode: ASImageNode
|
|
|
|
private var ranges: (RangeSet<Int64>, Int64)?
|
|
|
|
init(color: UIColor, lineCap: MediaPlayerScrubbingNodeCap, lineHeight: CGFloat) {
|
|
self.color = color
|
|
|
|
self.containerNode = ASDisplayNode()
|
|
self.containerNode.isLayerBacked = true
|
|
self.containerNode.clipsToBounds = true
|
|
self.containerNode.cornerRadius = lineHeight / 2.0
|
|
|
|
self.foregroundNode = ASImageNode()
|
|
self.foregroundNode.isLayerBacked = true
|
|
self.foregroundNode.displayWithoutProcessing = true
|
|
self.foregroundNode.displaysAsynchronously = false
|
|
self.foregroundNode.image = generateStretchableFilledCircleImage(diameter: lineHeight, color: color)
|
|
|
|
super.init()
|
|
|
|
self.containerNode.addSubnode(self.foregroundNode)
|
|
self.addSubnode(self.containerNode)
|
|
}
|
|
|
|
func updateStatus(_ ranges: RangeSet<Int64>, _ size: Int64) {
|
|
self.ranges = (ranges, size)
|
|
if !self.bounds.width.isZero {
|
|
self.updateLayout(size: self.bounds.size, transition: .animated(duration: 0.15, curve: .easeInOut))
|
|
}
|
|
}
|
|
|
|
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
|
|
transition.updateFrame(node: self.foregroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)))
|
|
if let ranges = self.ranges, !ranges.0.isEmpty, ranges.1 != 0 {
|
|
for range in ranges.0.ranges {
|
|
let rangeWidth = min(size.width, (CGFloat(range.count) / CGFloat(ranges.1)) * size.width)
|
|
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: rangeWidth, height: size.height)))
|
|
transition.updateAlpha(node: self.foregroundNode, alpha: abs(size.width - rangeWidth) < 1.0 ? 0.0 : 1.0)
|
|
break
|
|
}
|
|
} else {
|
|
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: 0.0, height: size.height)))
|
|
transition.updateAlpha(node: self.foregroundNode, alpha: 0.0)
|
|
}
|
|
}
|
|
}
|
|
|
|
public final class MediaPlayerScrubbingNode: ASDisplayNode {
|
|
private var contentNodes: MediaPlayerScrubbingNodeContentNodes
|
|
|
|
private var displayLink: SharedDisplayLinkDriver.Link?
|
|
private var isInHierarchyValue: Bool = false
|
|
|
|
private var playbackStatusValue: MediaPlayerPlaybackStatus?
|
|
private var scrubbingBeginTimestamp: Double?
|
|
private var scrubbingTimestampValue: Double?
|
|
|
|
public var playbackStatusUpdated: ((MediaPlayerPlaybackStatus?) -> Void)?
|
|
public var playerStatusUpdated: ((MediaPlayerStatus?) -> Void)?
|
|
public var seek: ((Double) -> Void)?
|
|
public var update: ((Double?, CGFloat) -> Void)?
|
|
|
|
private let _scrubbingTimestamp = Promise<Double?>(nil)
|
|
public var scrubbingTimestamp: Signal<Double?, NoError> {
|
|
return self._scrubbingTimestamp.get()
|
|
}
|
|
|
|
private let _scrubbingPosition = Promise<Double?>(nil)
|
|
public var scrubbingPosition: Signal<Double?, NoError> {
|
|
return self._scrubbingPosition.get()
|
|
}
|
|
|
|
public var isScrubbing: Bool {
|
|
return self.scrubbingTimestampValue != nil
|
|
}
|
|
|
|
public var ignoreSeekId: Int?
|
|
|
|
public var enableScrubbing: Bool = true {
|
|
didSet {
|
|
switch self.contentNodes {
|
|
case let .standard(node):
|
|
node.handleNodeContainer?.isUserInteractionEnabled = self.enableScrubbing
|
|
case let .custom(node):
|
|
node.handleNodeContainer?.isUserInteractionEnabled = self.enableScrubbing
|
|
}
|
|
}
|
|
}
|
|
|
|
public var enableFineScrubbing: Bool = false {
|
|
didSet {
|
|
switch self.contentNodes {
|
|
case let .standard(node):
|
|
node.handleNodeContainer?.verticalPanEnabled = self.enableFineScrubbing
|
|
case let .custom(node):
|
|
node.handleNodeContainer?.verticalPanEnabled = self.enableFineScrubbing
|
|
}
|
|
}
|
|
}
|
|
|
|
public var containerNode: ASDisplayNode {
|
|
switch self.contentNodes {
|
|
case let .standard(node):
|
|
return node.containerNode
|
|
case let .custom(node):
|
|
return node.backgroundNode
|
|
}
|
|
}
|
|
|
|
private var _statusValue: MediaPlayerStatus?
|
|
private var statusValue: MediaPlayerStatus? {
|
|
get {
|
|
return self._statusValue
|
|
} set(value) {
|
|
if value != self._statusValue {
|
|
if let value = value, value.seekId == self.ignoreSeekId {
|
|
} else {
|
|
let previousStatusValue = self._statusValue
|
|
self._statusValue = value
|
|
self.updateProgressAnimations()
|
|
|
|
var playbackStatus = value?.status
|
|
if self.playbackStatusValue != playbackStatus {
|
|
self.playbackStatusValue = playbackStatus
|
|
if let playbackStatusUpdated = self.playbackStatusUpdated {
|
|
if playbackStatus == .paused, previousStatusValue?.status == .playing, let value = value, value.timestamp > value.duration - 0.1 {
|
|
playbackStatus = .playing
|
|
}
|
|
playbackStatusUpdated(playbackStatus)
|
|
}
|
|
}
|
|
|
|
self.playerStatusUpdated?(value)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private var statusDisposable: Disposable?
|
|
private var statusValuePromise = Promise<MediaPlayerStatus?>()
|
|
|
|
public var status: Signal<MediaPlayerStatus, NoError>? {
|
|
didSet {
|
|
if let status = self.status {
|
|
self.statusValuePromise.set(status |> map { $0 })
|
|
} else {
|
|
self.statusValuePromise.set(.single(nil))
|
|
}
|
|
}
|
|
}
|
|
|
|
private var bufferingStatusDisposable: Disposable?
|
|
private var bufferingStatusValuePromise = Promise<(RangeSet<Int64>, Int64)?>()
|
|
|
|
public var bufferingStatus: Signal<(RangeSet<Int64>, Int64)?, NoError>? {
|
|
didSet {
|
|
if let bufferingStatus = self.bufferingStatus {
|
|
self.bufferingStatusValuePromise.set(bufferingStatus)
|
|
} else {
|
|
self.bufferingStatusValuePromise.set(.single(nil))
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func contentNodesFromContent(_ content: MediaPlayerScrubbingNodeContent, enableScrubbing: Bool) -> MediaPlayerScrubbingNodeContentNodes {
|
|
switch content {
|
|
case let .standard(lineHeight, lineCap, scrubberHandle, backgroundColor, foregroundColor, bufferingColor, chapters):
|
|
let backgroundNode = ASImageNode()
|
|
backgroundNode.isLayerBacked = true
|
|
backgroundNode.displaysAsynchronously = false
|
|
backgroundNode.displayWithoutProcessing = true
|
|
|
|
let bufferingNode = MediaPlayerScrubbingBufferingNode(color: bufferingColor, lineCap: lineCap, lineHeight: lineHeight)
|
|
|
|
let foregroundContentNode = ASImageNode()
|
|
foregroundContentNode.isLayerBacked = true
|
|
foregroundContentNode.displaysAsynchronously = false
|
|
foregroundContentNode.displayWithoutProcessing = true
|
|
|
|
switch lineCap {
|
|
case .round:
|
|
backgroundNode.image = generateStretchableFilledCircleImage(diameter: lineHeight, color: backgroundColor)
|
|
foregroundContentNode.image = generateStretchableFilledCircleImage(diameter: lineHeight, color: foregroundColor)
|
|
case .square:
|
|
backgroundNode.backgroundColor = backgroundColor
|
|
foregroundContentNode.backgroundColor = foregroundColor
|
|
}
|
|
|
|
let foregroundNode = MediaPlayerScrubbingForegroundNode()
|
|
foregroundNode.isLayerBacked = true
|
|
foregroundNode.clipsToBounds = true
|
|
|
|
var handleNodeImpl: ASImageNode?
|
|
var highlightedHandleNodeImpl: ASImageNode?
|
|
var handleNodeContainerImpl: MediaPlayerScrubbingNodeButton?
|
|
|
|
switch scrubberHandle {
|
|
case .none:
|
|
break
|
|
case .line:
|
|
let handleNode = ASImageNode()
|
|
handleNode.image = generateHandleBackground(color: foregroundColor)
|
|
handleNode.isLayerBacked = true
|
|
handleNodeImpl = handleNode
|
|
|
|
let handleNodeContainer = MediaPlayerScrubbingNodeButton()
|
|
handleNodeContainer.addSubnode(handleNode)
|
|
handleNodeContainerImpl = handleNodeContainer
|
|
case .circle:
|
|
let handleNode = ASImageNode()
|
|
handleNode.image = generateFilledCircleImage(diameter: lineHeight + 3.0, color: foregroundColor)
|
|
handleNode.isLayerBacked = true
|
|
handleNodeImpl = handleNode
|
|
|
|
let highlightedHandleNode = ASImageNode()
|
|
let highlightedHandleImage = generateFilledCircleImage(diameter: lineHeight + 3.0 + 20.0, color: foregroundColor)!
|
|
highlightedHandleNode.image = highlightedHandleImage
|
|
highlightedHandleNode.bounds = CGRect(origin: CGPoint(), size: highlightedHandleImage.size)
|
|
highlightedHandleNode.isLayerBacked = true
|
|
highlightedHandleNode.transform = CATransform3DMakeScale(0.1875, 0.1875, 1.0)
|
|
highlightedHandleNodeImpl = highlightedHandleNode
|
|
|
|
let handleNodeContainer = MediaPlayerScrubbingNodeButton()
|
|
handleNodeContainer.addSubnode(handleNode)
|
|
handleNodeContainer.addSubnode(highlightedHandleNode)
|
|
handleNodeContainerImpl = handleNodeContainer
|
|
}
|
|
|
|
handleNodeContainerImpl?.isUserInteractionEnabled = enableScrubbing
|
|
|
|
var chapterNodesContainerImpl: ASDisplayNode?
|
|
var chapterNodes: [(MediaPlayerScrubbingChapter, ASDisplayNode)] = []
|
|
|
|
if !chapters.isEmpty {
|
|
let chapterNodesContainer = ASDisplayNode()
|
|
chapterNodesContainer.isUserInteractionEnabled = false
|
|
chapterNodesContainerImpl = chapterNodesContainer
|
|
|
|
var chapters = chapters
|
|
if let firstChapter = chapters.first, firstChapter.start > 0.0 {
|
|
chapters.insert(MediaPlayerScrubbingChapter(title: "", start: 0.0), at: 0)
|
|
}
|
|
|
|
for i in 0 ..< chapters.count {
|
|
let chapterNode = ASDisplayNode()
|
|
chapterNode.backgroundColor = .white
|
|
chapterNodesContainer.addSubnode(chapterNode)
|
|
|
|
chapterNodes.append((chapters[i], chapterNode))
|
|
}
|
|
}
|
|
|
|
return .standard(StandardMediaPlayerScrubbingNodeContentNode(lineHeight: lineHeight, lineCap: lineCap, containerNode: ASDisplayNode(), backgroundNode: backgroundNode, bufferingNode: bufferingNode, foregroundContentNode: foregroundContentNode, foregroundNode: foregroundNode, chapterNodesContainer: chapterNodesContainerImpl, chapterNodes: chapterNodes, handle: scrubberHandle, handleNode: handleNodeImpl, highlightedHandleNode: highlightedHandleNodeImpl, handleNodeContainer: handleNodeContainerImpl))
|
|
case let .custom(backgroundNode, foregroundContentNode):
|
|
let foregroundNode = MediaPlayerScrubbingForegroundNode()
|
|
foregroundNode.isLayerBacked = true
|
|
foregroundNode.clipsToBounds = true
|
|
|
|
let handleNodeContainer = MediaPlayerScrubbingNodeButton()
|
|
handleNodeContainer.isUserInteractionEnabled = enableScrubbing
|
|
|
|
return .custom(CustomMediaPlayerScrubbingNodeContentNode(backgroundNode: backgroundNode, foregroundContentNode: foregroundContentNode, foregroundNode: foregroundNode, handleNodeContainer: handleNodeContainer))
|
|
}
|
|
}
|
|
|
|
public init(content: MediaPlayerScrubbingNodeContent) {
|
|
self.contentNodes = MediaPlayerScrubbingNode.contentNodesFromContent(content, enableScrubbing: self.enableScrubbing)
|
|
|
|
super.init()
|
|
|
|
self.setupContentNodes()
|
|
|
|
self.statusDisposable = (self.statusValuePromise.get()
|
|
|> deliverOnMainQueue).start(next: { [weak self] status in
|
|
if let strongSelf = self {
|
|
strongSelf.statusValue = status
|
|
}
|
|
})
|
|
|
|
self.bufferingStatusDisposable = (self.bufferingStatusValuePromise.get()
|
|
|> deliverOnMainQueue).start(next: { [weak self] status in
|
|
if let strongSelf = self {
|
|
switch strongSelf.contentNodes {
|
|
case let .standard(node):
|
|
if let status = status {
|
|
node.bufferingNode.updateStatus(status.0, status.1)
|
|
}
|
|
case .custom:
|
|
break
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
deinit {
|
|
self.displayLink?.invalidate()
|
|
self.statusDisposable?.dispose()
|
|
self.bufferingStatusDisposable?.dispose()
|
|
}
|
|
|
|
private func setupContentNodes() {
|
|
if let subnodes = self.subnodes {
|
|
for subnode in subnodes {
|
|
subnode.removeFromSupernode()
|
|
}
|
|
}
|
|
|
|
switch self.contentNodes {
|
|
case let .standard(node):
|
|
self.addSubnode(node.containerNode)
|
|
|
|
node.containerNode.addSubnode(node.backgroundNode)
|
|
node.containerNode.addSubnode(node.bufferingNode)
|
|
node.foregroundNode.addSubnode(node.foregroundContentNode)
|
|
node.containerNode.addSubnode(node.foregroundNode)
|
|
|
|
let highlightedHandleNode = node.highlightedHandleNode
|
|
|
|
if let handleNodeContainer = node.handleNodeContainer {
|
|
self.addSubnode(handleNodeContainer)
|
|
handleNodeContainer.highlighted = { [weak self] highlighted in
|
|
if let strongSelf = self, let highlightedHandleNode, let statusValue = strongSelf.statusValue, Double(0.0).isLess(than: statusValue.duration) {
|
|
if highlighted {
|
|
strongSelf.displayLink?.isPaused = true
|
|
|
|
var timestamp = statusValue.timestamp
|
|
if statusValue.generationTimestamp > 0 && statusValue.status == .playing {
|
|
let currentTimestamp = CACurrentMediaTime()
|
|
timestamp = timestamp + (currentTimestamp - statusValue.generationTimestamp) * statusValue.baseRate
|
|
}
|
|
strongSelf.scrubbingTimestampValue = timestamp
|
|
strongSelf._scrubbingTimestamp.set(.single(strongSelf.scrubbingTimestampValue))
|
|
strongSelf._scrubbingPosition.set(.single(strongSelf.scrubbingTimestampValue.flatMap { $0 / statusValue.duration }))
|
|
|
|
highlightedHandleNode.layer.animateSpring(from: 0.1875 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.65, initialVelocity: 0.0, damping: 80.0, removeOnCompletion: false)
|
|
} else {
|
|
strongSelf.scrubbingTimestampValue = nil
|
|
strongSelf._scrubbingTimestamp.set(.single(nil))
|
|
strongSelf._scrubbingPosition.set(.single(nil))
|
|
strongSelf.updateProgressAnimations()
|
|
|
|
highlightedHandleNode.layer.animateSpring(from: 1.0 as NSNumber, to: 0.1875 as NSNumber, keyPath: "transform.scale", duration: 0.65, initialVelocity: 0.0, damping: 120.0, removeOnCompletion: false)
|
|
}
|
|
}
|
|
}
|
|
handleNodeContainer.beginScrubbing = { [weak self] in
|
|
if let strongSelf = self {
|
|
if let statusValue = strongSelf.statusValue, Double(0.0).isLess(than: statusValue.duration) {
|
|
strongSelf.scrubbingBeginTimestamp = statusValue.timestamp
|
|
strongSelf.scrubbingTimestampValue = statusValue.timestamp
|
|
strongSelf._scrubbingTimestamp.set(.single(strongSelf.scrubbingTimestampValue))
|
|
strongSelf._scrubbingPosition.set(.single(strongSelf.scrubbingTimestampValue.flatMap { $0 / statusValue.duration }))
|
|
strongSelf.update?(strongSelf.scrubbingTimestampValue, CGFloat(statusValue.timestamp / statusValue.duration))
|
|
strongSelf.updateProgressAnimations()
|
|
}
|
|
}
|
|
}
|
|
handleNodeContainer.updateScrubbing = { [weak self] addedFraction, multiplier in
|
|
if let strongSelf = self {
|
|
if let statusValue = strongSelf.statusValue, let scrubbingBeginTimestamp = strongSelf.scrubbingBeginTimestamp, Double(0.0).isLess(than: statusValue.duration) {
|
|
let delta: Double = (statusValue.duration * Double(addedFraction)) * multiplier
|
|
let timestampValue = max(0.0, min(statusValue.duration, scrubbingBeginTimestamp + delta))
|
|
strongSelf.scrubbingTimestampValue = timestampValue
|
|
strongSelf._scrubbingTimestamp.set(.single(strongSelf.scrubbingTimestampValue))
|
|
strongSelf._scrubbingPosition.set(.single(strongSelf.scrubbingTimestampValue.flatMap { $0 / statusValue.duration }))
|
|
strongSelf.update?(timestampValue, CGFloat(timestampValue / statusValue.duration))
|
|
strongSelf.updateProgressAnimations()
|
|
}
|
|
}
|
|
}
|
|
handleNodeContainer.updateMultiplier = { [weak self] multiplier in
|
|
if let strongSelf = self {
|
|
if let statusValue = strongSelf.statusValue, let _ = strongSelf.scrubbingBeginTimestamp, Double(0.0).isLess(than: statusValue.duration) {
|
|
strongSelf.scrubbingBeginTimestamp = strongSelf.scrubbingTimestampValue
|
|
}
|
|
}
|
|
}
|
|
handleNodeContainer.endScrubbing = { [weak self] apply in
|
|
if let strongSelf = self {
|
|
strongSelf.scrubbingBeginTimestamp = nil
|
|
let scrubbingTimestampValue = strongSelf.scrubbingTimestampValue
|
|
Queue.mainQueue().after(0.05, {
|
|
strongSelf._scrubbingTimestamp.set(.single(nil))
|
|
strongSelf._scrubbingPosition.set(.single(nil))
|
|
strongSelf.scrubbingTimestampValue = nil
|
|
})
|
|
if let scrubbingTimestampValue = scrubbingTimestampValue, apply {
|
|
if let statusValue = strongSelf.statusValue {
|
|
switch statusValue.status {
|
|
case .buffering:
|
|
break
|
|
default:
|
|
strongSelf.ignoreSeekId = statusValue.seekId
|
|
}
|
|
}
|
|
strongSelf.seek?(scrubbingTimestampValue)
|
|
}
|
|
strongSelf.update?(nil, 0.0)
|
|
strongSelf.updateProgressAnimations()
|
|
}
|
|
}
|
|
}
|
|
|
|
node.foregroundNode.onEnterHierarchy = { [weak self] in
|
|
self?.isInHierarchyValue = true
|
|
self?.updateProgressAnimations()
|
|
}
|
|
node.foregroundNode.onExitHierarchy = { [weak self] in
|
|
self?.isInHierarchyValue = false
|
|
self?.updateProgressAnimations()
|
|
}
|
|
case let .custom(node):
|
|
self.addSubnode(node.backgroundNode)
|
|
node.foregroundNode.addSubnode(node.foregroundContentNode)
|
|
self.addSubnode(node.foregroundNode)
|
|
|
|
if let handleNodeContainer = node.handleNodeContainer {
|
|
self.addSubnode(handleNodeContainer)
|
|
handleNodeContainer.beginScrubbing = { [weak self] in
|
|
if let strongSelf = self {
|
|
if let statusValue = strongSelf.statusValue, Double(0.0).isLess(than: statusValue.duration) {
|
|
strongSelf.scrubbingBeginTimestamp = statusValue.timestamp
|
|
strongSelf.scrubbingTimestampValue = statusValue.timestamp
|
|
strongSelf.updateProgressAnimations()
|
|
}
|
|
}
|
|
}
|
|
handleNodeContainer.updateScrubbing = { [weak self] addedFraction, multiplier in
|
|
if let strongSelf = self {
|
|
if let statusValue = strongSelf.statusValue, let scrubbingBeginTimestamp = strongSelf.scrubbingBeginTimestamp, Double(0.0).isLess(than: statusValue.duration) {
|
|
strongSelf.scrubbingTimestampValue = scrubbingBeginTimestamp + (statusValue.duration * Double(addedFraction)) * multiplier
|
|
strongSelf.updateProgressAnimations()
|
|
}
|
|
}
|
|
}
|
|
handleNodeContainer.updateMultiplier = { [weak self] multiplier in
|
|
if let strongSelf = self {
|
|
if let statusValue = strongSelf.statusValue, let _ = strongSelf.scrubbingBeginTimestamp, Double(0.0).isLess(than: statusValue.duration) {
|
|
strongSelf.scrubbingBeginTimestamp = strongSelf.scrubbingTimestampValue
|
|
}
|
|
}
|
|
}
|
|
handleNodeContainer.endScrubbing = { [weak self] apply in
|
|
if let strongSelf = self {
|
|
strongSelf.scrubbingBeginTimestamp = nil
|
|
let scrubbingTimestampValue = strongSelf.scrubbingTimestampValue
|
|
strongSelf.scrubbingTimestampValue = nil
|
|
if let scrubbingTimestampValue = scrubbingTimestampValue, apply {
|
|
strongSelf.seek?(scrubbingTimestampValue)
|
|
}
|
|
strongSelf.updateProgressAnimations()
|
|
}
|
|
}
|
|
}
|
|
|
|
node.foregroundNode.onEnterHierarchy = { [weak self] in
|
|
self?.isInHierarchyValue = true
|
|
self?.updateProgressAnimations()
|
|
}
|
|
node.foregroundNode.onExitHierarchy = { [weak self] in
|
|
self?.isInHierarchyValue = false
|
|
self?.updateProgressAnimations()
|
|
}
|
|
}
|
|
}
|
|
|
|
public func updateContent(_ content: MediaPlayerScrubbingNodeContent) {
|
|
self.contentNodes = MediaPlayerScrubbingNode.contentNodesFromContent(content, enableScrubbing: self.enableScrubbing)
|
|
|
|
self.setupContentNodes()
|
|
|
|
self.updateProgressAnimations()
|
|
}
|
|
|
|
private var isCollapsed = false
|
|
public func setCollapsed(_ collapsed: Bool, animated: Bool) {
|
|
self.isCollapsed = collapsed
|
|
|
|
let alpha: CGFloat = collapsed ? 0.0 : 1.0
|
|
let backgroundScale: CGFloat = collapsed ? 0.4 : 1.0
|
|
let handleScale: CGFloat = collapsed ? 0.2 : 1.0
|
|
switch self.contentNodes {
|
|
case let .standard(node):
|
|
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate
|
|
node.foregroundContentNode.backgroundColor = collapsed ? .white : nil
|
|
|
|
if let handleNode = node.handleNodeContainer {
|
|
transition.updateAlpha(node: node.foregroundContentNode, alpha: collapsed ? 0.45 : 1.0)
|
|
transition.updateAlpha(node: handleNode, alpha: collapsed ? 0.0 : 1.0)
|
|
transition.updateTransformScale(node: handleNode, scale: CGPoint(x: 1.0, y: handleScale))
|
|
}
|
|
transition.updateAlpha(node: node.bufferingNode, alpha: alpha)
|
|
transition.updateAlpha(node: node.backgroundNode, alpha: alpha)
|
|
transition.updateTransformScale(node: node.foregroundContentNode, scale: CGPoint(x: 1.0, y: backgroundScale))
|
|
transition.updateTransformScale(node: node.backgroundNode, scale: CGPoint(x: 1.0, y: backgroundScale))
|
|
case .custom:
|
|
break
|
|
}
|
|
}
|
|
|
|
override public var frame: CGRect {
|
|
didSet {
|
|
if self.frame.size != oldValue.size {
|
|
self.updateProgressAnimations()
|
|
}
|
|
}
|
|
}
|
|
|
|
public func update(size: CGSize, animator: ControlledTransitionAnimator) {
|
|
self.updateProgressAnimations(animator: animator)
|
|
}
|
|
|
|
public func updateColors(backgroundColor: UIColor, foregroundColor: UIColor) {
|
|
switch self.contentNodes {
|
|
case let .standard(node):
|
|
switch node.lineCap {
|
|
case .round:
|
|
node.backgroundNode.image = generateStretchableFilledCircleImage(diameter: node.lineHeight, color: backgroundColor)
|
|
node.foregroundContentNode.image = generateStretchableFilledCircleImage(diameter: node.lineHeight, color: foregroundColor)
|
|
case .square:
|
|
node.backgroundNode.backgroundColor = backgroundColor
|
|
node.foregroundContentNode.backgroundColor = foregroundColor
|
|
}
|
|
if let handleNode = node.handleNode as? ASImageNode {
|
|
switch node.handle {
|
|
case .line:
|
|
handleNode.image = generateHandleBackground(color: foregroundColor)
|
|
case .circle:
|
|
handleNode.image = generateFilledCircleImage(diameter: node.lineHeight + 3.0, color: foregroundColor)
|
|
case .none:
|
|
break
|
|
}
|
|
}
|
|
case .custom:
|
|
break
|
|
}
|
|
}
|
|
|
|
private func updateProgressAnimations(animator: ControlledTransitionAnimator? = nil) {
|
|
self.updateProgress(animator: animator)
|
|
|
|
let needsAnimation: Bool
|
|
|
|
if !self.isInHierarchyValue {
|
|
needsAnimation = false
|
|
} else if let _ = self.scrubbingTimestampValue {
|
|
needsAnimation = false
|
|
} else if let statusValue = self.statusValue {
|
|
switch statusValue.status {
|
|
case .buffering:
|
|
needsAnimation = false
|
|
case .paused:
|
|
needsAnimation = false
|
|
case .playing:
|
|
needsAnimation = true
|
|
}
|
|
} else {
|
|
needsAnimation = false
|
|
}
|
|
|
|
if needsAnimation {
|
|
if self.displayLink == nil {
|
|
let displayLink = SharedDisplayLinkDriver.shared.add { [weak self] _ in
|
|
self?.updateProgress()
|
|
}
|
|
self.displayLink = displayLink
|
|
}
|
|
self.displayLink?.isPaused = false
|
|
} else {
|
|
self.displayLink?.isPaused = true
|
|
}
|
|
}
|
|
|
|
private var animateToValue: Double?
|
|
private var animating = false
|
|
public func animateTo(_ timestamp: Double) {
|
|
self.animateToValue = timestamp
|
|
UIView.animate(withDuration: 0.2, delay: 0.0, options: [.curveEaseInOut, .allowAnimatedContent, .layoutSubviews], animations: {
|
|
self.updateProgress()
|
|
}, completion: { _ in
|
|
self.animateToValue = nil
|
|
self.animating = false
|
|
})
|
|
}
|
|
|
|
private func updateProgress(animator: ControlledTransitionAnimator? = nil) {
|
|
let bounds = self.bounds
|
|
|
|
var isPlaying = false
|
|
var timestampAndDuration: (timestamp: Double?, duration: Double)?
|
|
if let statusValue = self.statusValue {
|
|
switch statusValue.status {
|
|
case .playing:
|
|
isPlaying = true
|
|
default:
|
|
break
|
|
}
|
|
if case .buffering(true, _, _, _) = statusValue.status {
|
|
timestampAndDuration = (nil, statusValue.duration)
|
|
//initialBuffering = true
|
|
} else if Double(0.0).isLess(than: statusValue.duration) {
|
|
if let scrubbingTimestampValue = self.scrubbingTimestampValue {
|
|
timestampAndDuration = (max(0.0, min(scrubbingTimestampValue, statusValue.duration)), statusValue.duration)
|
|
} else {
|
|
timestampAndDuration = (statusValue.timestamp, statusValue.duration)
|
|
}
|
|
}
|
|
}
|
|
|
|
if let animateToValue = self.animateToValue {
|
|
if self.animating {
|
|
return
|
|
} else if let (_, duration) = timestampAndDuration {
|
|
self.animating = true
|
|
timestampAndDuration = (animateToValue, duration)
|
|
}
|
|
}
|
|
|
|
switch self.contentNodes {
|
|
case let .standard(node):
|
|
node.containerNode.frame = CGRect(origin: CGPoint(), size: bounds.size)
|
|
|
|
let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: floor((bounds.size.height - node.lineHeight) / 2.0)), size: CGSize(width: bounds.size.width, height: node.lineHeight))
|
|
let foregroundContentFrame = CGRect(origin: CGPoint(), size: CGSize(width: backgroundFrame.size.width, height: backgroundFrame.size.height))
|
|
|
|
node.backgroundNode.position = CGPoint(x: backgroundFrame.midX, y: backgroundFrame.midY)
|
|
node.backgroundNode.bounds = CGRect(origin: CGPoint(), size: backgroundFrame.size)
|
|
|
|
node.foregroundContentNode.position = CGPoint(x: foregroundContentFrame.midX, y: foregroundContentFrame.midY)
|
|
node.foregroundContentNode.bounds = CGRect(origin: CGPoint(), size: foregroundContentFrame.size)
|
|
|
|
node.bufferingNode.frame = backgroundFrame
|
|
node.bufferingNode.updateLayout(size: backgroundFrame.size, transition: .immediate)
|
|
|
|
if let chapterNodesContainer = node.chapterNodesContainer {
|
|
if let duration = timestampAndDuration?.duration, duration > 1.0, backgroundFrame.width > 0.0, node.chapterNodes.count > 1 {
|
|
if node.containerNode.view.mask == nil {
|
|
node.containerNode.view.mask = chapterNodesContainer.view
|
|
|
|
let transitionView = UIView()
|
|
transitionView.backgroundColor = .white
|
|
transitionView.frame = node.containerNode.bounds
|
|
chapterNodesContainer.view.addSubview(transitionView)
|
|
transitionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak transitionView] _ in
|
|
transitionView?.removeFromSuperview()
|
|
})
|
|
}
|
|
|
|
chapterNodesContainer.frame = backgroundFrame
|
|
|
|
var addedBeginning = false
|
|
for i in 1 ..< node.chapterNodes.count {
|
|
let (previousChapter, previousChapterNode) = node.chapterNodes[i - 1]
|
|
let (chapter, chapterNode) = node.chapterNodes[i]
|
|
|
|
let lineWidth: CGFloat = 1.0 + UIScreenPixel * 2.0
|
|
let startPosition: CGFloat
|
|
if !addedBeginning {
|
|
startPosition = 0.0
|
|
} else {
|
|
startPosition = floor(backgroundFrame.width * CGFloat(previousChapter.start / duration)) + lineWidth / 2.0
|
|
}
|
|
let endPosition: CGFloat = max(startPosition, floor(backgroundFrame.width * CGFloat(chapter.start / duration)) - lineWidth / 2.0)
|
|
let width = endPosition - startPosition
|
|
if width < lineWidth * 0.5 {
|
|
previousChapterNode.frame = CGRect()
|
|
continue
|
|
}
|
|
previousChapterNode.frame = CGRect(x: startPosition, y: 0.0, width: endPosition - startPosition, height: backgroundFrame.size.height)
|
|
|
|
addedBeginning = true
|
|
|
|
if i == node.chapterNodes.count - 1 {
|
|
let startPosition = endPosition + lineWidth
|
|
chapterNode.frame = CGRect(x: startPosition, y: 0.0, width: backgroundFrame.size.width - startPosition, height: backgroundFrame.size.height)
|
|
}
|
|
}
|
|
} else {
|
|
node.containerNode.view.mask = nil
|
|
}
|
|
}
|
|
|
|
if let handleNode = node.handleNode {
|
|
var handleSize: CGSize = CGSize(width: 2.0, height: bounds.size.height)
|
|
var handleOffset: CGFloat = 0.0
|
|
if case .circle = node.handle, let handleNode = handleNode as? ASImageNode, let image = handleNode.image {
|
|
handleSize = image.size
|
|
handleOffset = -1.0 + UIScreenPixel
|
|
}
|
|
handleNode.frame = CGRect(origin: CGPoint(x: -handleSize.width / 2.0, y: floor((bounds.size.height - handleSize.height) / 2.0) + handleOffset), size: handleSize)
|
|
|
|
if let highlightedHandleNode = node.highlightedHandleNode {
|
|
highlightedHandleNode.position = handleNode.position
|
|
}
|
|
}
|
|
|
|
if let handleNodeContainer = node.handleNodeContainer {
|
|
handleNodeContainer.frame = bounds
|
|
}
|
|
|
|
if let (maybeTimestamp, duration) = timestampAndDuration, let timestamp = maybeTimestamp, duration > 0.01 {
|
|
if let scrubbingTimestampValue = self.scrubbingTimestampValue {
|
|
var progress = CGFloat(scrubbingTimestampValue / duration)
|
|
if progress.isNaN || !progress.isFinite {
|
|
progress = 0.0
|
|
}
|
|
progress = min(1.0, progress)
|
|
node.foregroundNode.frame = CGRect(origin: backgroundFrame.origin, size: CGSize(width: floorToScreenPixels(progress * backgroundFrame.size.width), height: backgroundFrame.size.height))
|
|
|
|
if let handleNodeContainer = node.handleNodeContainer {
|
|
handleNodeContainer.bounds = bounds.offsetBy(dx: -floorToScreenPixels(bounds.size.width * progress), dy: 0.0)
|
|
if !self.isCollapsed {
|
|
if handleNodeContainer.alpha.isZero {
|
|
handleNodeContainer.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
handleNodeContainer.alpha = 1.0
|
|
}
|
|
}
|
|
} else if let statusValue = self.statusValue {
|
|
var actualTimestamp: Double
|
|
if statusValue.generationTimestamp.isZero || !isPlaying || self.animateToValue != nil {
|
|
actualTimestamp = timestamp
|
|
} else {
|
|
let currentTimestamp = CACurrentMediaTime()
|
|
actualTimestamp = timestamp + (currentTimestamp - statusValue.generationTimestamp) * statusValue.baseRate
|
|
}
|
|
|
|
var progress = CGFloat(actualTimestamp / duration)
|
|
if progress.isNaN || !progress.isFinite {
|
|
progress = 0.0
|
|
}
|
|
progress = min(1.0, progress)
|
|
|
|
let foregroundFrame = CGRect(origin: backgroundFrame.origin, size: CGSize(width: floorToScreenPixels(progress * backgroundFrame.size.width), height: backgroundFrame.size.height))
|
|
if let _ = self.animateToValue {
|
|
let previousFrame = node.foregroundNode.frame
|
|
node.foregroundNode.frame = foregroundFrame
|
|
node.foregroundNode.layer.animateFrame(from: previousFrame, to: foregroundFrame, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
|
|
} else {
|
|
node.foregroundNode.frame = foregroundFrame
|
|
}
|
|
|
|
if let handleNodeContainer = node.handleNodeContainer {
|
|
handleNodeContainer.bounds = bounds.offsetBy(dx: -floorToScreenPixels(bounds.size.width * progress), dy: 0.0)
|
|
if !self.isCollapsed {
|
|
if handleNodeContainer.alpha.isZero {
|
|
handleNodeContainer.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
handleNodeContainer.alpha = 1.0
|
|
}
|
|
}
|
|
} else {
|
|
node.foregroundNode.frame = CGRect(origin: backgroundFrame.origin, size: CGSize(width: 0.0, height: backgroundFrame.size.height))
|
|
node.handleNodeContainer?.alpha = 0.0
|
|
}
|
|
} else {
|
|
node.foregroundNode.frame = CGRect(origin: backgroundFrame.origin, size: CGSize(width: 0.0, height: backgroundFrame.size.height))
|
|
node.handleNodeContainer?.alpha = 0.0
|
|
}
|
|
case let .custom(node):
|
|
if let handleNodeContainer = node.handleNodeContainer {
|
|
handleNodeContainer.frame = bounds
|
|
}
|
|
|
|
let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: bounds.size.width, height: bounds.size.height))
|
|
|
|
if let animator = animator {
|
|
animator.updateFrame(layer: node.backgroundNode.layer, frame: backgroundFrame, completion: nil)
|
|
animator.updateFrame(layer: node.foregroundContentNode.layer, frame: CGRect(origin: CGPoint(), size: CGSize(width: backgroundFrame.size.width, height: backgroundFrame.size.height)), completion: nil)
|
|
} else {
|
|
node.backgroundNode.frame = backgroundFrame
|
|
node.foregroundContentNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: backgroundFrame.size.width, height: backgroundFrame.size.height))
|
|
}
|
|
|
|
let timestampAndDuration: (timestamp: Double, duration: Double)?
|
|
if let statusValue = self.statusValue, Double(0.0).isLess(than: statusValue.duration) {
|
|
if let scrubbingTimestampValue = self.scrubbingTimestampValue {
|
|
timestampAndDuration = (max(0.0, min(scrubbingTimestampValue, statusValue.duration)), statusValue.duration)
|
|
} else {
|
|
timestampAndDuration = (statusValue.timestamp, statusValue.duration)
|
|
}
|
|
} else {
|
|
timestampAndDuration = nil
|
|
}
|
|
|
|
if let (timestamp, duration) = timestampAndDuration {
|
|
if let scrubbingTimestampValue = self.scrubbingTimestampValue {
|
|
var progress = CGFloat(scrubbingTimestampValue / duration)
|
|
if progress.isNaN || !progress.isFinite {
|
|
progress = 0.0
|
|
}
|
|
progress = max(0.0, min(1.0, progress))
|
|
node.backgroundNode.progress = nil
|
|
node.foregroundContentNode.progress = nil
|
|
node.foregroundNode.frame = CGRect(origin: backgroundFrame.origin, size: CGSize(width: floorToScreenPixels(progress * backgroundFrame.size.width), height: backgroundFrame.size.height))
|
|
} else if let statusValue = self.statusValue {
|
|
let actualTimestamp: Double
|
|
if statusValue.generationTimestamp.isZero || !isPlaying {
|
|
actualTimestamp = timestamp
|
|
} else {
|
|
let currentTimestamp = CACurrentMediaTime()
|
|
actualTimestamp = timestamp + (currentTimestamp - statusValue.generationTimestamp) * statusValue.baseRate
|
|
}
|
|
var progress = CGFloat(actualTimestamp / duration)
|
|
if progress.isNaN || !progress.isFinite {
|
|
progress = 0.0
|
|
}
|
|
progress = max(0.0, min(1.0, progress))
|
|
node.backgroundNode.progress = progress
|
|
node.foregroundContentNode.progress = progress
|
|
node.foregroundNode.frame = CGRect(origin: backgroundFrame.origin, size: CGSize(width: floorToScreenPixels(progress * backgroundFrame.size.width), height: backgroundFrame.size.height))
|
|
} else {
|
|
node.foregroundNode.frame = CGRect(origin: backgroundFrame.origin, size: CGSize(width: 0.0, height: backgroundFrame.size.height))
|
|
}
|
|
} else {
|
|
node.foregroundNode.frame = CGRect(origin: backgroundFrame.origin, size: CGSize(width: 0.0, height: backgroundFrame.size.height))
|
|
}
|
|
}
|
|
}
|
|
|
|
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
switch self.contentNodes {
|
|
case let .standard(node):
|
|
if let handleNodeContainer = node.handleNodeContainer, handleNodeContainer.isUserInteractionEnabled, handleNodeContainer.frame.insetBy(dx: 0.0, dy: -16.0).contains(point) {
|
|
if let handleNode = node.handleNode, handleNode.convert(handleNode.bounds, to: self).insetBy(dx: -32.0, dy: -16.0).contains(point) {
|
|
return handleNodeContainer.view
|
|
} else {
|
|
return nil
|
|
}
|
|
} else {
|
|
return nil
|
|
}
|
|
case let .custom(node):
|
|
if let handleNodeContainer = node.handleNodeContainer, handleNodeContainer.isUserInteractionEnabled, handleNodeContainer.frame.insetBy(dx: 0.0, dy: -5.0).contains(point) {
|
|
return handleNodeContainer.view
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
}
|