mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
1062 lines
46 KiB
Swift
1062 lines
46 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import AsyncDisplayKit
|
|
import ComponentFlow
|
|
import SwiftSignalKit
|
|
import ViewControllerComponent
|
|
import ComponentDisplayAdapters
|
|
import TelegramPresentationData
|
|
import AccountContext
|
|
import TelegramCore
|
|
import PresentationDataUtils
|
|
import Camera
|
|
import MultilineTextComponent
|
|
import BlurredBackgroundComponent
|
|
import Photos
|
|
|
|
let videoRedColor = UIColor(rgb: 0xff3b30)
|
|
|
|
enum CameraMode: Equatable {
|
|
case photo
|
|
case video
|
|
}
|
|
|
|
private struct CameraState {
|
|
enum Recording: Equatable {
|
|
case none
|
|
case holding
|
|
case handsFree
|
|
}
|
|
let mode: CameraMode
|
|
let flashMode: Camera.FlashMode
|
|
let recording: Recording
|
|
let duration: Double
|
|
|
|
func updatedMode(_ mode: CameraMode) -> CameraState {
|
|
return CameraState(mode: mode, flashMode: self.flashMode, recording: self.recording, duration: self.duration)
|
|
}
|
|
|
|
func updatedFlashMode(_ flashMode: Camera.FlashMode) -> CameraState {
|
|
return CameraState(mode: self.mode, flashMode: flashMode, recording: self.recording, duration: self.duration)
|
|
}
|
|
|
|
func updatedRecording(_ recording: Recording) -> CameraState {
|
|
return CameraState(mode: self.mode, flashMode: self.flashMode, recording: recording, duration: self.duration)
|
|
}
|
|
|
|
func updatedDuration(_ duration: Double) -> CameraState {
|
|
return CameraState(mode: self.mode, flashMode: self.flashMode, recording: self.recording, duration: duration)
|
|
}
|
|
}
|
|
|
|
enum CameraScreenTransition {
|
|
case animateIn
|
|
case animateOut
|
|
}
|
|
|
|
private let cancelButtonTag = GenericComponentViewTag()
|
|
private let flashButtonTag = GenericComponentViewTag()
|
|
private let zoomControlTag = GenericComponentViewTag()
|
|
private let captureControlsTag = GenericComponentViewTag()
|
|
private let modeControlTag = GenericComponentViewTag()
|
|
|
|
private final class CameraScreenComponent: CombinedComponent {
|
|
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
|
|
|
let context: AccountContext
|
|
let camera: Camera
|
|
let changeMode: ActionSlot<CameraMode>
|
|
let present: (ViewController) -> Void
|
|
let push: (ViewController) -> Void
|
|
let completion: ActionSlot<Signal<CameraScreen.Result, NoError>>
|
|
|
|
init(
|
|
context: AccountContext,
|
|
camera: Camera,
|
|
changeMode: ActionSlot<CameraMode>,
|
|
present: @escaping (ViewController) -> Void,
|
|
push: @escaping (ViewController) -> Void,
|
|
completion: ActionSlot<Signal<CameraScreen.Result, NoError>>
|
|
) {
|
|
self.context = context
|
|
self.camera = camera
|
|
self.changeMode = changeMode
|
|
self.present = present
|
|
self.push = push
|
|
self.completion = completion
|
|
}
|
|
|
|
static func ==(lhs: CameraScreenComponent, rhs: CameraScreenComponent) -> Bool {
|
|
if lhs.context !== rhs.context {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
final class State: ComponentState {
|
|
enum ImageKey: Hashable {
|
|
case cancel
|
|
case flip
|
|
case flash
|
|
}
|
|
private var cachedImages: [ImageKey: UIImage] = [:]
|
|
func image(_ key: ImageKey) -> UIImage {
|
|
if let image = self.cachedImages[key] {
|
|
return image
|
|
} else {
|
|
var image: UIImage
|
|
switch key {
|
|
case .cancel:
|
|
image = UIImage(bundleImageName: "Camera/CloseIcon")!
|
|
case .flip:
|
|
image = UIImage(bundleImageName: "Camera/FlipIcon")!
|
|
case .flash:
|
|
image = UIImage(bundleImageName: "Camera/FlashIcon")!
|
|
}
|
|
cachedImages[key] = image
|
|
return image
|
|
}
|
|
}
|
|
|
|
private let context: AccountContext
|
|
fileprivate let camera: Camera
|
|
private let present: (ViewController) -> Void
|
|
private let completion: ActionSlot<Signal<CameraScreen.Result, NoError>>
|
|
|
|
private var cameraStateDisposable: Disposable?
|
|
private var resultDisposable = MetaDisposable()
|
|
|
|
private var mediaAssetsContext: MediaAssetsContext
|
|
fileprivate var lastGalleryAsset: PHAsset?
|
|
private var lastGalleryAssetsDisposable: Disposable?
|
|
|
|
var cameraState = CameraState(mode: .photo, flashMode: .off, recording: .none, duration: 0.0)
|
|
var swipeHint: CaptureControlsComponent.SwipeHint = .none
|
|
|
|
init(context: AccountContext, camera: Camera, present: @escaping (ViewController) -> Void, completion: ActionSlot<Signal<CameraScreen.Result, NoError>>) {
|
|
self.context = context
|
|
self.camera = camera
|
|
self.present = present
|
|
self.completion = completion
|
|
|
|
self.mediaAssetsContext = MediaAssetsContext()
|
|
|
|
super.init()
|
|
|
|
self.cameraStateDisposable = (camera.flashMode
|
|
|> deliverOnMainQueue).start(next: { [weak self] flashMode in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.cameraState = self.cameraState.updatedFlashMode(flashMode)
|
|
self.updated(transition: .easeInOut(duration: 0.2))
|
|
})
|
|
|
|
self.lastGalleryAssetsDisposable = (self.mediaAssetsContext.recentAssets()
|
|
|> map { fetchResult in
|
|
return fetchResult?.lastObject
|
|
}
|
|
|> deliverOnMainQueue).start(next: { [weak self] asset in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.lastGalleryAsset = asset
|
|
self.updated(transition: .easeInOut(duration: 0.2))
|
|
})
|
|
}
|
|
|
|
deinit {
|
|
self.cameraStateDisposable?.dispose()
|
|
self.lastGalleryAssetsDisposable?.dispose()
|
|
self.resultDisposable.dispose()
|
|
}
|
|
|
|
func updateCameraMode(_ mode: CameraMode) {
|
|
self.cameraState = self.cameraState.updatedMode(mode)
|
|
self.updated(transition: .spring(duration: 0.3))
|
|
}
|
|
|
|
func updateSwipeHint(_ hint: CaptureControlsComponent.SwipeHint) {
|
|
self.swipeHint = hint
|
|
self.updated(transition: .easeInOut(duration: 0.2))
|
|
}
|
|
|
|
func takePhoto() {
|
|
let takePhoto = self.camera.takePhoto()
|
|
|> mapToSignal { value -> Signal<CameraScreen.Result, NoError> in
|
|
switch value {
|
|
case .began:
|
|
return .single(.pendingImage)
|
|
case let .finished(image):
|
|
return .single(.image(image))
|
|
case .failed:
|
|
return .complete()
|
|
}
|
|
}
|
|
self.completion.invoke(takePhoto)
|
|
}
|
|
|
|
func startVideoRecording(pressing: Bool) {
|
|
self.cameraState = self.cameraState.updatedDuration(0.0).updatedRecording(pressing ? .holding : .handsFree)
|
|
self.resultDisposable.set((self.camera.startRecording()
|
|
|> deliverOnMainQueue).start(next: { [weak self] duration in
|
|
if let self {
|
|
self.cameraState = self.cameraState.updatedDuration(duration)
|
|
self.updated(transition: .easeInOut(duration: 0.1))
|
|
}
|
|
}))
|
|
self.updated(transition: .spring(duration: 0.4))
|
|
}
|
|
|
|
func stopVideoRecording() {
|
|
self.cameraState = self.cameraState.updatedRecording(.none).updatedDuration(0.0)
|
|
self.resultDisposable.set((self.camera.stopRecording()
|
|
|> deliverOnMainQueue).start(next: { [weak self] path in
|
|
if let self, let path {
|
|
self.completion.invoke(.single(.video(path, PixelDimensions(width: 1080, height: 1920))))
|
|
}
|
|
}))
|
|
self.updated(transition: .spring(duration: 0.4))
|
|
}
|
|
|
|
func lockVideoRecording() {
|
|
self.cameraState = self.cameraState.updatedRecording(.handsFree)
|
|
self.updated(transition: .spring(duration: 0.4))
|
|
}
|
|
}
|
|
|
|
func makeState() -> State {
|
|
return State(context: self.context, camera: self.camera, present: self.present, completion: self.completion)
|
|
}
|
|
|
|
static var body: Body {
|
|
let cancelButton = Child(CameraButton.self)
|
|
let captureControls = Child(CaptureControlsComponent.self)
|
|
let zoomControl = Child(ZoomComponent.self)
|
|
let flashButton = Child(CameraButton.self)
|
|
let modeControl = Child(ModeComponent.self)
|
|
let hintLabel = Child(MultilineTextComponent.self)
|
|
|
|
let timeBackground = Child(RoundedRectangle.self)
|
|
let timeLabel = Child(MultilineTextComponent.self)
|
|
|
|
return { context in
|
|
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
|
|
let component = context.component
|
|
let state = context.state
|
|
let controller = environment.controller
|
|
let availableSize = context.availableSize
|
|
|
|
let accountContext = component.context
|
|
let push = component.push
|
|
let completion = component.completion
|
|
|
|
let topControlInset: CGFloat = 20.0
|
|
|
|
component.changeMode.connect({ [weak state] mode in
|
|
state?.updateCameraMode(mode)
|
|
})
|
|
|
|
if case .none = state.cameraState.recording {
|
|
let cancelButton = cancelButton.update(
|
|
component: CameraButton(
|
|
content: AnyComponent(Image(
|
|
image: state.image(.cancel),
|
|
size: CGSize(width: 40.0, height: 40.0)
|
|
)),
|
|
action: {
|
|
guard let controller = controller() as? CameraScreen else {
|
|
return
|
|
}
|
|
controller.requestDismiss(animated: true)
|
|
}
|
|
).tagged(cancelButtonTag),
|
|
availableSize: CGSize(width: 40.0, height: 40.0),
|
|
transition: .immediate
|
|
)
|
|
context.add(cancelButton
|
|
.position(CGPoint(x: topControlInset + cancelButton.size.width / 2.0, y: environment.safeInsets.top + topControlInset + cancelButton.size.height / 2.0))
|
|
.appear(.default(scale: true))
|
|
.disappear(.default(scale: true))
|
|
.cornerRadius(20.0)
|
|
)
|
|
|
|
let flashButton = flashButton.update(
|
|
component: CameraButton(
|
|
content: AnyComponent(Image(
|
|
image: state.image(.flash)
|
|
)),
|
|
action: { [weak state] in
|
|
guard let state else {
|
|
return
|
|
}
|
|
if state.cameraState.flashMode == .off {
|
|
state.camera.setFlashMode(.on)
|
|
} else {
|
|
state.camera.setFlashMode(.off)
|
|
}
|
|
}
|
|
).tagged(flashButtonTag),
|
|
availableSize: CGSize(width: 40.0, height: 40.0),
|
|
transition: .immediate
|
|
)
|
|
context.add(flashButton
|
|
.position(CGPoint(x: availableSize.width - topControlInset - flashButton.size.width / 2.0, y: environment.safeInsets.top + topControlInset + flashButton.size.height / 2.0))
|
|
.appear(.default(scale: true))
|
|
.disappear(.default(scale: true))
|
|
.cornerRadius(20.0)
|
|
)
|
|
}
|
|
|
|
if case .holding = state.cameraState.recording {
|
|
|
|
} else {
|
|
let _ = zoomControl
|
|
// let zoomControl = zoomControl.update(
|
|
// component: ZoomComponent(
|
|
// availableValues: state.camera.metrics.zoomLevels,
|
|
// value: 1.0,
|
|
// tag: zoomControlTag
|
|
// ),
|
|
// availableSize: context.availableSize,
|
|
// transition: context.transition
|
|
// )
|
|
// context.add(zoomControl
|
|
// .position(CGPoint(x: context.availableSize.width / 2.0, y: availableSize.height - zoomControl.size.height / 2.0 - 187.0 - environment.safeInsets.bottom))
|
|
// .appear(.default(alpha: true))
|
|
// .disappear(.default(alpha: true))
|
|
// )
|
|
}
|
|
|
|
let shutterState: ShutterButtonState
|
|
switch state.cameraState.recording {
|
|
case .handsFree:
|
|
shutterState = .stopRecording
|
|
case .holding:
|
|
shutterState = .holdRecording(progress: min(1.0, Float(state.cameraState.duration / 60.0)))
|
|
case .none:
|
|
switch state.cameraState.mode {
|
|
case .photo:
|
|
shutterState = .generic
|
|
case .video:
|
|
shutterState = .video
|
|
}
|
|
}
|
|
|
|
let captureControls = captureControls.update(
|
|
component: CaptureControlsComponent(
|
|
shutterState: shutterState,
|
|
lastGalleryAsset: state.lastGalleryAsset,
|
|
tag: captureControlsTag,
|
|
shutterTapped: { [weak state] in
|
|
guard let state else {
|
|
return
|
|
}
|
|
if case .none = state.cameraState.recording {
|
|
if state.cameraState.mode == .photo {
|
|
state.takePhoto()
|
|
} else if state.cameraState.mode == .video {
|
|
state.startVideoRecording(pressing: false)
|
|
}
|
|
} else {
|
|
state.stopVideoRecording()
|
|
}
|
|
},
|
|
shutterPressed: { [weak state] in
|
|
guard let state, case .none = state.cameraState.recording else {
|
|
return
|
|
}
|
|
state.startVideoRecording(pressing: true)
|
|
},
|
|
shutterReleased: { [weak state] in
|
|
guard let state, state.cameraState.recording != .none else {
|
|
return
|
|
}
|
|
state.stopVideoRecording()
|
|
},
|
|
lockRecording: { [weak state] in
|
|
guard let state, state.cameraState.recording != .none else {
|
|
return
|
|
}
|
|
state.lockVideoRecording()
|
|
},
|
|
flipTapped: { [weak state] in
|
|
guard let state else {
|
|
return
|
|
}
|
|
state.camera.togglePosition()
|
|
},
|
|
galleryTapped: {
|
|
var dismissGalleryControllerImpl: (() -> Void)?
|
|
let controller = accountContext.sharedContext.makeMediaPickerScreen(context: accountContext, completion: { asset in
|
|
dismissGalleryControllerImpl?()
|
|
completion.invoke(.single(.asset(asset)))
|
|
})
|
|
dismissGalleryControllerImpl = { [weak controller] in
|
|
controller?.dismiss(animated: true)
|
|
}
|
|
push(controller)
|
|
},
|
|
swipeHintUpdated: { hint in
|
|
state.updateSwipeHint(hint)
|
|
}
|
|
),
|
|
availableSize: availableSize,
|
|
transition: context.transition
|
|
)
|
|
context.add(captureControls
|
|
.position(CGPoint(x: availableSize.width / 2.0, y: availableSize.height - captureControls.size.height / 2.0 - environment.safeInsets.bottom - 5.0))
|
|
)
|
|
|
|
var isVideoRecording = false
|
|
if case .video = state.cameraState.mode {
|
|
isVideoRecording = true
|
|
} else if state.cameraState.recording != .none {
|
|
isVideoRecording = true
|
|
}
|
|
|
|
if isVideoRecording {
|
|
let duration = Int(state.cameraState.duration)
|
|
let durationString = String(format: "%02d:%02d", (duration / 60) % 60, duration % 60)
|
|
let timeLabel = timeLabel.update(
|
|
component: MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: durationString, font: Font.with(size: 21.0, design: .camera), textColor: .white)),
|
|
horizontalAlignment: .center,
|
|
textShadowColor: UIColor(rgb: 0x000000, alpha: 0.2)
|
|
),
|
|
availableSize: context.availableSize,
|
|
transition: context.transition
|
|
)
|
|
|
|
if state.cameraState.recording != .none {
|
|
let timeBackground = timeBackground.update(
|
|
component: RoundedRectangle(color: videoRedColor, cornerRadius: 4.0),
|
|
availableSize: CGSize(width: timeLabel.size.width + 8.0, height: 28.0),
|
|
transition: context.transition
|
|
)
|
|
context.add(timeBackground
|
|
.position(CGPoint(x: availableSize.width / 2.0, y: environment.safeInsets.top + 40.0))
|
|
.appear(.default(alpha: true))
|
|
.disappear(.default(alpha: true))
|
|
)
|
|
}
|
|
|
|
context.add(timeLabel
|
|
.position(CGPoint(x: availableSize.width / 2.0, y: environment.safeInsets.top + 40.0))
|
|
.appear(.default(alpha: true))
|
|
.disappear(.default(alpha: true))
|
|
)
|
|
|
|
if case .holding = state.cameraState.recording {
|
|
let hintText: String?
|
|
switch state.swipeHint {
|
|
case .none:
|
|
hintText = nil
|
|
case .zoom:
|
|
hintText = "Swipe up to zoom"
|
|
case .lock:
|
|
hintText = "Swipe left to lock"
|
|
case .releaseLock:
|
|
hintText = "Release to lock"
|
|
case .flip:
|
|
hintText = "Swipe right to flip"
|
|
}
|
|
if let hintText {
|
|
let hintLabel = hintLabel.update(
|
|
component: MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: hintText.uppercased(), font: Font.with(size: 14.0, design: .camera, weight: .semibold), textColor: .white)),
|
|
horizontalAlignment: .center
|
|
),
|
|
availableSize: availableSize,
|
|
transition: .immediate
|
|
)
|
|
context.add(hintLabel
|
|
.position(CGPoint(x: availableSize.width / 2.0, y: availableSize.height - environment.safeInsets.bottom + 14.0 + hintLabel.size.height / 2.0))
|
|
.appear(.default(alpha: true))
|
|
.disappear(.default(alpha: true))
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
if case .none = state.cameraState.recording {
|
|
let modeControl = modeControl.update(
|
|
component: ModeComponent(
|
|
availableModes: [.photo, .video],
|
|
currentMode: state.cameraState.mode,
|
|
updatedMode: { [weak state] mode in
|
|
if let state {
|
|
state.updateCameraMode(mode)
|
|
}
|
|
},
|
|
tag: modeControlTag
|
|
),
|
|
availableSize: availableSize,
|
|
transition: context.transition
|
|
)
|
|
context.add(modeControl
|
|
.clipsToBounds(true)
|
|
.position(CGPoint(x: availableSize.width / 2.0, y: availableSize.height - environment.safeInsets.bottom + modeControl.size.height / 2.0))
|
|
.appear(.default(alpha: true))
|
|
.disappear(.default(alpha: true))
|
|
)
|
|
}
|
|
return availableSize
|
|
}
|
|
}
|
|
}
|
|
|
|
private let useSimplePreviewView = true
|
|
|
|
private class BlurView: UIVisualEffectView {
|
|
private func setup() {
|
|
for subview in self.subviews {
|
|
if subview.description.contains("VisualEffectSubview") {
|
|
subview.isHidden = true
|
|
}
|
|
}
|
|
|
|
if let sublayer = self.layer.sublayers?[0], let filters = sublayer.filters {
|
|
sublayer.backgroundColor = nil
|
|
sublayer.isOpaque = false
|
|
let allowedKeys: [String] = [
|
|
"gaussianBlur"
|
|
]
|
|
sublayer.filters = filters.filter { filter in
|
|
guard let filter = filter as? NSObject else {
|
|
return true
|
|
}
|
|
let filterName = String(describing: filter)
|
|
if !allowedKeys.contains(filterName) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
override var effect: UIVisualEffect? {
|
|
get {
|
|
return super.effect
|
|
}
|
|
set {
|
|
super.effect = newValue
|
|
self.setup()
|
|
}
|
|
}
|
|
|
|
override func didAddSubview(_ subview: UIView) {
|
|
super.didAddSubview(subview)
|
|
self.setup()
|
|
}
|
|
}
|
|
|
|
public class CameraScreen: ViewController {
|
|
public enum Mode {
|
|
case generic
|
|
case story
|
|
case instantVideo
|
|
}
|
|
|
|
public enum Result {
|
|
case pendingImage
|
|
case image(UIImage)
|
|
case video(String, PixelDimensions)
|
|
case asset(PHAsset)
|
|
}
|
|
|
|
fileprivate final class Node: ViewControllerTracingNode {
|
|
private weak var controller: CameraScreen?
|
|
private let context: AccountContext
|
|
private let updateState: ActionSlot<CameraState>
|
|
|
|
private let backgroundEffectView: UIVisualEffectView
|
|
private let backgroundDimView: UIView
|
|
fileprivate let componentHost: ComponentView<ViewControllerComponentContainer.Environment>
|
|
private let previewContainerView: UIView
|
|
fileprivate let previewView: CameraPreviewView?
|
|
fileprivate let simplePreviewView: CameraSimplePreviewView?
|
|
fileprivate let previewBlurView: BlurView
|
|
fileprivate let camera: Camera
|
|
|
|
private var presentationData: PresentationData
|
|
private var validLayout: ContainerViewLayout?
|
|
|
|
private var changingPositionDisposable: Disposable?
|
|
|
|
private let changeMode = ActionSlot<CameraMode>()
|
|
private let completion = ActionSlot<Signal<CameraScreen.Result, NoError>>()
|
|
|
|
private var effectivePreviewView: UIView {
|
|
if let simplePreviewView = self.simplePreviewView {
|
|
return simplePreviewView
|
|
} else if let previewView = self.previewView {
|
|
return previewView
|
|
} else {
|
|
fatalError()
|
|
}
|
|
}
|
|
|
|
private var previewBlurPromise = ValuePromise<Bool>(false)
|
|
|
|
init(controller: CameraScreen) {
|
|
self.controller = controller
|
|
self.context = controller.context
|
|
self.updateState = ActionSlot<CameraState>()
|
|
|
|
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
|
|
|
self.backgroundEffectView = UIVisualEffectView(effect: nil)
|
|
self.backgroundDimView = UIView()
|
|
self.backgroundDimView.backgroundColor = UIColor(rgb: 0x000000)
|
|
|
|
self.componentHost = ComponentView<ViewControllerComponentContainer.Environment>()
|
|
|
|
self.previewContainerView = UIView()
|
|
self.previewContainerView.clipsToBounds = true
|
|
self.previewContainerView.layer.cornerRadius = 12.0
|
|
if #available(iOS 13.0, *) {
|
|
self.previewContainerView.layer.cornerCurve = .continuous
|
|
}
|
|
|
|
self.previewBlurView = BlurView()
|
|
self.previewBlurView.isUserInteractionEnabled = false
|
|
|
|
if let holder = controller.holder {
|
|
self.simplePreviewView = nil
|
|
self.previewView = holder.previewView
|
|
self.camera = holder.camera
|
|
} else {
|
|
if useSimplePreviewView {
|
|
self.simplePreviewView = CameraSimplePreviewView()
|
|
self.previewView = nil
|
|
} else {
|
|
self.previewView = CameraPreviewView(test: false)!
|
|
self.simplePreviewView = nil
|
|
}
|
|
self.camera = Camera(configuration: Camera.Configuration(preset: .hd1920x1080, position: .back, audio: true, photo: true, metadata: false, preferredFps: 60.0), previewView: self.simplePreviewView)
|
|
if !useSimplePreviewView {
|
|
self.camera.attachPreviewView(self.previewView!)
|
|
}
|
|
}
|
|
|
|
super.init()
|
|
|
|
self.backgroundColor = .clear
|
|
|
|
self.view.addSubview(self.backgroundEffectView)
|
|
self.view.addSubview(self.backgroundDimView)
|
|
|
|
self.view.addSubview(self.previewContainerView)
|
|
self.previewContainerView.addSubview(self.effectivePreviewView)
|
|
self.previewContainerView.addSubview(self.previewBlurView)
|
|
|
|
self.changingPositionDisposable = combineLatest(
|
|
queue: Queue.mainQueue(),
|
|
self.camera.changingPosition,
|
|
self.previewBlurPromise.get()
|
|
).start(next: { [weak self] changingPosition, forceBlur in
|
|
if let self {
|
|
if changingPosition {
|
|
UIView.transition(with: self.previewContainerView, duration: 0.4, options: [.transitionFlipFromLeft, .curveEaseOut], animations: {
|
|
self.previewBlurView.effect = UIBlurEffect(style: .dark)
|
|
})
|
|
} else if forceBlur {
|
|
self.previewBlurView.effect = UIBlurEffect(style: .dark)
|
|
} else {
|
|
UIView.animate(withDuration: 0.4) {
|
|
self.previewBlurView.effect = nil
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
self.completion.connect { [weak self] result in
|
|
if let self {
|
|
self.animateOutToEditor()
|
|
self.controller?.completion(
|
|
result
|
|
|> beforeNext { [weak self] value in
|
|
guard let self else {
|
|
return
|
|
}
|
|
if case .pendingImage = value {
|
|
Queue.mainQueue().async {
|
|
self.effectivePreviewView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
|
self.simplePreviewView?.isEnabled = false
|
|
}
|
|
} else {
|
|
Queue.mainQueue().async {
|
|
if case .image = value {
|
|
Queue.mainQueue().after(0.3) {
|
|
self.previewBlurPromise.set(true)
|
|
}
|
|
}
|
|
self.simplePreviewView?.isEnabled = false
|
|
self.camera.stopCapture()
|
|
}
|
|
}
|
|
}
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
deinit {
|
|
self.changingPositionDisposable?.dispose()
|
|
}
|
|
|
|
override func didLoad() {
|
|
super.didLoad()
|
|
|
|
self.view.disablesInteractiveModalDismiss = true
|
|
self.view.disablesInteractiveKeyboardGestureRecognizer = true
|
|
|
|
let pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.handlePinch(_:)))
|
|
self.effectivePreviewView.addGestureRecognizer(pinchGestureRecognizer)
|
|
|
|
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
|
|
self.effectivePreviewView.addGestureRecognizer(panGestureRecognizer)
|
|
|
|
self.camera.focus(at: CGPoint(x: 0.5, y: 0.5))
|
|
self.camera.startCapture()
|
|
}
|
|
|
|
@objc private func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) {
|
|
switch gestureRecognizer.state {
|
|
case .began:
|
|
gestureRecognizer.scale = 1.0
|
|
case .changed:
|
|
let scale = gestureRecognizer.scale
|
|
self.camera.setZoomLevel(scale)
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
private var panTranslation: CGFloat?
|
|
private var previewInitialPosition: CGPoint?
|
|
private var controlsInitialPosition: CGPoint?
|
|
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
|
switch gestureRecognizer.state {
|
|
case .began:
|
|
self.panTranslation = nil
|
|
self.previewInitialPosition = self.previewContainerView.center
|
|
self.controlsInitialPosition = self.componentHost.view?.center
|
|
case .changed:
|
|
let translation = gestureRecognizer.translation(in: gestureRecognizer.view)
|
|
if !"".isEmpty {
|
|
|
|
} else {
|
|
if abs(translation.x) > 50.0 && abs(translation.y) < 50.0, self.panTranslation == nil {
|
|
self.changeMode.invoke(translation.x > 0.0 ? .photo : .video)
|
|
gestureRecognizer.isEnabled = false
|
|
gestureRecognizer.isEnabled = true
|
|
} else if translation.y > 10.0 {
|
|
self.panTranslation = translation.y
|
|
if let previewInitialPosition = self.previewInitialPosition {
|
|
self.previewContainerView.center = CGPoint(x: previewInitialPosition.x, y: previewInitialPosition.y + translation.y)
|
|
}
|
|
if let controlsInitialPosition = self.controlsInitialPosition, let view = self.componentHost.view {
|
|
view.center = CGPoint(x: controlsInitialPosition.x, y: controlsInitialPosition.y + translation.y)
|
|
}
|
|
|
|
if self.backgroundEffectView.isHidden {
|
|
self.backgroundEffectView.isHidden = false
|
|
|
|
UIView.animate(withDuration: 0.25, animations: {
|
|
self.backgroundEffectView.effect = nil
|
|
self.backgroundDimView.alpha = 0.0
|
|
})
|
|
}
|
|
}
|
|
}
|
|
case .ended:
|
|
let velocity = gestureRecognizer.velocity(in: self.view)
|
|
if velocity.y > 1000.0 {
|
|
self.controller?.requestDismiss(animated: true)
|
|
} else if let panTranslation = self.panTranslation, abs(panTranslation) > 300.0 {
|
|
self.controller?.requestDismiss(animated: true)
|
|
} else {
|
|
let transition = Transition(animation: .curve(duration: 0.3, curve: .spring))
|
|
if let previewInitialPosition = self.previewInitialPosition {
|
|
transition.setPosition(view: self.previewContainerView, position: previewInitialPosition)
|
|
}
|
|
if let controlsInitialPosition = self.controlsInitialPosition, let view = self.componentHost.view {
|
|
transition.setPosition(view: view, position: controlsInitialPosition)
|
|
}
|
|
if !self.backgroundEffectView.isHidden {
|
|
UIView.animate(withDuration: 0.25, animations: {
|
|
self.backgroundEffectView.effect = UIBlurEffect(style: .dark)
|
|
self.backgroundDimView.alpha = 1.0
|
|
}, completion: { _ in
|
|
self.backgroundEffectView.isHidden = true
|
|
})
|
|
}
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
func animateIn() {
|
|
guard let layout = self.validLayout else {
|
|
return
|
|
}
|
|
self.backgroundDimView.alpha = 0.0
|
|
UIView.animate(withDuration: 0.4, animations: {
|
|
self.backgroundEffectView.effect = UIBlurEffect(style: .dark)
|
|
self.backgroundDimView.alpha = 1.0
|
|
|
|
}, completion: { _ in
|
|
self.backgroundEffectView.isHidden = true
|
|
})
|
|
|
|
self.previewContainerView.layer.animatePosition(from: CGPoint(x: 0.0, y: layout.size.height / 2.0 - layout.intrinsicInsets.bottom - 22.0), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
|
self.componentHost.view?.layer.animatePosition(from: CGPoint(x: 0.0, y: layout.size.height / 2.0 - layout.intrinsicInsets.bottom - 22.0), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
|
self.previewContainerView.layer.animateScale(from: 0.2, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
|
|
self.componentHost.view?.layer.animateScale(from: 0.2, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
|
|
}
|
|
|
|
func animateOut(completion: @escaping () -> Void) {
|
|
guard let layout = self.validLayout else {
|
|
return
|
|
}
|
|
self.backgroundEffectView.isHidden = false
|
|
|
|
UIView.animate(withDuration: 0.25, animations: {
|
|
self.backgroundEffectView.effect = nil
|
|
self.backgroundDimView.alpha = 0.0
|
|
})
|
|
|
|
self.previewContainerView.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: layout.size.height / 2.0 - layout.intrinsicInsets.bottom - 8.0), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
|
|
self.componentHost.view?.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: layout.size.height / 2.0 - layout.intrinsicInsets.bottom - 8.0), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { _ in
|
|
completion()
|
|
})
|
|
self.previewContainerView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
|
self.previewContainerView.layer.animateBounds(from: self.previewContainerView.bounds, to: CGRect(origin: .zero, size: CGSize(width: self.previewContainerView.bounds.width, height: self.previewContainerView.bounds.width)), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
|
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
|
|
transition.updateCornerRadius(layer: self.previewContainerView.layer, cornerRadius: self.previewContainerView.bounds.width / 2.0)
|
|
self.componentHost.view?.layer.animateScale(from: 1.0, to: 0.2, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
|
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)
|
|
}
|
|
|
|
func animateOutToEditor() {
|
|
let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
|
|
if let view = self.componentHost.findTaggedView(tag: cancelButtonTag) {
|
|
transition.setScale(view: view, scale: 0.1)
|
|
transition.setAlpha(view: view, alpha: 0.0)
|
|
}
|
|
if let view = self.componentHost.findTaggedView(tag: flashButtonTag) {
|
|
transition.setScale(view: view, scale: 0.1)
|
|
transition.setAlpha(view: view, alpha: 0.0)
|
|
}
|
|
if let view = self.componentHost.findTaggedView(tag: zoomControlTag) {
|
|
transition.setAlpha(view: view, alpha: 0.0)
|
|
}
|
|
if let view = self.componentHost.findTaggedView(tag: captureControlsTag) as? CaptureControlsComponent.View {
|
|
view.animateOutToEditor(transition: transition)
|
|
}
|
|
if let view = self.componentHost.findTaggedView(tag: modeControlTag) as? ModeComponent.View {
|
|
view.animateOutToEditor(transition: transition)
|
|
}
|
|
}
|
|
|
|
func animateInFromEditor() {
|
|
self.simplePreviewView?.isEnabled = true
|
|
self.camera.startCapture()
|
|
|
|
if #available(iOS 13.0, *), let isPreviewing = self.simplePreviewView?.isPreviewing {
|
|
let _ = (isPreviewing
|
|
|> filter {
|
|
$0
|
|
}
|
|
|> take(1)).start(next: { [weak self] _ in
|
|
if let self {
|
|
self.previewBlurPromise.set(false)
|
|
}
|
|
})
|
|
} else {
|
|
Queue.mainQueue().after(1.0) {
|
|
self.previewBlurPromise.set(false)
|
|
}
|
|
}
|
|
|
|
let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
|
|
if let view = self.componentHost.findTaggedView(tag: cancelButtonTag) {
|
|
transition.setScale(view: view, scale: 1.0)
|
|
transition.setAlpha(view: view, alpha: 1.0)
|
|
}
|
|
if let view = self.componentHost.findTaggedView(tag: flashButtonTag) {
|
|
transition.setScale(view: view, scale: 1.0)
|
|
transition.setAlpha(view: view, alpha: 1.0)
|
|
}
|
|
if let view = self.componentHost.findTaggedView(tag: zoomControlTag) {
|
|
transition.setScale(view: view, scale: 1.0)
|
|
transition.setAlpha(view: view, alpha: 1.0)
|
|
}
|
|
if let view = self.componentHost.findTaggedView(tag: captureControlsTag) as? CaptureControlsComponent.View {
|
|
view.animateInFromEditor(transition: transition)
|
|
}
|
|
if let view = self.componentHost.findTaggedView(tag: modeControlTag) as? ModeComponent.View {
|
|
view.animateInFromEditor(transition: transition)
|
|
}
|
|
}
|
|
|
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
let result = super.hitTest(point, with: event)
|
|
if result == self.componentHost.view {
|
|
return self.effectivePreviewView
|
|
}
|
|
return result
|
|
}
|
|
|
|
func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: Transition) {
|
|
guard let _ = self.controller else {
|
|
return
|
|
}
|
|
let isFirstTime = self.validLayout == nil
|
|
self.validLayout = layout
|
|
|
|
let previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778))
|
|
let topInset: CGFloat = floor(layout.size.height - previewSize.height) / 2.0
|
|
|
|
let environment = ViewControllerComponentContainer.Environment(
|
|
statusBarHeight: layout.statusBarHeight ?? 0.0,
|
|
navigationHeight: 0.0,
|
|
safeInsets: UIEdgeInsets(
|
|
top: topInset,
|
|
left: layout.safeInsets.left,
|
|
bottom: topInset,
|
|
right: layout.safeInsets.right
|
|
),
|
|
inputHeight: layout.inputHeight ?? 0.0,
|
|
metrics: layout.metrics,
|
|
deviceMetrics: layout.deviceMetrics,
|
|
orientation: nil,
|
|
isVisible: true,
|
|
theme: self.presentationData.theme,
|
|
strings: self.presentationData.strings,
|
|
dateTimeFormat: self.presentationData.dateTimeFormat,
|
|
controller: { [weak self] in
|
|
return self?.controller
|
|
}
|
|
)
|
|
|
|
var transition = transition
|
|
if isFirstTime {
|
|
transition = transition.withUserData(CameraScreenTransition.animateIn)
|
|
} else if animateOut {
|
|
transition = transition.withUserData(CameraScreenTransition.animateOut)
|
|
}
|
|
|
|
let componentSize = self.componentHost.update(
|
|
transition: transition,
|
|
component: AnyComponent(
|
|
CameraScreenComponent(
|
|
context: self.context,
|
|
camera: self.camera,
|
|
changeMode: self.changeMode,
|
|
present: { [weak self] c in
|
|
self?.controller?.present(c, in: .window(.root))
|
|
},
|
|
push: { [weak self] c in
|
|
self?.controller?.push(c)
|
|
},
|
|
completion: self.completion
|
|
)
|
|
),
|
|
environment: {
|
|
environment
|
|
},
|
|
forceUpdate: forceUpdate || animateOut,
|
|
containerSize: layout.size
|
|
)
|
|
if let componentView = self.componentHost.view {
|
|
if componentView.superview == nil {
|
|
self.view.insertSubview(componentView, at: 3)
|
|
componentView.clipsToBounds = true
|
|
}
|
|
|
|
let componentFrame = CGRect(origin: .zero, size: componentSize)
|
|
transition.setFrame(view: componentView, frame: CGRect(origin: componentFrame.origin, size: CGSize(width: componentFrame.width, height: componentFrame.height)))
|
|
|
|
if isFirstTime {
|
|
self.animateIn()
|
|
}
|
|
}
|
|
|
|
transition.setFrame(view: self.backgroundDimView, frame: CGRect(origin: .zero, size: layout.size))
|
|
transition.setFrame(view: self.backgroundEffectView, frame: CGRect(origin: .zero, size: layout.size))
|
|
|
|
let previewFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: previewSize)
|
|
transition.setFrame(view: self.previewContainerView, frame: previewFrame)
|
|
transition.setFrame(view: self.effectivePreviewView, frame: CGRect(origin: .zero, size: previewFrame.size))
|
|
transition.setFrame(view: self.previewBlurView, frame: CGRect(origin: .zero, size: previewFrame.size))
|
|
}
|
|
}
|
|
|
|
fileprivate var node: Node {
|
|
return self.displayNode as! Node
|
|
}
|
|
|
|
private let context: AccountContext
|
|
fileprivate let mode: Mode
|
|
fileprivate let holder: CameraHolder?
|
|
fileprivate let completion: (Signal<CameraScreen.Result, NoError>) -> Void
|
|
|
|
public init(context: AccountContext, mode: Mode, holder: CameraHolder? = nil, completion: @escaping (Signal<CameraScreen.Result, NoError>) -> Void) {
|
|
self.context = context
|
|
self.mode = mode
|
|
self.holder = holder
|
|
self.completion = completion
|
|
|
|
super.init(navigationBarPresentationData: nil)
|
|
|
|
self.statusBar.statusBarStyle = .White
|
|
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
|
|
|
self.navigationPresentation = .flatModal
|
|
}
|
|
|
|
required public init(coder: NSCoder) {
|
|
preconditionFailure()
|
|
}
|
|
|
|
override public func loadDisplayNode() {
|
|
self.displayNode = Node(controller: self)
|
|
|
|
super.displayNodeDidLoad()
|
|
}
|
|
|
|
public func returnFromEditor() {
|
|
self.node.animateInFromEditor()
|
|
}
|
|
|
|
private var isDismissed = false
|
|
fileprivate func requestDismiss(animated: Bool) {
|
|
guard !self.isDismissed else {
|
|
return
|
|
}
|
|
self.isDismissed = true
|
|
self.statusBar.statusBarStyle = .Ignore
|
|
if animated {
|
|
self.node.animateOut(completion: {
|
|
self.dismiss(animated: false)
|
|
})
|
|
} else {
|
|
self.dismiss(animated: false)
|
|
}
|
|
}
|
|
|
|
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
|
super.containerLayoutUpdated(layout, transition: transition)
|
|
|
|
if !self.isDismissed {
|
|
(self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: Transition(transition))
|
|
}
|
|
}
|
|
}
|