Camera and editor improvements

This commit is contained in:
Ilya Laktyushin
2023-06-11 01:08:55 +04:00
parent 12dde45917
commit ec9cb0a09f
8 changed files with 173 additions and 24 deletions

View File

@@ -180,6 +180,7 @@ public final class _UpdatedChildComponent {
var _opacity: CGFloat? var _opacity: CGFloat?
var _cornerRadius: CGFloat? var _cornerRadius: CGFloat?
var _clipsToBounds: Bool? var _clipsToBounds: Bool?
var _shadow: Shadow?
fileprivate var transitionAppear: Transition.Appear? fileprivate var transitionAppear: Transition.Appear?
fileprivate var transitionAppearWithGuide: (Transition.AppearWithGuide, _AnyChildComponent.Id)? fileprivate var transitionAppearWithGuide: (Transition.AppearWithGuide, _AnyChildComponent.Id)?
@@ -260,6 +261,11 @@ public final class _UpdatedChildComponent {
self._clipsToBounds = clipsToBounds self._clipsToBounds = clipsToBounds
return self return self
} }
@discardableResult public func shadow(_ shadow: Shadow?) -> _UpdatedChildComponent {
self._shadow = shadow
return self
}
@discardableResult public func gesture(_ gesture: Gesture) -> _UpdatedChildComponent { @discardableResult public func gesture(_ gesture: Gesture) -> _UpdatedChildComponent {
self.gestures.append(gesture) self.gestures.append(gesture)
@@ -706,6 +712,16 @@ public extension CombinedComponent {
updatedChild.view.alpha = updatedChild._opacity ?? 1.0 updatedChild.view.alpha = updatedChild._opacity ?? 1.0
updatedChild.view.clipsToBounds = updatedChild._clipsToBounds ?? false updatedChild.view.clipsToBounds = updatedChild._clipsToBounds ?? false
updatedChild.view.layer.cornerRadius = updatedChild._cornerRadius ?? 0.0 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 updatedChild.view.context(typeErasedComponent: updatedChild.component).erasedState._updated = { [weak viewContext] transition in
guard let viewContext = viewContext else { guard let viewContext = viewContext else {
return return
@@ -834,3 +850,19 @@ public extension CombinedComponent {
return ActionSlot<Arguments>() return ActionSlot<Arguments>()
} }
} }
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
}
}

View File

@@ -178,6 +178,11 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
return entities return entities
} }
public var hasEntities: Bool {
let entities = self.entities.filter { !($0 is DrawingMediaEntity) }
return !entities.isEmpty
}
private var initialEntitiesData: Data? private var initialEntitiesData: Data?
public func setup(withEntitiesData entitiesData: Data?) { public func setup(withEntitiesData entitiesData: Data?) {
self.clear() self.clear()

View File

@@ -17,6 +17,7 @@ import ComponentDisplayAdapters
import LottieAnimationComponent import LottieAnimationComponent
import ViewControllerComponent import ViewControllerComponent
import BlurredBackgroundComponent import BlurredBackgroundComponent
import MultilineTextComponent
import ContextUI import ContextUI
import ChatEntityKeyboardInputNode import ChatEntityKeyboardInputNode
import EntityKeyboard 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)) .position(CGPoint(x: context.availableSize.width / 2.0 + (isFilled != nil ? 46.0 : 0.0), y: environment.safeInsets.top + 31.0))
.appear(.default(scale: true)) .appear(.default(scale: true))
.disappear(.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)) .position(CGPoint(x: environment.safeInsets.left + undoButton.size.width / 2.0 + 2.0, y: topInset))
.scale(isEditingText ? 0.01 : 1.0) .scale(isEditingText ? 0.01 : 1.0)
.opacity(isEditingText || !controlsAreVisible ? 0.0 : 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)) .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) .scale(state.drawingViewState.canRedo && !isEditingText ? 1.0 : 0.01)
.opacity(state.drawingViewState.canRedo && !isEditingText && controlsAreVisible ? 1.0 : 0.0) .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( let clearAllButton = clearAllButton.update(
component: Button( component: Button(
content: AnyComponent( 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, isEnabled: state.drawingViewState.canClear,
action: { action: {

View File

@@ -895,7 +895,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, UIPencilInt
self.stateUpdated(NavigationState( self.stateUpdated(NavigationState(
canUndo: !self.undoStack.isEmpty, canUndo: !self.undoStack.isEmpty,
canRedo: !self.redoStack.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, canZoomOut: self.zoomScale > 1.0 + .ulpOfOne,
isDrawing: self.isDrawing isDrawing: self.isDrawing
)) ))

View File

@@ -11,9 +11,11 @@ import TelegramPresentationData
import FastBlur import FastBlur
public struct MediaEditorPlayerState { public struct MediaEditorPlayerState {
public let generationTimestamp: Double
public let duration: Double public let duration: Double
public let timeRange: Range<Double>? public let timeRange: Range<Double>?
public let position: Double public let position: Double
public let isPlaying: Bool
public let frames: [UIImage] public let frames: [UIImage]
public let framesCount: Int public let framesCount: Int
public let framesUpdateTimestamp: Double public let framesUpdateTimestamp: Double
@@ -107,12 +109,12 @@ public final class MediaEditor {
} }
private let playerPromise = Promise<AVPlayer?>() private let playerPromise = Promise<AVPlayer?>()
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 { didSet {
self.playerPlaybackStatePromise.set(.single(self.playerPlaybackState)) 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 = {} public var onFirstDisplay: () -> Void = {}
@@ -122,12 +124,14 @@ public final class MediaEditor {
if let self, let asset = player?.currentItem?.asset { if let self, let asset = player?.currentItem?.asset {
return combineLatest(self.valuesPromise.get(), self.playerPlaybackStatePromise.get(), self.videoFrames(asset: asset, count: framesCount)) return combineLatest(self.valuesPromise.get(), self.playerPlaybackStatePromise.get(), self.videoFrames(asset: asset, count: framesCount))
|> map { values, durationAndPosition, framesAndUpdateTimestamp in |> map { values, durationAndPosition, framesAndUpdateTimestamp in
let (duration, position, hasAudio) = durationAndPosition let (duration, position, isPlaying, hasAudio) = durationAndPosition
let (frames, framesUpdateTimestamp) = framesAndUpdateTimestamp let (frames, framesUpdateTimestamp) = framesAndUpdateTimestamp
return MediaEditorPlayerState( return MediaEditorPlayerState(
generationTimestamp: CACurrentMediaTime(),
duration: duration, duration: duration,
timeRange: values.videoTrimRange, timeRange: values.videoTrimRange,
position: position, position: position,
isPlaying: isPlaying,
frames: frames, frames: frames,
framesCount: framesCount, framesCount: framesCount,
framesUpdateTimestamp: framesUpdateTimestamp, framesUpdateTimestamp: framesUpdateTimestamp,
@@ -409,7 +413,7 @@ public final class MediaEditor {
if let audioTracks = player.currentItem?.asset.tracks(withMediaType: .audio) { if let audioTracks = player.currentItem?.asset.tracks(withMediaType: .audio) {
hasAudio = !audioTracks.isEmpty 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 self.didPlayToEndTimeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player.currentItem, queue: nil, using: { [weak self] notification in
if let self { if let self {

View File

@@ -23,6 +23,8 @@ public final class MediaEditorDraft: Codable, Equatable {
case dimensionsWidth case dimensionsWidth
case dimensionsHeight case dimensionsHeight
case values case values
case caption
case privacy
} }
public let path: String public let path: String
@@ -30,14 +32,16 @@ public final class MediaEditorDraft: Codable, Equatable {
public let thumbnail: UIImage public let thumbnail: UIImage
public let dimensions: PixelDimensions public let dimensions: PixelDimensions
public let values: MediaEditorValues public let values: MediaEditorValues
public let caption: NSAttributedString
public let privacy: MediaEditorResultPrivacy? 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.path = path
self.isVideo = isVideo self.isVideo = isVideo
self.thumbnail = thumbnail self.thumbnail = thumbnail
self.dimensions = dimensions self.dimensions = dimensions
self.values = values self.values = values
self.caption = caption
self.privacy = privacy self.privacy = privacy
} }
@@ -62,6 +66,7 @@ public final class MediaEditorDraft: Codable, Equatable {
} else { } else {
fatalError() fatalError()
} }
self.caption = NSAttributedString()
self.privacy = nil self.privacy = nil
} }

View File

@@ -647,11 +647,13 @@ final class MediaEditorScreenComponent: Component {
transition: transition, transition: transition,
component: AnyComponent(VideoScrubberComponent( component: AnyComponent(VideoScrubberComponent(
context: component.context, context: component.context,
generationTimestamp: playerState.generationTimestamp,
duration: playerState.duration, duration: playerState.duration,
startPosition: playerState.timeRange?.lowerBound ?? 0.0, startPosition: playerState.timeRange?.lowerBound ?? 0.0,
endPosition: playerState.timeRange?.upperBound ?? min(playerState.duration, storyMaxVideoDuration), endPosition: playerState.timeRange?.upperBound ?? min(playerState.duration, storyMaxVideoDuration),
position: playerState.position, position: playerState.position,
maxDuration: storyMaxVideoDuration, maxDuration: storyMaxVideoDuration,
isPlaying: playerState.isPlaying,
frames: playerState.frames, frames: playerState.frames,
framesUpdateTimestamp: playerState.framesUpdateTimestamp, framesUpdateTimestamp: playerState.framesUpdateTimestamp,
trimUpdated: { [weak mediaEditor] start, end, updatedEnd, done in trimUpdated: { [weak mediaEditor] start, end, updatedEnd, done in
@@ -893,7 +895,7 @@ final class MediaEditorScreenComponent: Component {
if let saveButtonView = self.saveButton.view { if let saveButtonView = self.saveButton.view {
if saveButtonView.superview == nil { if saveButtonView.superview == nil {
saveButtonView.layer.shadowOffset = CGSize(width: 0.0, height: 0.0) 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.shadowColor = UIColor.black.cgColor
saveButtonView.layer.shadowOpacity = 0.35 saveButtonView.layer.shadowOpacity = 0.35
self.addSubview(saveButtonView) self.addSubview(saveButtonView)
@@ -941,7 +943,7 @@ final class MediaEditorScreenComponent: Component {
if let muteButtonView = self.muteButton.view { if let muteButtonView = self.muteButton.view {
if muteButtonView.superview == nil { if muteButtonView.superview == nil {
muteButtonView.layer.shadowOffset = CGSize(width: 0.0, height: 0.0) 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.shadowColor = UIColor.black.cgColor
muteButtonView.layer.shadowOpacity = 0.35 muteButtonView.layer.shadowOpacity = 0.35
self.addSubview(muteButtonView) self.addSubview(muteButtonView)
@@ -979,7 +981,7 @@ final class MediaEditorScreenComponent: Component {
if let settingsButtonView = self.settingsButton.view { if let settingsButtonView = self.settingsButton.view {
if settingsButtonView.superview == nil { if settingsButtonView.superview == nil {
settingsButtonView.layer.shadowOffset = CGSize(width: 0.0, height: 0.0) 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.shadowColor = UIColor.black.cgColor
settingsButtonView.layer.shadowOpacity = 0.35 settingsButtonView.layer.shadowOpacity = 0.35
//self.addSubview(settingsButtonView) //self.addSubview(settingsButtonView)
@@ -2644,7 +2646,7 @@ public final class MediaEditorScreen: ViewController {
let path = draftPath() + "/\(Int64.random(in: .min ... .max)).jpg" let path = draftPath() + "/\(Int64.random(in: .min ... .max)).jpg"
if let data = image.jpegData(compressionQuality: 0.87) { if let data = image.jpegData(compressionQuality: 0.87) {
try? data.write(to: URL(fileURLWithPath: path)) 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 { if let id {
saveStorySource(engine: self.context.engine, item: draft, id: id) saveStorySource(engine: self.context.engine, item: draft, id: id)
} else { } else {
@@ -2658,7 +2660,7 @@ public final class MediaEditorScreen: ViewController {
if let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) { if let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) {
let path = draftPath() + "/\(Int64.random(in: .min ... .max)).mp4" let path = draftPath() + "/\(Int64.random(in: .min ... .max)).mp4"
try? FileManager.default.moveItem(atPath: videoPath, toPath: path) 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 { if let id {
saveStorySource(engine: self.context.engine, item: draft, id: id) saveStorySource(engine: self.context.engine, item: draft, id: id)
} else { } else {

View File

@@ -41,11 +41,13 @@ final class VideoScrubberComponent: Component {
typealias EnvironmentType = Empty typealias EnvironmentType = Empty
let context: AccountContext let context: AccountContext
let generationTimestamp: Double
let duration: Double let duration: Double
let startPosition: Double let startPosition: Double
let endPosition: Double let endPosition: Double
let position: Double let position: Double
let maxDuration: Double let maxDuration: Double
let isPlaying: Bool
let frames: [UIImage] let frames: [UIImage]
let framesUpdateTimestamp: Double let framesUpdateTimestamp: Double
let trimUpdated: (Double, Double, Bool, Bool) -> Void let trimUpdated: (Double, Double, Bool, Bool) -> Void
@@ -53,22 +55,26 @@ final class VideoScrubberComponent: Component {
init( init(
context: AccountContext, context: AccountContext,
generationTimestamp: Double,
duration: Double, duration: Double,
startPosition: Double, startPosition: Double,
endPosition: Double, endPosition: Double,
position: Double, position: Double,
maxDuration: Double, maxDuration: Double,
isPlaying: Bool,
frames: [UIImage], frames: [UIImage],
framesUpdateTimestamp: Double, framesUpdateTimestamp: Double,
trimUpdated: @escaping (Double, Double, Bool, Bool) -> Void, trimUpdated: @escaping (Double, Double, Bool, Bool) -> Void,
positionUpdated: @escaping (Double, Bool) -> Void positionUpdated: @escaping (Double, Bool) -> Void
) { ) {
self.context = context self.context = context
self.generationTimestamp = generationTimestamp
self.duration = duration self.duration = duration
self.startPosition = startPosition self.startPosition = startPosition
self.endPosition = endPosition self.endPosition = endPosition
self.position = position self.position = position
self.maxDuration = maxDuration self.maxDuration = maxDuration
self.isPlaying = isPlaying
self.frames = frames self.frames = frames
self.framesUpdateTimestamp = framesUpdateTimestamp self.framesUpdateTimestamp = framesUpdateTimestamp
self.trimUpdated = trimUpdated self.trimUpdated = trimUpdated
@@ -79,6 +85,9 @@ final class VideoScrubberComponent: Component {
if lhs.context !== rhs.context { if lhs.context !== rhs.context {
return false return false
} }
if lhs.generationTimestamp != rhs.generationTimestamp {
return false
}
if lhs.duration != rhs.duration { if lhs.duration != rhs.duration {
return false return false
} }
@@ -94,6 +103,9 @@ final class VideoScrubberComponent: Component {
if lhs.maxDuration != rhs.maxDuration { if lhs.maxDuration != rhs.maxDuration {
return false return false
} }
if lhs.isPlaying != rhs.isPlaying {
return false
}
if lhs.framesUpdateTimestamp != rhs.framesUpdateTimestamp { if lhs.framesUpdateTimestamp != rhs.framesUpdateTimestamp {
return false return false
} }
@@ -114,6 +126,13 @@ final class VideoScrubberComponent: Component {
private var component: VideoScrubberComponent? private var component: VideoScrubberComponent?
private weak var state: EmptyComponentState? 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) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
@@ -134,17 +153,19 @@ final class VideoScrubberComponent: Component {
context.addPath(innerPath.cgPath) context.addPath(innerPath.cgPath)
context.fillPath() context.fillPath()
context.setBlendMode(.clear)
let holeSize = CGSize(width: 2.0, height: 11.0) 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) 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.addPath(holePath.cgPath)
context.fillPath() context.fillPath()
})?.withRenderingMode(.alwaysTemplate) })?.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.clear(CGRect(origin: .zero, size: size))
context.setFillColor(UIColor.white.cgColor) 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.addPath(path.cgPath)
context.fillPath() context.fillPath()
}) })
@@ -189,20 +210,28 @@ final class VideoScrubberComponent: Component {
self.leftHandleView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handleLeftHandlePan(_:)))) self.leftHandleView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handleLeftHandlePan(_:))))
self.rightHandleView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handleRightHandlePan(_:)))) self.rightHandleView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handleRightHandlePan(_:))))
self.cursorView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handlePositionHandlePan(_:)))) 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) { required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
private var isPanningHandle = false deinit {
self.displayLink?.invalidate()
}
@objc private func handleLeftHandlePan(_ gestureRecognizer: UIPanGestureRecognizer) { @objc private func handleLeftHandlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
guard let component = self.component else { guard let component = self.component else {
return return
} }
let location = gestureRecognizer.location(in: self) let location = gestureRecognizer.location(in: self)
let start = handleWidth / 2.0 let start = handleWidth / 2.0
let end = self.frame.width - handleWidth let end = self.frame.width - handleWidth / 2.0
let length = end - start let length = end - start
let fraction = (location.x - start) / length let fraction = (location.x - start) / length
@@ -219,13 +248,13 @@ final class VideoScrubberComponent: Component {
var transition: Transition = .immediate var transition: Transition = .immediate
switch gestureRecognizer.state { switch gestureRecognizer.state {
case .began, .changed: case .began, .changed:
self.isPanningHandle = true self.isPanningTrimHandle = true
component.trimUpdated(startValue, endValue, false, false) component.trimUpdated(startValue, endValue, false, false)
if case .began = gestureRecognizer.state { if case .began = gestureRecognizer.state {
transition = .easeInOut(duration: 0.25) transition = .easeInOut(duration: 0.25)
} }
case .ended, .cancelled: case .ended, .cancelled:
self.isPanningHandle = false self.isPanningTrimHandle = false
component.trimUpdated(startValue, endValue, false, true) component.trimUpdated(startValue, endValue, false, true)
transition = .easeInOut(duration: 0.25) transition = .easeInOut(duration: 0.25)
default: default:
@@ -240,7 +269,7 @@ final class VideoScrubberComponent: Component {
} }
let location = gestureRecognizer.location(in: self) let location = gestureRecognizer.location(in: self)
let start = handleWidth / 2.0 let start = handleWidth / 2.0
let end = self.frame.width - handleWidth let end = self.frame.width - handleWidth / 2.0
let length = end - start let length = end - start
let fraction = (location.x - start) / length let fraction = (location.x - start) / length
@@ -257,13 +286,13 @@ final class VideoScrubberComponent: Component {
var transition: Transition = .immediate var transition: Transition = .immediate
switch gestureRecognizer.state { switch gestureRecognizer.state {
case .began, .changed: case .began, .changed:
self.isPanningHandle = true self.isPanningTrimHandle = true
component.trimUpdated(startValue, endValue, true, false) component.trimUpdated(startValue, endValue, true, false)
if case .began = gestureRecognizer.state { if case .began = gestureRecognizer.state {
transition = .easeInOut(duration: 0.25) transition = .easeInOut(duration: 0.25)
} }
case .ended, .cancelled: case .ended, .cancelled:
self.isPanningHandle = false self.isPanningTrimHandle = false
component.trimUpdated(startValue, endValue, true, true) component.trimUpdated(startValue, endValue, true, true)
transition = .easeInOut(duration: 0.25) transition = .easeInOut(duration: 0.25)
default: default:
@@ -273,10 +302,59 @@ final class VideoScrubberComponent: Component {
} }
@objc private func handlePositionHandlePan(_ gestureRecognizer: UIPanGestureRecognizer) { @objc private func handlePositionHandlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
guard let _ = self.component else { guard let component = self.component else {
return 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<EnvironmentType>, transition: Transition) -> CGSize { func update(component: VideoScrubberComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
@@ -285,6 +363,8 @@ final class VideoScrubberComponent: Component {
self.state = state self.state = state
let scrubberSize = CGSize(width: availableSize.width, height: scrubberHeight) let scrubberSize = CGSize(width: availableSize.width, height: scrubberHeight)
self.scrubberSize = scrubberSize
let bounds = CGRect(origin: .zero, size: scrubberSize) let bounds = CGRect(origin: .zero, size: scrubberSize)
if component.framesUpdateTimestamp != previousFramesUpdateTimestamp { 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.leftHandleView, color: trimColor)
transition.setTintColor(view: self.rightHandleView, color: trimColor) transition.setTintColor(view: self.rightHandleView, color: trimColor)
transition.setTintColor(view: self.borderView, 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)) 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) 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)) 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) transition.setFrame(view: self.borderView, frame: borderFrame)