From 62f92ee5af4a1d76d650955632ca9ee4e6dda5bb Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 10 Jun 2023 02:23:53 +0400 Subject: [PATCH] Camera and editor improvements --- .../CameraScreen/Sources/CameraScreen.swift | 28 ++++---- .../Sources/CaptureControlsComponent.swift | 19 ++++-- .../MediaEditor/Sources/MediaEditor.swift | 4 ++ .../Sources/MediaEditorValues.swift | 55 +++++++++++----- .../Sources/VideoTextureSource.swift | 2 +- .../Sources/MediaEditorScreen.swift | 66 +++++++++++++++---- .../Sources/VideoScrubberComponent.swift | 17 ++++- 7 files changed, 135 insertions(+), 56 deletions(-) diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 13a0d15f9a..c168cf8100 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -43,7 +43,7 @@ private struct CameraState { return CameraState(mode: mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, recording: self.recording, duration: self.duration) } - func updatedPosition(_ mode: Camera.Position) -> CameraState { + func updatedPosition(_ position: Camera.Position) -> CameraState { return CameraState(mode: self.mode, position: position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, recording: self.recording, duration: self.duration) } @@ -216,13 +216,7 @@ private final class CameraScreenComponent: CombinedComponent { self.hapticFeedback.impact(.light) } - private var lastFlipTimestamp: Double? func togglePosition() { - let currentTimestamp = CACurrentMediaTime() - if let lastFlipTimestamp = self.lastFlipTimestamp, currentTimestamp - lastFlipTimestamp < 2.0 { - return - } - self.lastFlipTimestamp = currentTimestamp self.camera.togglePosition() self.hapticFeedback.impact(.light) } @@ -1334,7 +1328,7 @@ public class CameraScreen: ViewController { private var galleryController: ViewController? public func returnFromEditor() { - self.node.animateInFromEditor(toGallery: self.galleryController != nil) + self.node.animateInFromEditor(toGallery: self.galleryController?.displayNode.supernode != nil) } func presentGallery(fromGesture: Bool = false) { @@ -1377,17 +1371,17 @@ public class CameraScreen: ViewController { self.node.resumeCameraCapture() } }) - controller.customModalStyleOverlayTransitionFactorUpdated = { [weak self, weak controller] transition in - if let self, let controller { - let transitionFactor = controller.modalStyleOverlayTransitionFactor - if transitionFactor > 0.1 { - stopCameraCapture() - } - self.node.updateModalTransitionFactor(transitionFactor, transition: transition) - } - } self.galleryController = controller } + controller.customModalStyleOverlayTransitionFactorUpdated = { [weak self, weak controller] transition in + if let self, let controller { + let transitionFactor = controller.modalStyleOverlayTransitionFactor + if transitionFactor > 0.1 { + stopCameraCapture() + } + self.node.updateModalTransitionFactor(transitionFactor, transition: transition) + } + } self.push(controller) } diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift index d1d547619f..9d332c01ed 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift @@ -370,6 +370,12 @@ final class CaptureControlsComponent: Component { private let lockImage = UIImage(bundleImageName: "Camera/LockIcon") + private var lastFlipTimestamp: Double? + private var didFlip = false + + private var wasBanding: Bool? + private var panBlobState: ShutterBlobView.BlobState? + private let hapticFeedback = HapticFeedback() public func matches(tag: Any) -> Bool { @@ -425,9 +431,6 @@ final class CaptureControlsComponent: Component { } } - private var didFlip = false - private var wasBanding: Bool? - private var panBlobState: ShutterBlobView.BlobState? @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { guard let component = self.component else { return @@ -651,7 +654,15 @@ final class CaptureControlsComponent: Component { ) ), minSize: CGSize(width: 44.0, height: 44.0), - action: { + action: { [weak self] in + guard let self else { + return + } + let currentTimestamp = CACurrentMediaTime() + if let lastFlipTimestamp = self.lastFlipTimestamp, currentTimestamp - lastFlipTimestamp < 1.3 { + return + } + self.lastFlipTimestamp = currentTimestamp component.flipTapped() flipAnimationAction.invoke(Void()) } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 5878884c34..3eb4bd197c 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -494,6 +494,10 @@ public final class MediaEditor { } } + public var isPlaying: Bool { + return (self.player?.rate ?? 0.0) > 0.0 + } + public func play() { self.player?.play() } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift index a53b2c9272..db7cbc1525 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift @@ -21,22 +21,22 @@ public enum EditorToolKey: Int32, CaseIterable { case highlightsTint case blur case curves + + static let adjustmentToolsKeys: [EditorToolKey] = [ + .enhance, + .brightness, + .contrast, + .saturation, + .warmth, + .fade, + .highlights, + .shadows, + .vignette, + .grain, + .sharpen + ] } -private let adjustmentToolsKeys: [EditorToolKey] = [ - .enhance, - .brightness, - .contrast, - .saturation, - .warmth, - .fade, - .highlights, - .shadows, - .vignette, - .grain, - .sharpen -] - public final class MediaEditorValues: Codable, Equatable { public static func == (lhs: MediaEditorValues, rhs: MediaEditorValues) -> Bool { if lhs.originalDimensions != rhs.originalDimensions { @@ -75,9 +75,28 @@ public final class MediaEditorValues: Codable, Equatable { if lhs.entities != rhs.entities { return false } -// if lhs.toolValues != rhs.toolValues { -// return false -// } + + + for key in EditorToolKey.allCases { + let lhsToolValue = lhs.toolValues[key] + let rhsToolValue = rhs.toolValues[key] + if (lhsToolValue == nil) != (rhsToolValue == nil) { + return false + } + if let lhsToolValue = lhsToolValue as? Float, let rhsToolValue = rhsToolValue as? Float { + return lhsToolValue != rhsToolValue + } + if let lhsToolValue = lhsToolValue as? BlurValue, let rhsToolValue = rhsToolValue as? BlurValue { + return lhsToolValue != rhsToolValue + } + if let lhsToolValue = lhsToolValue as? TintValue, let rhsToolValue = rhsToolValue as? TintValue { + return lhsToolValue != rhsToolValue + } + if let lhsToolValue = lhsToolValue as? CurvesValue, let rhsToolValue = rhsToolValue as? CurvesValue { + return lhsToolValue != rhsToolValue + } + } + return true } @@ -662,7 +681,7 @@ public struct CurvesValue: Equatable, Codable { private let toolEpsilon: Float = 0.005 public extension MediaEditorValues { var hasAdjustments: Bool { - for key in adjustmentToolsKeys { + for key in EditorToolKey.adjustmentToolsKeys { if let value = self.toolValues[key] as? Float, abs(value) > toolEpsilon { return true } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift index ba56a8f5a6..a2c9051877 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift @@ -71,7 +71,7 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD } func invalidate() { - self.playerItemOutput?.setDelegate(nil, queue: self.queue) + self.playerItemOutput?.setDelegate(nil, queue: nil) self.playerItemOutput = nil self.playerItemObservation?.invalidate() self.playerItemStatusObservation?.invalidate() diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 90ccaa4f1e..556ef181dd 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -287,10 +287,14 @@ final class MediaEditorScreenComponent: Component { view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) } - } - - if let view = self.inputPanel.view { - if case .camera = source { + + if let view = self.inputPanel.view { + view.layer.animatePosition(from: CGPoint(x: 0.0, y: 44.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + view.layer.animateScale(from: 0.6, to: 1.0, duration: 0.2) + } + + if let view = self.scrubber.view { view.layer.animatePosition(from: CGPoint(x: 0.0, y: 44.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) view.layer.animateScale(from: 0.6, to: 1.0, duration: 0.2) @@ -327,8 +331,15 @@ final class MediaEditorScreenComponent: Component { transition.setScale(view: view, scale: 0.1) } - if let view = self.inputPanel.view { - if case .camera = source { + + if case .camera = source { + if let view = self.inputPanel.view { + view.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 44.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) + view.layer.animateAlpha(from: view.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) + view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) + } + + if let view = self.scrubber.view { view.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 44.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) view.layer.animateAlpha(from: view.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) @@ -426,6 +437,7 @@ final class MediaEditorScreenComponent: Component { } } + private var isEditingCaption = false func update(component: MediaEditorScreenComponent, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { guard !self.isDismissed else { return availableSize @@ -749,16 +761,25 @@ final class MediaEditorScreenComponent: Component { environment: {}, containerSize: CGSize(width: availableSize.width, height: 200.0) ) - + let fadeTransition = Transition(animation: .curve(duration: 0.3, curve: .easeInOut)) if self.inputPanelExternalState.isEditing { - component.mediaEditor?.stop() fadeTransition.setAlpha(view: self.fadeView, alpha: 1.0) } else { - component.mediaEditor?.play() fadeTransition.setAlpha(view: self.fadeView, alpha: 0.0) } transition.setFrame(view: self.fadeView, frame: CGRect(origin: .zero, size: availableSize)) + + let isEditingCaption = self.inputPanelExternalState.isEditing + if self.isEditingCaption != isEditingCaption { + self.isEditingCaption = isEditingCaption + + if isEditingCaption { + mediaEditor?.stop() + } else { + mediaEditor?.play() + } + } var isEditingTextEntity = false var sizeSliderVisible = false @@ -1130,6 +1151,8 @@ public final class MediaEditorScreen: ViewController { fileprivate var subject: MediaEditorScreen.Subject? private var subjectDisposable: Disposable? + private var appInForegroundDisposable: Disposable? + private var wasPlaying = false private let backgroundDimView: UIView fileprivate let componentHost: ComponentView @@ -1289,11 +1312,24 @@ public final class MediaEditorScreen: ViewController { } } } + + self.appInForegroundDisposable = (controller.context.sharedContext.applicationBindings.applicationInForeground + |> deliverOnMainQueue).start(next: { [weak self] inForeground in + if let self, let mediaEditor = self.mediaEditor { + if inForeground && self.wasPlaying { + mediaEditor.play() + } else if !inForeground { + self.wasPlaying = mediaEditor.isPlaying + mediaEditor.stop() + } + } + }) } deinit { self.subjectDisposable?.dispose() self.gradientColorsDisposable?.dispose() + self.appInForegroundDisposable?.dispose() } private func setup(with subject: MediaEditorScreen.Subject) { @@ -2254,6 +2290,10 @@ public final class MediaEditorScreen: ViewController { self.transitionOut = transitionOut self.completion = completion + if let transitionIn, case .camera = transitionIn { + self.isSavingAvailable = true + } + super.init(navigationBarPresentationData: nil) self.navigationPresentation = .flatModal @@ -3094,7 +3134,7 @@ private final class ToolValueComponent: Component { self.state = state let titleSize = self.title.update( - transition: transition, + transition: .immediate, component: AnyComponent(Text( text: component.title, font: Font.light(34.0), @@ -3116,11 +3156,11 @@ private final class ToolValueComponent: Component { self.addSubview(titleView) } transition.setPosition(view: titleView, position: titleFrame.center) - transition.setBounds(view: titleView, bounds: CGRect(origin: .zero, size: titleFrame.size)) + titleView.bounds = CGRect(origin: .zero, size: titleFrame.size) } let valueSize = self.value.update( - transition: transition, + transition: .immediate, component: AnyComponent(Text( text: component.value, font: Font.with(size: 90.0, weight: .thin, traits: .monospacedNumbers), @@ -3142,7 +3182,7 @@ private final class ToolValueComponent: Component { self.addSubview(valueView) } transition.setPosition(view: valueView, position: valueFrame.center) - transition.setBounds(view: valueView, bounds: CGRect(origin: .zero, size: valueFrame.size)) + valueView.bounds = CGRect(origin: .zero, size: valueFrame.size) } if let previousValue, component.value != previousValue, self.alpha > 0.0 { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift index d6c6b085ca..3dff49801f 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift @@ -29,6 +29,14 @@ private class VideoFrameLayer: SimpleShapeLayer { } } +private final class HandleView: UIImageView { + var hitTestSlop = UIEdgeInsets() + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + return self.bounds.inset(by: self.hitTestSlop).contains(point) + } +} + final class VideoScrubberComponent: Component { typealias EnvironmentType = Empty @@ -93,10 +101,10 @@ final class VideoScrubberComponent: Component { } final class View: UIView, UITextFieldDelegate { - private let leftHandleView = UIImageView() - private let rightHandleView = UIImageView() + private let leftHandleView = HandleView() + private let rightHandleView = HandleView() private let borderView = UIImageView() - private let cursorView = UIImageView() + private let cursorView = HandleView() private let transparentFramesContainer = UIView() private let opaqueFramesContainer = UIView() @@ -144,14 +152,17 @@ final class VideoScrubberComponent: Component { self.leftHandleView.image = handleImage self.leftHandleView.isUserInteractionEnabled = true self.leftHandleView.tintColor = .white + self.leftHandleView.hitTestSlop = UIEdgeInsets(top: -8.0, left: -9.0, bottom: -8.0, right: -9.0) self.rightHandleView.image = handleImage self.rightHandleView.transform = CGAffineTransform(scaleX: -1.0, y: 1.0) self.rightHandleView.isUserInteractionEnabled = true self.rightHandleView.tintColor = .white + self.rightHandleView.hitTestSlop = UIEdgeInsets(top: -8.0, left: -9.0, bottom: -8.0, right: -9.0) self.cursorView.image = positionImage self.cursorView.isUserInteractionEnabled = true + self.cursorView.hitTestSlop = UIEdgeInsets(top: -8.0, left: -9.0, bottom: -8.0, right: -9.0) self.borderView.image = generateImage(CGSize(width: 1.0, height: scrubberHeight), rotatedContext: { size, context in context.clear(CGRect(origin: .zero, size: size))