[WIP] Stickers editor

This commit is contained in:
Ilya Laktyushin
2024-04-11 14:15:37 +04:00
parent 8a11a281b7
commit 2d357afd12
22 changed files with 709 additions and 288 deletions

View File

@@ -751,6 +751,13 @@ private final class CameraScreenComponent: CombinedComponent {
state.cameraState = component.cameraState
state.volumeButtonsListenerActive = component.hasAppeared && component.isVisible
let isSticker: Bool
if let controller = controller() as? CameraScreen, case .sticker = controller.mode {
isSticker = true
} else {
isSticker = false
}
let isTablet: Bool
if case .regular = environment.metrics.widthClass {
isTablet = true
@@ -879,6 +886,7 @@ private final class CameraScreenComponent: CombinedComponent {
let captureControls = captureControls.update(
component: CaptureControlsComponent(
isTablet: isTablet,
isSticker: isSticker,
hasAppeared: component.hasAppeared && hasAllRequiredAccess,
hasAccess: hasAllRequiredAccess,
tintColor: controlsTintColor,
@@ -1063,7 +1071,7 @@ private final class CameraScreenComponent: CombinedComponent {
.disappear(.default(scale: true))
)
if !isTablet && Camera.isDualCameraSupported(forRoundVideo: false) {
if !isSticker && !isTablet && Camera.isDualCameraSupported(forRoundVideo: false) {
let dualButton = dualButton.update(
component: CameraButton(
content: AnyComponentWithIdentity(
@@ -1201,7 +1209,7 @@ private final class CameraScreenComponent: CombinedComponent {
}
}
if case .none = component.cameraState.recording, !state.isTransitioning && hasAllRequiredAccess {
if !isSticker, case .none = component.cameraState.recording, !state.isTransitioning && hasAllRequiredAccess {
let availableModeControlSize: CGSize
if isTablet {
availableModeControlSize = CGSize(width: panelWidth, height: 120.0)
@@ -1314,6 +1322,11 @@ private class BlurView: UIVisualEffectView {
}
public class CameraScreen: ViewController {
public enum Mode {
case story
case sticker
}
public enum PIPPosition: Int32 {
case topLeft
case topRight
@@ -1443,7 +1456,7 @@ public class CameraScreen: ViewController {
private var validLayout: ContainerViewLayout?
fileprivate var didAppear: () -> Void = {}
private let completion = ActionSlot<Signal<CameraScreen.Result, NoError>>()
var cameraState: CameraState {
@@ -1735,22 +1748,31 @@ public class CameraScreen: ViewController {
fileprivate var captureStartTimestamp: Double?
private func setupCamera() {
guard self.camera == nil else {
guard self.camera == nil, let controller = self.controller else {
return
}
let camera = Camera(
configuration: Camera.Configuration(
preset: .hd1920x1080,
position: self.cameraState.position,
isDualEnabled: self.cameraState.isDualCameraEnabled,
audio: true,
photo: true,
metadata: false
),
previewView: self.mainPreviewView,
secondaryPreviewView: self.additionalPreviewView
)
var isNew = false
let camera: Camera
if let cameraHolder = controller.holder {
camera = cameraHolder.camera
self.mainPreviewView = cameraHolder.previewView
self.mainPreviewContainerView.addSubview(self.mainPreviewView)
} else {
camera = Camera(
configuration: Camera.Configuration(
preset: .hd1920x1080,
position: self.cameraState.position,
isDualEnabled: self.cameraState.isDualCameraEnabled,
audio: true,
photo: true,
metadata: false
),
previewView: self.mainPreviewView,
secondaryPreviewView: self.additionalPreviewView
)
isNew = true
}
self.cameraStateDisposable = combineLatest(
queue: Queue.mainQueue(),
@@ -1841,12 +1863,14 @@ public class CameraScreen: ViewController {
})
camera.focus(at: CGPoint(x: 0.5, y: 0.5), autoFocus: true)
camera.startCapture()
if isNew {
camera.startCapture()
}
self.captureStartTimestamp = CACurrentMediaTime()
self.camera = camera
if self.hasAppeared {
if isNew && self.hasAppeared {
self.maybePresentTooltips()
}
}
@@ -2025,45 +2049,75 @@ public class CameraScreen: ViewController {
)
}
var animatedIn = false
func animateIn() {
guard let controller = self.controller else {
return
}
self.transitionDimView.alpha = 0.0
self.backgroundView.alpha = 0.0
UIView.animate(withDuration: 0.4, animations: {
self.backgroundView.alpha = 1.0
})
if let layout = self.validLayout, layout.metrics.isTablet {
self.controller?.statusBar.updateStatusBarStyle(.Hide, animated: true)
if let layout = self.validLayout {
if layout.metrics.isTablet {
controller.statusBar.updateStatusBarStyle(.Hide, animated: true)
} else {
controller.statusBar.updateStatusBarStyle(.White, animated: true)
}
}
if let transitionIn = self.controller?.transitionIn, let sourceView = transitionIn.sourceView {
let sourceLocalFrame = sourceView.convert(transitionIn.sourceRect, to: self.view)
let sourceScale = sourceLocalFrame.width / self.previewContainerView.frame.width
self.previewContainerView.layer.animatePosition(from: sourceLocalFrame.center, to: self.previewContainerView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
self.requestUpdateLayout(hasAppeared: true, transition: .immediate)
})
self.previewContainerView.layer.animateScale(from: sourceScale, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
let minSide = min(self.previewContainerView.bounds.width, self.previewContainerView.bounds.height)
self.previewContainerView.layer.animateBounds(from: CGRect(origin: CGPoint(x: (self.previewContainerView.bounds.width - minSide) / 2.0, y: (self.previewContainerView.bounds.height - minSide) / 2.0), size: CGSize(width: minSide, height: minSide)), to: self.previewContainerView.bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
self.previewContainerView.layer.animate(
from: minSide / 2.0 as NSNumber,
to: self.previewContainerView.layer.cornerRadius as NSNumber,
keyPath: "cornerRadius",
timingFunction: kCAMediaTimingFunctionSpring,
duration: 0.3
)
if case .story = controller.mode {
let sourceScale = sourceLocalFrame.width / self.previewContainerView.frame.width
self.previewContainerView.layer.animatePosition(from: sourceLocalFrame.center, to: self.previewContainerView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
self.requestUpdateLayout(hasAppeared: true, transition: .immediate)
})
self.previewContainerView.layer.animateScale(from: sourceScale, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
let minSide = min(self.previewContainerView.bounds.width, self.previewContainerView.bounds.height)
self.previewContainerView.layer.animateBounds(from: CGRect(origin: CGPoint(x: (self.previewContainerView.bounds.width - minSide) / 2.0, y: (self.previewContainerView.bounds.height - minSide) / 2.0), size: CGSize(width: minSide, height: minSide)), to: self.previewContainerView.bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
self.previewContainerView.layer.animate(
from: minSide / 2.0 as NSNumber,
to: self.previewContainerView.layer.cornerRadius as NSNumber,
keyPath: "cornerRadius",
timingFunction: kCAMediaTimingFunctionSpring,
duration: 0.3
)
} else {
let sourceInnerFrame = sourceView.convert(transitionIn.sourceRect, to: self.previewContainerView)
let sourceCenter = sourceInnerFrame.center
self.mainPreviewView.layer.position = CGPoint(x: self.previewContainerView.frame.width / 2.0, y: self.previewContainerView.frame.height / 2.0)
self.mainPreviewView.layer.animatePosition(from: sourceCenter, to: self.mainPreviewView.layer.position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
self.requestUpdateLayout(hasAppeared: true, transition: .immediate)
})
self.mainPreviewView.layer.animateBounds(from: self.mainPreviewView.bounds, to: CGRect(origin: .zero, size: self.previewContainerView.frame.size), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
let sourceScale = self.mainPreviewView.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0
self.mainPreviewView.transform = CGAffineTransform.identity
self.mainPreviewView.layer.animateScale(from: sourceScale, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
Queue.mainQueue().justDispatch {
self.animatedIn = true
}
})
}
if let view = self.componentHost.view {
view.layer.animatePosition(from: sourceLocalFrame.center, to: view.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
}
func animateOut(completion: @escaping () -> Void) {
self.camera?.stopCapture(invalidate: true)
guard let controller = self.controller else {
return
}
UIView.animate(withDuration: 0.25, animations: {
self.backgroundView.alpha = 0.0
@@ -2071,34 +2125,52 @@ public class CameraScreen: ViewController {
if let transitionOut = self.controller?.transitionOut(false), let destinationView = transitionOut.destinationView {
let destinationLocalFrame = destinationView.convert(transitionOut.destinationRect, to: self.view)
let targetScale = destinationLocalFrame.width / self.previewContainerView.frame.width
self.previewContainerView.layer.animatePosition(from: self.previewContainerView.center, to: destinationLocalFrame.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
completion()
})
self.previewContainerView.layer.animateScale(from: 1.0, to: targetScale, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
let minSide = min(self.previewContainerView.bounds.width, self.previewContainerView.bounds.height)
self.previewContainerView.layer.animateBounds(from: self.previewContainerView.bounds, to: CGRect(origin: CGPoint(x: (self.previewContainerView.bounds.width - minSide) / 2.0, y: (self.previewContainerView.bounds.height - minSide) / 2.0), size: CGSize(width: minSide, height: minSide)), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
self.previewContainerView.layer.animate(
from: self.previewContainerView.layer.cornerRadius as NSNumber,
to: minSide / 2.0 as NSNumber,
keyPath: "cornerRadius",
timingFunction: kCAMediaTimingFunctionSpring,
duration: 0.3,
removeOnCompletion: false
)
if case .story = controller.mode {
self.previewContainerView.layer.animatePosition(from: self.previewContainerView.center, to: destinationLocalFrame.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
completion()
})
self.previewContainerView.layer.animateScale(from: 1.0, to: targetScale, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
let minSide = min(self.previewContainerView.bounds.width, self.previewContainerView.bounds.height)
self.previewContainerView.layer.animateBounds(from: self.previewContainerView.bounds, to: CGRect(origin: CGPoint(x: (self.previewContainerView.bounds.width - minSide) / 2.0, y: (self.previewContainerView.bounds.height - minSide) / 2.0), size: CGSize(width: minSide, height: minSide)), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
self.previewContainerView.layer.animate(
from: self.previewContainerView.layer.cornerRadius as NSNumber,
to: minSide / 2.0 as NSNumber,
keyPath: "cornerRadius",
timingFunction: kCAMediaTimingFunctionSpring,
duration: 0.3,
removeOnCompletion: false
)
} else {
let destinationInnerFrame = destinationView.convert(transitionOut.destinationRect, to: self.previewContainerView)
let initialCenter = self.mainPreviewView.layer.position
self.mainPreviewView.layer.position = destinationInnerFrame.center
self.mainPreviewView.layer.animatePosition(from: initialCenter, to: self.mainPreviewView.layer.position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
completion()
})
self.mainPreviewView.layer.animateBounds(from: self.mainPreviewView.bounds, to: CGRect(origin: .zero, size: self.previewContainerView.frame.size), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
let targetScale = destinationInnerFrame.width / self.previewContainerView.frame.width
// self.mainPreviewView.transform = CGAffineTransform.identity
self.mainPreviewView.layer.animateScale(from: 1.0, to: targetScale, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
}
if let view = self.componentHost.view {
view.layer.animatePosition(from: view.center, to: destinationLocalFrame.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
view.layer.animateScale(from: 1.0, to: targetScale, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
view.layer.animateScale(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
} else {
completion()
}
self.componentHost.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
self.previewContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false)
if case .story = controller.mode {
self.previewContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false)
}
}
func animateOutToEditor() {
@@ -2344,7 +2416,7 @@ public class CameraScreen: ViewController {
fileprivate var hasAppeared = false
func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, hasAppeared: Bool = false, transition: Transition) {
guard let _ = self.controller else {
guard let controller = self.controller else {
return
}
let isFirstTime = self.validLayout == nil
@@ -2578,7 +2650,14 @@ public class CameraScreen: ViewController {
self.additionalPreviewContainerView.insertSubview(additionalPreviewView, at: 0)
}
mainPreviewView.frame = mainPreviewInnerFrame
if case .sticker = controller.mode {
if self.animatedIn {
mainPreviewView.frame = mainPreviewInnerFrame
}
} else {
mainPreviewView.frame = mainPreviewInnerFrame
}
additionalPreviewView.frame = additionalPreviewInnerFrame
self.previewFrameLeftDimView.isHidden = !isTablet
@@ -2604,14 +2683,14 @@ public class CameraScreen: ViewController {
transition.setPosition(view: self.transitionCornersView, position: CGPoint(x: layout.size.width + screenCornerRadius / 2.0, y: layout.size.height / 2.0))
transition.setBounds(view: self.transitionCornersView, bounds: CGRect(origin: .zero, size: CGSize(width: screenCornerRadius, height: layout.size.height)))
if isTablet && isFirstTime {
if (controller.mode == .sticker || isTablet) && isFirstTime {
self.animateIn()
}
if self.cameraState.flashMode == .on && (self.cameraState.recording != .none || self.cameraState.mode == .video) {
self.controller?.statusBarStyle = .Black
controller.statusBarStyle = .Black
} else {
self.controller?.statusBarStyle = .White
controller.statusBarStyle = .White
}
}
}
@@ -2621,6 +2700,7 @@ public class CameraScreen: ViewController {
}
private let context: AccountContext
fileprivate let mode: Mode
fileprivate let holder: CameraHolder?
fileprivate let transitionIn: TransitionIn?
fileprivate let transitionOut: (Bool) -> TransitionOut?
@@ -2645,6 +2725,7 @@ public class CameraScreen: ViewController {
}
fileprivate let completion: (Signal<CameraScreen.Result, NoError>, ResultTransition?, @escaping () -> Void) -> Void
public var transitionedIn: () -> Void = {}
public var transitionedOut: () -> Void = {}
private var audioSessionDisposable: Disposable?
@@ -2663,6 +2744,8 @@ public class CameraScreen: ViewController {
return self.node.cameraState
}
public var isEmbedded = false
fileprivate func updateCameraState(_ f: (CameraState) -> CameraState, transition: Transition) {
self.node.cameraState = f(self.node.cameraState)
self.node.requestUpdateLayout(hasAppeared: self.node.hasAppeared, transition: transition)
@@ -2670,12 +2753,14 @@ public class CameraScreen: ViewController {
public init(
context: AccountContext,
mode: Mode,
holder: CameraHolder? = nil,
transitionIn: TransitionIn?,
transitionOut: @escaping (Bool) -> TransitionOut?,
completion: @escaping (Signal<CameraScreen.Result, NoError>, ResultTransition?, @escaping () -> Void) -> Void
) {
self.context = context
self.mode = mode
self.holder = holder
self.transitionIn = transitionIn
self.transitionOut = transitionOut
@@ -2907,13 +2992,17 @@ public class CameraScreen: ViewController {
self.hapticFeedback.impact(.light)
}
self.node.camera?.stopCapture(invalidate: true)
if case .story = self.mode {
self.node.camera?.stopCapture(invalidate: true)
}
self.isDismissed = true
if animated {
self.ignoreStatusBar = true
if let layout = self.validLayout, layout.metrics.isTablet {
if let layout = self.validLayout, layout.metrics.isTablet || self.mode == .sticker {
self.node.animateOut(completion: {
self.dismiss(animated: false)
self.transitionedOut()
})
} else {
if !interactive {
@@ -2923,6 +3012,7 @@ public class CameraScreen: ViewController {
}
self.updateTransitionProgress(0.0, transition: .animated(duration: 0.4, curve: .spring), completion: { [weak self] in
self?.dismiss(animated: false)
self?.transitionedOut()
})
}
} else {