From ec9cb0a09f8be0f75bfdf047584263be41c35f55 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sun, 11 Jun 2023 01:08:55 +0400 Subject: [PATCH] Camera and editor improvements --- .../Source/Base/CombinedComponent.swift | 32 +++++ .../Sources/DrawingEntitiesView.swift | 5 + .../DrawingUI/Sources/DrawingScreen.swift | 10 +- .../DrawingUI/Sources/DrawingView.swift | 2 +- .../MediaEditor/Sources/MediaEditor.swift | 12 +- .../Sources/MediaEditorDraft.swift | 7 +- .../Sources/MediaEditorScreen.swift | 12 +- .../Sources/VideoScrubberComponent.swift | 117 ++++++++++++++++-- 8 files changed, 173 insertions(+), 24 deletions(-) diff --git a/submodules/ComponentFlow/Source/Base/CombinedComponent.swift b/submodules/ComponentFlow/Source/Base/CombinedComponent.swift index af917c84f2..b4f27b849e 100644 --- a/submodules/ComponentFlow/Source/Base/CombinedComponent.swift +++ b/submodules/ComponentFlow/Source/Base/CombinedComponent.swift @@ -180,6 +180,7 @@ public final class _UpdatedChildComponent { var _opacity: CGFloat? var _cornerRadius: CGFloat? var _clipsToBounds: Bool? + var _shadow: Shadow? fileprivate var transitionAppear: Transition.Appear? fileprivate var transitionAppearWithGuide: (Transition.AppearWithGuide, _AnyChildComponent.Id)? @@ -260,6 +261,11 @@ public final class _UpdatedChildComponent { self._clipsToBounds = clipsToBounds return self } + + @discardableResult public func shadow(_ shadow: Shadow?) -> _UpdatedChildComponent { + self._shadow = shadow + return self + } @discardableResult public func gesture(_ gesture: Gesture) -> _UpdatedChildComponent { self.gestures.append(gesture) @@ -706,6 +712,16 @@ public extension CombinedComponent { updatedChild.view.alpha = updatedChild._opacity ?? 1.0 updatedChild.view.clipsToBounds = updatedChild._clipsToBounds ?? false updatedChild.view.layer.cornerRadius = updatedChild._cornerRadius ?? 0.0 + if let shadow = updatedChild._shadow { + updatedChild.view.layer.shadowColor = shadow.color.withAlphaComponent(1.0).cgColor + updatedChild.view.layer.shadowRadius = shadow.radius + updatedChild.view.layer.shadowOpacity = Float(shadow.color.alpha) + updatedChild.view.layer.shadowOffset = shadow.offset + } else { + updatedChild.view.layer.shadowColor = nil + updatedChild.view.layer.shadowRadius = 0.0 + updatedChild.view.layer.shadowOpacity = 0.0 + } updatedChild.view.context(typeErasedComponent: updatedChild.component).erasedState._updated = { [weak viewContext] transition in guard let viewContext = viewContext else { return @@ -834,3 +850,19 @@ public extension CombinedComponent { return ActionSlot() } } + +public struct Shadow { + public let color: UIColor + public let radius: CGFloat + public let offset: CGSize + + public init( + color: UIColor, + radius: CGFloat, + offset: CGSize + ) { + self.color = color + self.radius = radius + self.offset = offset + } +} diff --git a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift index 0086109d72..c7e28e21ee 100644 --- a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift +++ b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift @@ -178,6 +178,11 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { return entities } + public var hasEntities: Bool { + let entities = self.entities.filter { !($0 is DrawingMediaEntity) } + return !entities.isEmpty + } + private var initialEntitiesData: Data? public func setup(withEntitiesData entitiesData: Data?) { self.clear() diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index a79eac7699..8d66f547af 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -17,6 +17,7 @@ import ComponentDisplayAdapters import LottieAnimationComponent import ViewControllerComponent import BlurredBackgroundComponent +import MultilineTextComponent import ContextUI import ChatEntityKeyboardInputNode import EntityKeyboard @@ -1676,6 +1677,7 @@ private final class DrawingScreenComponent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0 + (isFilled != nil ? 46.0 : 0.0), y: environment.safeInsets.top + 31.0)) .appear(.default(scale: true)) .disappear(.default(scale: true)) + .shadow(component.sourceHint == .storyEditor ? Shadow(color: UIColor(rgb: 0x000000, alpha: 0.35), radius: 2.0, offset: .zero) : nil) ) } } @@ -1774,6 +1776,7 @@ private final class DrawingScreenComponent: CombinedComponent { .position(CGPoint(x: environment.safeInsets.left + undoButton.size.width / 2.0 + 2.0, y: topInset)) .scale(isEditingText ? 0.01 : 1.0) .opacity(isEditingText || !controlsAreVisible ? 0.0 : 1.0) + .shadow(component.sourceHint == .storyEditor ? Shadow(color: UIColor(rgb: 0x000000, alpha: 0.35), radius: 2.0, offset: .zero) : nil) ) @@ -1794,12 +1797,17 @@ private final class DrawingScreenComponent: CombinedComponent { .position(CGPoint(x: environment.safeInsets.left + undoButton.size.width + 2.0 + redoButton.size.width / 2.0, y: topInset)) .scale(state.drawingViewState.canRedo && !isEditingText ? 1.0 : 0.01) .opacity(state.drawingViewState.canRedo && !isEditingText && controlsAreVisible ? 1.0 : 0.0) + .shadow(component.sourceHint == .storyEditor ? Shadow(color: UIColor(rgb: 0x000000, alpha: 0.35), radius: 2.0, offset: .zero) : nil) ) let clearAllButton = clearAllButton.update( component: Button( content: AnyComponent( - Text(text: strings.Paint_Clear, font: Font.regular(17.0), color: .white) + MultilineTextComponent( + text: .plain(NSAttributedString(string: strings.Paint_Clear, font: Font.regular(17.0), textColor: .white)), + textShadowColor: component.sourceHint == .storyEditor ? UIColor(rgb: 0x000000, alpha: 0.35) : nil, + textShadowBlur: 2.0 + ) ), isEnabled: state.drawingViewState.canClear, action: { diff --git a/submodules/DrawingUI/Sources/DrawingView.swift b/submodules/DrawingUI/Sources/DrawingView.swift index 2fc79b5798..8411fe3359 100644 --- a/submodules/DrawingUI/Sources/DrawingView.swift +++ b/submodules/DrawingUI/Sources/DrawingView.swift @@ -895,7 +895,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, UIPencilInt self.stateUpdated(NavigationState( canUndo: !self.undoStack.isEmpty, canRedo: !self.redoStack.isEmpty, - canClear: !self.undoStack.isEmpty || self.hasOpaqueData || !(self.entitiesView?.entities.isEmpty ?? true), + canClear: !self.undoStack.isEmpty || self.hasOpaqueData || (self.entitiesView?.hasEntities ?? false), canZoomOut: self.zoomScale > 1.0 + .ulpOfOne, isDrawing: self.isDrawing )) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 3eb4bd197c..2da4d50cd0 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -11,9 +11,11 @@ import TelegramPresentationData import FastBlur public struct MediaEditorPlayerState { + public let generationTimestamp: Double public let duration: Double public let timeRange: Range? public let position: Double + public let isPlaying: Bool public let frames: [UIImage] public let framesCount: Int public let framesUpdateTimestamp: Double @@ -107,12 +109,12 @@ public final class MediaEditor { } private let playerPromise = Promise() - private var playerPlaybackState: (Double, Double, Bool) = (0.0, 0.0, false) { + private var playerPlaybackState: (Double, Double, Bool, Bool) = (0.0, 0.0, false, false) { didSet { self.playerPlaybackStatePromise.set(.single(self.playerPlaybackState)) } } - private let playerPlaybackStatePromise = Promise<(Double, Double, Bool)>((0.0, 0.0, false)) + private let playerPlaybackStatePromise = Promise<(Double, Double, Bool, Bool)>((0.0, 0.0, false, false)) public var onFirstDisplay: () -> Void = {} @@ -122,12 +124,14 @@ public final class MediaEditor { if let self, let asset = player?.currentItem?.asset { return combineLatest(self.valuesPromise.get(), self.playerPlaybackStatePromise.get(), self.videoFrames(asset: asset, count: framesCount)) |> map { values, durationAndPosition, framesAndUpdateTimestamp in - let (duration, position, hasAudio) = durationAndPosition + let (duration, position, isPlaying, hasAudio) = durationAndPosition let (frames, framesUpdateTimestamp) = framesAndUpdateTimestamp return MediaEditorPlayerState( + generationTimestamp: CACurrentMediaTime(), duration: duration, timeRange: values.videoTrimRange, position: position, + isPlaying: isPlaying, frames: frames, framesCount: framesCount, framesUpdateTimestamp: framesUpdateTimestamp, @@ -409,7 +413,7 @@ public final class MediaEditor { if let audioTracks = player.currentItem?.asset.tracks(withMediaType: .audio) { hasAudio = !audioTracks.isEmpty } - self.playerPlaybackState = (duration, time.seconds, hasAudio) + self.playerPlaybackState = (duration, time.seconds, player.rate > 0.0, hasAudio) } self.didPlayToEndTimeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player.currentItem, queue: nil, using: { [weak self] notification in if let self { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift index 62aa66ebef..bfd8c8e562 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift @@ -23,6 +23,8 @@ public final class MediaEditorDraft: Codable, Equatable { case dimensionsWidth case dimensionsHeight case values + case caption + case privacy } public let path: String @@ -30,14 +32,16 @@ public final class MediaEditorDraft: Codable, Equatable { public let thumbnail: UIImage public let dimensions: PixelDimensions public let values: MediaEditorValues + public let caption: NSAttributedString public let privacy: MediaEditorResultPrivacy? - public init(path: String, isVideo: Bool, thumbnail: UIImage, dimensions: PixelDimensions, values: MediaEditorValues, privacy: MediaEditorResultPrivacy?) { + public init(path: String, isVideo: Bool, thumbnail: UIImage, dimensions: PixelDimensions, values: MediaEditorValues, caption: NSAttributedString, privacy: MediaEditorResultPrivacy?) { self.path = path self.isVideo = isVideo self.thumbnail = thumbnail self.dimensions = dimensions self.values = values + self.caption = caption self.privacy = privacy } @@ -62,6 +66,7 @@ public final class MediaEditorDraft: Codable, Equatable { } else { fatalError() } + self.caption = NSAttributedString() self.privacy = nil } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index d5bd392617..5e72919954 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -647,11 +647,13 @@ final class MediaEditorScreenComponent: Component { transition: transition, component: AnyComponent(VideoScrubberComponent( context: component.context, + generationTimestamp: playerState.generationTimestamp, duration: playerState.duration, startPosition: playerState.timeRange?.lowerBound ?? 0.0, endPosition: playerState.timeRange?.upperBound ?? min(playerState.duration, storyMaxVideoDuration), position: playerState.position, maxDuration: storyMaxVideoDuration, + isPlaying: playerState.isPlaying, frames: playerState.frames, framesUpdateTimestamp: playerState.framesUpdateTimestamp, trimUpdated: { [weak mediaEditor] start, end, updatedEnd, done in @@ -893,7 +895,7 @@ final class MediaEditorScreenComponent: Component { if let saveButtonView = self.saveButton.view { if saveButtonView.superview == nil { saveButtonView.layer.shadowOffset = CGSize(width: 0.0, height: 0.0) - saveButtonView.layer.shadowRadius = 3.0 + saveButtonView.layer.shadowRadius = 2.0 saveButtonView.layer.shadowColor = UIColor.black.cgColor saveButtonView.layer.shadowOpacity = 0.35 self.addSubview(saveButtonView) @@ -941,7 +943,7 @@ final class MediaEditorScreenComponent: Component { if let muteButtonView = self.muteButton.view { if muteButtonView.superview == nil { muteButtonView.layer.shadowOffset = CGSize(width: 0.0, height: 0.0) - muteButtonView.layer.shadowRadius = 3.0 + muteButtonView.layer.shadowRadius = 2.0 muteButtonView.layer.shadowColor = UIColor.black.cgColor muteButtonView.layer.shadowOpacity = 0.35 self.addSubview(muteButtonView) @@ -979,7 +981,7 @@ final class MediaEditorScreenComponent: Component { if let settingsButtonView = self.settingsButton.view { if settingsButtonView.superview == nil { settingsButtonView.layer.shadowOffset = CGSize(width: 0.0, height: 0.0) - settingsButtonView.layer.shadowRadius = 3.0 + settingsButtonView.layer.shadowRadius = 2.0 settingsButtonView.layer.shadowColor = UIColor.black.cgColor settingsButtonView.layer.shadowOpacity = 0.35 //self.addSubview(settingsButtonView) @@ -2644,7 +2646,7 @@ public final class MediaEditorScreen: ViewController { let path = draftPath() + "/\(Int64.random(in: .min ... .max)).jpg" if let data = image.jpegData(compressionQuality: 0.87) { try? data.write(to: URL(fileURLWithPath: path)) - let draft = MediaEditorDraft(path: path, isVideo: false, thumbnail: thumbnailImage, dimensions: dimensions, values: values, privacy: privacy) + let draft = MediaEditorDraft(path: path, isVideo: false, thumbnail: thumbnailImage, dimensions: dimensions, values: values, caption: NSAttributedString(), privacy: privacy) if let id { saveStorySource(engine: self.context.engine, item: draft, id: id) } else { @@ -2658,7 +2660,7 @@ public final class MediaEditorScreen: ViewController { if let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) { let path = draftPath() + "/\(Int64.random(in: .min ... .max)).mp4" try? FileManager.default.moveItem(atPath: videoPath, toPath: path) - let draft = MediaEditorDraft(path: path, isVideo: true, thumbnail: thumbnailImage, dimensions: dimensions, values: values, privacy: privacy) + let draft = MediaEditorDraft(path: path, isVideo: true, thumbnail: thumbnailImage, dimensions: dimensions, values: values, caption: NSAttributedString(), privacy: privacy) if let id { saveStorySource(engine: self.context.engine, item: draft, id: id) } else { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift index c69cc6a380..8bab798564 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift @@ -41,11 +41,13 @@ final class VideoScrubberComponent: Component { typealias EnvironmentType = Empty let context: AccountContext + let generationTimestamp: Double let duration: Double let startPosition: Double let endPosition: Double let position: Double let maxDuration: Double + let isPlaying: Bool let frames: [UIImage] let framesUpdateTimestamp: Double let trimUpdated: (Double, Double, Bool, Bool) -> Void @@ -53,22 +55,26 @@ final class VideoScrubberComponent: Component { init( context: AccountContext, + generationTimestamp: Double, duration: Double, startPosition: Double, endPosition: Double, position: Double, maxDuration: Double, + isPlaying: Bool, frames: [UIImage], framesUpdateTimestamp: Double, trimUpdated: @escaping (Double, Double, Bool, Bool) -> Void, positionUpdated: @escaping (Double, Bool) -> Void ) { self.context = context + self.generationTimestamp = generationTimestamp self.duration = duration self.startPosition = startPosition self.endPosition = endPosition self.position = position self.maxDuration = maxDuration + self.isPlaying = isPlaying self.frames = frames self.framesUpdateTimestamp = framesUpdateTimestamp self.trimUpdated = trimUpdated @@ -79,6 +85,9 @@ final class VideoScrubberComponent: Component { if lhs.context !== rhs.context { return false } + if lhs.generationTimestamp != rhs.generationTimestamp { + return false + } if lhs.duration != rhs.duration { return false } @@ -94,6 +103,9 @@ final class VideoScrubberComponent: Component { if lhs.maxDuration != rhs.maxDuration { return false } + if lhs.isPlaying != rhs.isPlaying { + return false + } if lhs.framesUpdateTimestamp != rhs.framesUpdateTimestamp { return false } @@ -114,6 +126,13 @@ final class VideoScrubberComponent: Component { private var component: VideoScrubberComponent? private weak var state: EmptyComponentState? + private var scrubberSize: CGSize? + + private var isPanningTrimHandle = false + private var isPanningPositionHandle = false + + private var displayLink: SharedDisplayLinkDriver.Link? + private var positionAnimation: (start: Double, from: Double, to: Double)? override init(frame: CGRect) { super.init(frame: frame) @@ -134,17 +153,19 @@ final class VideoScrubberComponent: Component { context.addPath(innerPath.cgPath) context.fillPath() + context.setBlendMode(.clear) let holeSize = CGSize(width: 2.0, height: 11.0) let holePath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: 5.0 - UIScreenPixel, y: (size.height - holeSize.height) / 2.0), size: holeSize), cornerRadius: holeSize.width / 2.0) context.addPath(holePath.cgPath) context.fillPath() })?.withRenderingMode(.alwaysTemplate) - let positionImage = generateImage(CGSize(width: 2.0, height: 42.0), rotatedContext: { size, context in + let positionImage = generateImage(CGSize(width: handleWidth, height: 50.0), rotatedContext: { size, context in context.clear(CGRect(origin: .zero, size: size)) context.setFillColor(UIColor.white.cgColor) + context.setShadow(offset: .zero, blur: 2.0, color: UIColor(rgb: 0x000000, alpha: 0.55).cgColor) - let path = UIBezierPath(roundedRect: CGRect(origin: .zero, size: CGSize(width: 2.0, height: 42.0)), cornerRadius: 1.0) + let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: 6.0, y: 4.0), size: CGSize(width: 2.0, height: 42.0)), cornerRadius: 1.0) context.addPath(path.cgPath) context.fillPath() }) @@ -189,20 +210,28 @@ final class VideoScrubberComponent: Component { self.leftHandleView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handleLeftHandlePan(_:)))) self.rightHandleView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handleRightHandlePan(_:)))) self.cursorView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handlePositionHandlePan(_:)))) + + self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] in + self?.updateCursorPosition() + } + self.displayLink?.isPaused = true } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - private var isPanningHandle = false + deinit { + self.displayLink?.invalidate() + } + @objc private func handleLeftHandlePan(_ gestureRecognizer: UIPanGestureRecognizer) { guard let component = self.component else { return } let location = gestureRecognizer.location(in: self) let start = handleWidth / 2.0 - let end = self.frame.width - handleWidth + let end = self.frame.width - handleWidth / 2.0 let length = end - start let fraction = (location.x - start) / length @@ -219,13 +248,13 @@ final class VideoScrubberComponent: Component { var transition: Transition = .immediate switch gestureRecognizer.state { case .began, .changed: - self.isPanningHandle = true + self.isPanningTrimHandle = true component.trimUpdated(startValue, endValue, false, false) if case .began = gestureRecognizer.state { transition = .easeInOut(duration: 0.25) } case .ended, .cancelled: - self.isPanningHandle = false + self.isPanningTrimHandle = false component.trimUpdated(startValue, endValue, false, true) transition = .easeInOut(duration: 0.25) default: @@ -240,7 +269,7 @@ final class VideoScrubberComponent: Component { } let location = gestureRecognizer.location(in: self) let start = handleWidth / 2.0 - let end = self.frame.width - handleWidth + let end = self.frame.width - handleWidth / 2.0 let length = end - start let fraction = (location.x - start) / length @@ -257,13 +286,13 @@ final class VideoScrubberComponent: Component { var transition: Transition = .immediate switch gestureRecognizer.state { case .began, .changed: - self.isPanningHandle = true + self.isPanningTrimHandle = true component.trimUpdated(startValue, endValue, true, false) if case .began = gestureRecognizer.state { transition = .easeInOut(duration: 0.25) } case .ended, .cancelled: - self.isPanningHandle = false + self.isPanningTrimHandle = false component.trimUpdated(startValue, endValue, true, true) transition = .easeInOut(duration: 0.25) default: @@ -273,10 +302,59 @@ final class VideoScrubberComponent: Component { } @objc private func handlePositionHandlePan(_ gestureRecognizer: UIPanGestureRecognizer) { - guard let _ = self.component else { + guard let component = self.component else { return } - //let location = gestureRecognizer.location(in: self) + let location = gestureRecognizer.location(in: self) + let start = handleWidth + let end = self.frame.width - handleWidth + let length = end - start + let fraction = (location.x - start) / length + + let position = max(component.startPosition, min(component.endPosition, component.duration * fraction)) + let transition: Transition = .immediate + switch gestureRecognizer.state { + case .began, .changed: + self.isPanningPositionHandle = true + component.positionUpdated(position, false) + case .ended, .cancelled: + self.isPanningPositionHandle = false + component.positionUpdated(position, true) + default: + break + } + self.state?.updated(transition: transition) + } + + private func cursorFrame(size: CGSize, position: Double, duration : Double) -> CGRect { + let cursorPadding: CGFloat = 8.0 + let cursorPositionFraction = duration > 0.0 ? position / duration : 0.0 + let cursorPosition = floorToScreenPixels(handleWidth + handleWidth / 2.0 - cursorPadding + (size.width - handleWidth * 3.0 + cursorPadding * 2.0) * cursorPositionFraction) + var cursorFrame = CGRect(origin: CGPoint(x: cursorPosition - handleWidth / 2.0, y: -5.0 - UIScreenPixel), size: CGSize(width: handleWidth, height: 50.0)) + cursorFrame.origin.x = max(self.leftHandleView.frame.maxX - cursorPadding, cursorFrame.origin.x) + cursorFrame.origin.x = min(self.rightHandleView.frame.minX + cursorPadding, cursorFrame.origin.x) + return cursorFrame + } + + private func updateCursorPosition() { + guard let component = self.component, let scrubberSize = self.scrubberSize else { + return + } + let timestamp = CACurrentMediaTime() + + let updatedPosition: Double + if let (start, from, to) = self.positionAnimation { + let duration = to - from + let fraction = duration > 0.0 ? (timestamp - start) / duration : 0.0 + updatedPosition = max(component.startPosition, min(component.endPosition, from + (to - from) * fraction)) + if fraction >= 1.0 { + self.positionAnimation = (timestamp, component.startPosition, component.endPosition) + } + } else { + let advance = component.isPlaying ? timestamp - component.generationTimestamp : 0.0 + updatedPosition = max(component.startPosition, min(component.endPosition, component.position + advance)) + } + self.cursorView.frame = cursorFrame(size: scrubberSize, position: updatedPosition, duration: component.duration) } func update(component: VideoScrubberComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { @@ -285,6 +363,8 @@ final class VideoScrubberComponent: Component { self.state = state let scrubberSize = CGSize(width: availableSize.width, height: scrubberHeight) + self.scrubberSize = scrubberSize + let bounds = CGRect(origin: .zero, size: scrubberSize) if component.framesUpdateTimestamp != previousFramesUpdateTimestamp { @@ -316,7 +396,7 @@ final class VideoScrubberComponent: Component { } } - let trimColor = self.isPanningHandle ? UIColor(rgb: 0xf8d74a) : .white + let trimColor = self.isPanningTrimHandle ? UIColor(rgb: 0xf8d74a) : .white transition.setTintColor(view: self.leftHandleView, color: trimColor) transition.setTintColor(view: self.rightHandleView, color: trimColor) transition.setTintColor(view: self.borderView, color: trimColor) @@ -333,6 +413,19 @@ final class VideoScrubberComponent: Component { let rightHandleFrame = CGRect(origin: CGPoint(x: max(leftHandleFrame.maxX, rightHandlePosition - handleWidth / 2.0), y: 0.0), size: CGSize(width: handleWidth, height: scrubberSize.height)) transition.setFrame(view: self.rightHandleView, frame: rightHandleFrame) + + if self.isPanningPositionHandle || !component.isPlaying { + self.positionAnimation = nil + self.displayLink?.isPaused = true + transition.setFrame(view: self.cursorView, frame: cursorFrame(size: scrubberSize, position: component.position, duration: component.duration)) + } else { + if self.positionAnimation == nil { + self.positionAnimation = (CACurrentMediaTime(), component.position, component.endPosition) + } + self.displayLink?.isPaused = false + self.updateCursorPosition() + } + transition.setAlpha(view: self.cursorView, alpha: self.isPanningTrimHandle ? 0.0 : 1.0) let borderFrame = CGRect(origin: CGPoint(x: leftHandleFrame.maxX, y: 0.0), size: CGSize(width: rightHandleFrame.minX - leftHandleFrame.maxX, height: scrubberSize.height)) transition.setFrame(view: self.borderView, frame: borderFrame)