mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
1604 lines
64 KiB
Swift
1604 lines
64 KiB
Swift
import Foundation
|
|
import AVFoundation
|
|
import UIKit
|
|
import Display
|
|
import ComponentFlow
|
|
import SwiftSignalKit
|
|
import Camera
|
|
import ContextUI
|
|
import AccountContext
|
|
import MetalEngine
|
|
import TelegramPresentationData
|
|
import Photos
|
|
|
|
final class CameraCollage {
|
|
final class CaptureResult {
|
|
enum Content {
|
|
case pending(CameraCollage.State.Item.Content.Placeholder?)
|
|
case image(UIImage)
|
|
case video(asset: AVAsset, thumbnail: UIImage?, duration: Double, source: CameraCollage.State.Item.Content.VideoSource)
|
|
case failed
|
|
}
|
|
|
|
private var internalContent: Content
|
|
private var disposable: Disposable?
|
|
|
|
init(result: Signal<CameraScreenImpl.Result, NoError>, snapshotView: UIView?, contentUpdated: @escaping () -> Void) {
|
|
self.internalContent = .pending(snapshotView.flatMap { .view($0) })
|
|
self.disposable = (result
|
|
|> deliverOnMainQueue).start(next: { [weak self] value in
|
|
guard let self else {
|
|
return
|
|
}
|
|
switch value {
|
|
case .pendingImage:
|
|
contentUpdated()
|
|
case let .image(image):
|
|
self.internalContent = .image(image.image)
|
|
contentUpdated()
|
|
case let .video(video):
|
|
self.internalContent = .pending(video.coverImage.flatMap { .image($0) })
|
|
contentUpdated()
|
|
Queue.mainQueue().after(0.05, {
|
|
let asset = AVURLAsset(url: URL(fileURLWithPath: video.videoPath))
|
|
self.internalContent = .video(asset: asset, thumbnail: video.coverImage, duration: video.duration, source: .file(video.videoPath))
|
|
contentUpdated()
|
|
})
|
|
case let .asset(asset):
|
|
let targetSize = CGSize(width: 256.0, height: 256.0)
|
|
let options = PHImageRequestOptions()
|
|
let deliveryMode: PHImageRequestOptionsDeliveryMode = .highQualityFormat
|
|
options.deliveryMode = deliveryMode
|
|
options.isNetworkAccessAllowed = true
|
|
|
|
PHImageManager.default().requestImage(
|
|
for: asset,
|
|
targetSize: targetSize,
|
|
contentMode: .aspectFit,
|
|
options: options,
|
|
resultHandler: { [weak self] image, info in
|
|
if let image, let self {
|
|
if let info {
|
|
if let cancelled = info[PHImageCancelledKey] as? Bool, cancelled {
|
|
return
|
|
}
|
|
}
|
|
self.internalContent = .pending(.image(image))
|
|
contentUpdated()
|
|
|
|
PHImageManager.default().requestAVAsset(forVideo: asset, options: nil, resultHandler: { [weak self] avAsset, _, _ in
|
|
if let avAsset, let self {
|
|
Queue.mainQueue().async {
|
|
self.internalContent = .video(asset: avAsset, thumbnail: nil, duration: 0.0, source: .asset(asset))
|
|
contentUpdated()
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
)
|
|
default:
|
|
break
|
|
}
|
|
})
|
|
}
|
|
|
|
deinit {
|
|
self.disposable?.dispose()
|
|
}
|
|
|
|
var content: State.Item.Content? {
|
|
switch self.internalContent {
|
|
case let .pending(placeholder):
|
|
return .pending(placeholder)
|
|
case let .image(image):
|
|
return .image(image)
|
|
case let .video(asset, thumbnail, duration, source):
|
|
return .video(asset, thumbnail, duration, source)
|
|
case .failed:
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
struct State {
|
|
struct Item {
|
|
enum Content {
|
|
enum VideoSource {
|
|
case file(String)
|
|
case asset(PHAsset)
|
|
}
|
|
|
|
enum Placeholder {
|
|
case view(UIView)
|
|
case image(UIImage)
|
|
}
|
|
|
|
case empty
|
|
case camera
|
|
case pending(Placeholder?)
|
|
case image(UIImage)
|
|
case video(AVAsset, UIImage?, Double, VideoSource)
|
|
}
|
|
|
|
var uniqueId: Int64
|
|
var content: Content
|
|
|
|
var isReady: Bool {
|
|
switch self.content {
|
|
case .image, .video:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
var isAlmostReady: Bool {
|
|
switch self.content {
|
|
case .image, .video, .pending:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Row {
|
|
let items: [Item]
|
|
}
|
|
|
|
var grid: Camera.CollageGrid
|
|
var progress: Float
|
|
var innerProgress: Float
|
|
var rows: [Row]
|
|
}
|
|
private var _state: State
|
|
private var _statePromise = Promise<State>()
|
|
var state: Signal<State, NoError> {
|
|
return self._statePromise.get()
|
|
}
|
|
|
|
var grid: Camera.CollageGrid {
|
|
didSet {
|
|
if self.grid != oldValue {
|
|
self._state.grid = self.grid
|
|
if let cameraIndex = self.cameraIndex, cameraIndex > self.grid.count - 1 {
|
|
self.cameraIndex = self.grid.count - 1
|
|
}
|
|
self.updateState()
|
|
}
|
|
}
|
|
}
|
|
var results: [CaptureResult]
|
|
var uniqueIds: [Int64]
|
|
|
|
private(set) var cameraIndex: Int?
|
|
|
|
init(grid: Camera.CollageGrid) {
|
|
self.grid = grid
|
|
self.results = []
|
|
self.uniqueIds = (0 ..< 6).map { _ in Int64.random(in: .min ... .max) }
|
|
|
|
self._state = State(
|
|
grid: grid,
|
|
progress: 0.0,
|
|
innerProgress: 0.0,
|
|
rows: CameraCollage.computeRows(grid: grid, results: [], uniqueIds: self.uniqueIds, cameraIndex: self.cameraIndex)
|
|
)
|
|
self.updateState()
|
|
}
|
|
|
|
func addResult(_ signal: Signal<CameraScreenImpl.Result, NoError>, snapshotView: UIView?) {
|
|
guard self.results.count < self.grid.count else {
|
|
return
|
|
}
|
|
let result = CaptureResult(result: signal, snapshotView: snapshotView, contentUpdated: { [weak self] in
|
|
self?.checkResults()
|
|
self?.updateState()
|
|
})
|
|
if let cameraIndex = self.cameraIndex {
|
|
self.cameraIndex = nil
|
|
self.results.insert(result, at: cameraIndex)
|
|
} else {
|
|
self.results.append(result)
|
|
}
|
|
self.updateState()
|
|
}
|
|
|
|
func addResults(signals: [Signal<CameraScreenImpl.Result, NoError>]) {
|
|
guard self.results.count < self.grid.count else {
|
|
return
|
|
}
|
|
self.results.append(contentsOf: signals.map {
|
|
CaptureResult(result: $0, snapshotView: nil, contentUpdated: { [weak self] in
|
|
self?.checkResults()
|
|
self?.updateState()
|
|
})
|
|
})
|
|
self.updateState()
|
|
}
|
|
|
|
func moveItem(fromId: Int64, toId: Int64) {
|
|
guard let fromIndex = self.uniqueIds.firstIndex(where: { $0 == fromId }), let toIndex = self.uniqueIds.firstIndex(where: { $0 == toId }), toIndex < self.results.count else {
|
|
return
|
|
}
|
|
let fromItem = self.results[fromIndex]
|
|
let toItem = self.results[toIndex]
|
|
self.results[fromIndex] = toItem
|
|
self.uniqueIds[fromIndex] = toId
|
|
self.results[toIndex] = fromItem
|
|
self.uniqueIds[toIndex] = fromId
|
|
self.updateState()
|
|
}
|
|
|
|
func retakeItem(id: Int64) {
|
|
guard let index = self.uniqueIds.firstIndex(where: { $0 == id }) else {
|
|
return
|
|
}
|
|
self.cameraIndex = index
|
|
|
|
self.results.remove(at: index)
|
|
self.updateState()
|
|
}
|
|
|
|
func deleteItem(id: Int64) {
|
|
guard let index = self.uniqueIds.firstIndex(where: { $0 == id }) else {
|
|
return
|
|
}
|
|
self.results.remove(at: index)
|
|
self.uniqueIds.removeAll(where: { $0 == id })
|
|
self.uniqueIds.append(Int64.random(in: .min ... .max))
|
|
}
|
|
|
|
func getItem(id: Int64) -> CaptureResult? {
|
|
guard let index = self.uniqueIds.firstIndex(where: { $0 == id }) else {
|
|
return nil
|
|
}
|
|
return self.results[index]
|
|
}
|
|
|
|
private func checkResults() {
|
|
self.results = self.results.filter { $0.content != nil }
|
|
}
|
|
|
|
private static func computeRows(grid: Camera.CollageGrid, results: [CaptureResult], uniqueIds: [Int64], cameraIndex: Int?) -> [State.Row] {
|
|
var rows: [State.Row] = []
|
|
var index = 0
|
|
var contentIndex = 0
|
|
var addedCamera = false
|
|
for row in grid.rows {
|
|
var items: [State.Item] = []
|
|
for _ in 0 ..< row.columns {
|
|
if index == cameraIndex {
|
|
items.append(State.Item(uniqueId: uniqueIds[index], content: .camera))
|
|
addedCamera = true
|
|
contentIndex -= 1
|
|
} else if contentIndex < results.count {
|
|
if let content = results[contentIndex].content {
|
|
items.append(State.Item(uniqueId: uniqueIds[index], content: content))
|
|
} else {
|
|
items.append(State.Item(uniqueId: uniqueIds[index], content: .empty))
|
|
}
|
|
} else if index == results.count && !addedCamera {
|
|
items.append(State.Item(uniqueId: uniqueIds[index], content: .camera))
|
|
} else {
|
|
items.append(State.Item(uniqueId: uniqueIds[index], content: .empty))
|
|
}
|
|
index += 1
|
|
contentIndex += 1
|
|
}
|
|
rows.append(State.Row(items: items))
|
|
}
|
|
return rows
|
|
}
|
|
|
|
private static func computeProgress(rows: [State.Row], inner: Bool) -> Float {
|
|
var readyCount: Int = 0
|
|
var totalCount: Int = 0
|
|
for row in rows {
|
|
for item in row.items {
|
|
if inner {
|
|
if item.isAlmostReady {
|
|
readyCount += 1
|
|
}
|
|
} else {
|
|
if item.isReady {
|
|
readyCount += 1
|
|
}
|
|
}
|
|
totalCount += 1
|
|
}
|
|
}
|
|
guard totalCount > 0 else {
|
|
return 0.0
|
|
}
|
|
return Float(readyCount) / Float(totalCount)
|
|
}
|
|
|
|
private func updateState() {
|
|
self._state.rows = CameraCollage.computeRows(grid: self._state.grid, results: self.results, uniqueIds: self.uniqueIds, cameraIndex: self.cameraIndex)
|
|
self._state.progress = CameraCollage.computeProgress(rows: self._state.rows, inner: false)
|
|
self._state.innerProgress = CameraCollage.computeProgress(rows: self._state.rows, inner: true)
|
|
self._statePromise.set(.single(self._state))
|
|
}
|
|
|
|
var isComplete: Bool {
|
|
return self._state.progress > 1.0 - .ulpOfOne
|
|
}
|
|
|
|
func result(itemViews: [Int64 : CameraCollageView.ItemView]) -> Signal<CameraScreenImpl.Result, NoError> {
|
|
guard self.isComplete else {
|
|
return .complete()
|
|
}
|
|
|
|
var hasVideo = false
|
|
let state = self._state
|
|
|
|
outer: for row in state.rows {
|
|
for item in row.items {
|
|
if case .video = item.content {
|
|
hasVideo = true
|
|
break outer
|
|
}
|
|
}
|
|
}
|
|
|
|
let size = CGSize(width: 1080.0, height: 1920.0)
|
|
let rowHeight: CGFloat = ceil(size.height / CGFloat(state.rows.count))
|
|
|
|
if hasVideo {
|
|
var items: [CameraScreenImpl.Result.VideoCollage.Item] = []
|
|
var itemFrame: CGRect = .zero
|
|
for row in state.rows {
|
|
let columnWidth: CGFloat = floor(size.width / CGFloat(row.items.count))
|
|
itemFrame = CGRect(origin: itemFrame.origin, size: CGSize(width: columnWidth, height: rowHeight))
|
|
for item in row.items {
|
|
let scale = itemViews[item.uniqueId]?.contentScale ?? 1.0
|
|
let offset = itemViews[item.uniqueId]?.contentOffset ?? .zero
|
|
|
|
let content: CameraScreenImpl.Result.VideoCollage.Item.Content
|
|
switch item.content {
|
|
case let .image(image):
|
|
content = .image(image)
|
|
case let .video(_, _, duration, source):
|
|
switch source {
|
|
case let .file(path):
|
|
content = .video(path, duration)
|
|
case let .asset(asset):
|
|
content = .asset(asset)
|
|
}
|
|
default:
|
|
fatalError()
|
|
}
|
|
items.append(CameraScreenImpl.Result.VideoCollage.Item(
|
|
content: content,
|
|
frame: itemFrame,
|
|
contentScale: scale,
|
|
contentOffset: offset
|
|
))
|
|
itemFrame.origin.x += columnWidth
|
|
}
|
|
itemFrame.origin.x = 0.0
|
|
itemFrame.origin.y += rowHeight
|
|
}
|
|
return .single(.videoCollage(CameraScreenImpl.Result.VideoCollage(items: items)))
|
|
} else {
|
|
let image = generateImage(size, contextGenerator: { size, context in
|
|
var itemFrame: CGRect = .zero
|
|
for row in state.rows {
|
|
let columnWidth: CGFloat = ceil(size.width / CGFloat(row.items.count))
|
|
itemFrame = CGRect(origin: itemFrame.origin, size: CGSize(width: columnWidth, height: rowHeight))
|
|
for item in row.items {
|
|
let scale = itemViews[item.uniqueId]?.contentScale ?? 1.0
|
|
let offset = itemViews[item.uniqueId]?.contentOffset ?? .zero
|
|
|
|
let mappedItemFrame = CGRect(origin: CGPoint(x: itemFrame.minX, y: size.height - itemFrame.origin.y - rowHeight), size: CGSize(width: columnWidth, height: rowHeight))
|
|
if case let .image(image) = item.content {
|
|
context.clip(to: mappedItemFrame)
|
|
let drawingSize = image.size.aspectFilled(mappedItemFrame.size)
|
|
let center = mappedItemFrame.center.offsetBy(dx: offset.x * mappedItemFrame.width, dy: offset.y * mappedItemFrame.height)
|
|
|
|
let imageFrame = CGSize(width: drawingSize.width * scale, height: drawingSize.height * scale).centered(around: center)
|
|
if let cgImage = image.cgImage {
|
|
context.draw(cgImage, in: imageFrame, byTiling: false)
|
|
}
|
|
context.resetClip()
|
|
}
|
|
itemFrame.origin.x += columnWidth
|
|
}
|
|
itemFrame.origin.x = 0.0
|
|
itemFrame.origin.y += rowHeight
|
|
}
|
|
}, opaque: true, scale: 1.0)
|
|
if let image {
|
|
return .single(.image(CameraScreenImpl.Result.Image(image: image, additionalImage: nil, additionalImagePosition: .topLeft)))
|
|
} else {
|
|
return .single(.pendingImage)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
final class CameraCollageView: UIView, UIGestureRecognizerDelegate {
|
|
final class PreviewLayer: SimpleLayer {
|
|
var dispose: () -> Void = {}
|
|
|
|
let contentLayer: MetalEngineSubjectLayer
|
|
init(contentLayer: MetalEngineSubjectLayer) {
|
|
self.contentLayer = contentLayer
|
|
super.init()
|
|
self.addSublayer(contentLayer)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
preconditionFailure()
|
|
}
|
|
|
|
func update(size: CGSize, transition: ComponentTransition) {
|
|
let filledSize = CGSize(width: 320.0, height: 568.0).aspectFilled(size)
|
|
transition.setFrame(layer: self.contentLayer, frame: filledSize.centered(around: CGPoint(x: size.width / 2.0, y: size.height / 2.0)))
|
|
}
|
|
}
|
|
|
|
final class ItemView: ContextControllerSourceView, UIScrollViewDelegate {
|
|
private let extractedContainerView = ContextExtractedContentContainingView()
|
|
|
|
private let scrollView = UIScrollView()
|
|
private let clippingView = UIView()
|
|
private let contentView = UIView()
|
|
private var snapshotView: UIView?
|
|
private var cameraContainerView: UIView?
|
|
private var imageView: UIImageView?
|
|
private var previewLayer: PreviewLayer?
|
|
|
|
private var videoPlayer: AVPlayer?
|
|
private var videoLayer: AVPlayerLayer?
|
|
private var didPlayToEndTimeObserver: NSObjectProtocol?
|
|
|
|
private var originalCameraTransform: CATransform3D?
|
|
private var originalCameraFrame: CGRect?
|
|
|
|
var contextAction: ((Int64, ContextExtractedContentContainingView, ContextGesture?) -> Void)?
|
|
|
|
var contentScale: CGFloat {
|
|
return self.scrollView.zoomScale
|
|
}
|
|
|
|
var contentOffset: CGPoint {
|
|
return self.scrollView.offsetFromCenter
|
|
}
|
|
|
|
var isCamera: Bool {
|
|
if case .camera = self.item?.content {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
var isReady: Bool {
|
|
if case .image = self.item?.content {
|
|
return true
|
|
}
|
|
if case .video = self.item?.content {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
var isPlaying: Bool {
|
|
if let videoPlayer = self.videoPlayer {
|
|
return videoPlayer.rate > 0.0
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
var didPlayToEnd: (() -> Void) = {}
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
|
|
self.scrollView.delegate = self
|
|
self.scrollView.contentInsetAdjustmentBehavior = .never
|
|
self.scrollView.contentInset = .zero
|
|
self.scrollView.showsHorizontalScrollIndicator = false
|
|
self.scrollView.showsVerticalScrollIndicator = false
|
|
self.scrollView.decelerationRate = .fast
|
|
//self.scrollView.panGestureRecognizer.minimumNumberOfTouches = 2
|
|
|
|
self.clippingView.clipsToBounds = true
|
|
self.clippingView.isUserInteractionEnabled = false
|
|
|
|
self.addSubview(self.extractedContainerView)
|
|
|
|
self.isGestureEnabled = false
|
|
self.targetViewForActivationProgress = self.extractedContainerView.contentView
|
|
|
|
self.clipsToBounds = true
|
|
self.extractedContainerView.contentView.clipsToBounds = true
|
|
self.extractedContainerView.contentView.addSubview(self.scrollView)
|
|
self.extractedContainerView.contentView.addSubview(self.clippingView)
|
|
|
|
self.scrollView.addSubview(self.contentView)
|
|
|
|
self.extractedContainerView.willUpdateIsExtractedToContextPreview = { [weak self] value, _ in
|
|
guard let self else {
|
|
return
|
|
}
|
|
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
|
|
if value {
|
|
self.clippingView.layer.cornerRadius = 12.0
|
|
self.scrollView.layer.cornerRadius = 12.0
|
|
transition.updateSublayerTransformScale(layer: self.extractedContainerView.contentView.layer, scale: CGPoint(x: 0.9, y: 0.9))
|
|
} else {
|
|
self.clippingView.layer.cornerRadius = 0.0
|
|
self.clippingView.layer.animate(from: NSNumber(value: Float(12.0)), to: NSNumber(value: Float(0.0)), keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2)
|
|
self.scrollView.layer.cornerRadius = 0.0
|
|
self.scrollView.layer.animate(from: NSNumber(value: Float(12.0)), to: NSNumber(value: Float(0.0)), keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2)
|
|
transition.updateSublayerTransformScale(layer: self.extractedContainerView.contentView.layer, scale: CGPoint(x: 1.0, y: 1.0))
|
|
}
|
|
}
|
|
|
|
self.activated = { [weak self] gesture, _ in
|
|
guard let self, let item = self.item else {
|
|
gesture.cancel()
|
|
return
|
|
}
|
|
self.contextAction?(item.uniqueId, self.extractedContainerView, gesture)
|
|
}
|
|
}
|
|
required init?(coder aDecoder: NSCoder) {
|
|
preconditionFailure()
|
|
}
|
|
|
|
func requestContextAction() {
|
|
guard let item = self.item, self.isReady else {
|
|
return
|
|
}
|
|
self.contextAction?(item.uniqueId, self.extractedContainerView, nil)
|
|
}
|
|
|
|
func stopPlayback() {
|
|
self.videoPlayer?.pause()
|
|
}
|
|
|
|
func resetPlayback() {
|
|
self.videoPlayer?.seek(to: .zero)
|
|
self.videoPlayer?.play()
|
|
}
|
|
|
|
var getPreviewLayer: () -> PreviewLayer? = { return nil }
|
|
|
|
private var item: CameraCollage.State.Item?
|
|
func update(item: CameraCollage.State.Item, size: CGSize, cameraContainerView: UIView?, transition: ComponentTransition) {
|
|
self.item = item
|
|
|
|
let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
|
|
|
|
let bounds = CGRect(origin: .zero, size: size)
|
|
var sizeUpdated = false
|
|
if self.scrollView.frame.size.width > 0.0 && self.scrollView.frame.size != size {
|
|
sizeUpdated = true
|
|
}
|
|
transition.setFrame(view: self.scrollView, frame: CGRect(origin: .zero, size: size))
|
|
|
|
switch item.content {
|
|
case let .pending(placeholder):
|
|
if let placeholder {
|
|
let snapshotView: UIView
|
|
switch placeholder {
|
|
case let .view(view):
|
|
snapshotView = view
|
|
case let .image(image):
|
|
if let current = self.snapshotView as? UIImageView {
|
|
snapshotView = current
|
|
} else {
|
|
snapshotView = UIImageView(image: image)
|
|
snapshotView.contentMode = .scaleAspectFill
|
|
snapshotView.isUserInteractionEnabled = false
|
|
snapshotView.frame = image.size.aspectFilled(size).centered(in: CGRect(origin: .zero, size: size))
|
|
}
|
|
}
|
|
|
|
self.snapshotView = snapshotView
|
|
var snapshotTransition = transition
|
|
if snapshotView.superview !== self.clippingView {
|
|
snapshotTransition = .immediate
|
|
self.clippingView.addSubview(snapshotView)
|
|
|
|
let shutterLayer = SimpleLayer()
|
|
shutterLayer.backgroundColor = UIColor.white.cgColor
|
|
shutterLayer.frame = CGRect(origin: .zero, size: size)
|
|
self.layer.addSublayer(shutterLayer)
|
|
shutterLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
|
shutterLayer.removeFromSuperlayer()
|
|
})
|
|
}
|
|
let scale = max(size.width / snapshotView.bounds.width, size.height / snapshotView.bounds.height)
|
|
snapshotTransition.setPosition(layer: snapshotView.layer, position: center)
|
|
snapshotTransition.setTransform(layer: snapshotView.layer, transform: CATransform3DMakeScale(scale, scale, 1.0))
|
|
|
|
if let previewLayer = self.previewLayer {
|
|
previewLayer.dispose()
|
|
previewLayer.removeFromSuperlayer()
|
|
self.previewLayer = nil
|
|
}
|
|
if let cameraContainerView = self.cameraContainerView {
|
|
cameraContainerView.removeFromSuperview()
|
|
self.cameraContainerView = nil
|
|
}
|
|
}
|
|
case .camera:
|
|
if let cameraContainerView {
|
|
self.cameraContainerView = cameraContainerView
|
|
if cameraContainerView.superview !== self.extractedContainerView.contentView {
|
|
self.originalCameraTransform = CATransform3DIdentity
|
|
self.originalCameraFrame = cameraContainerView.bounds
|
|
self.clippingView.addSubview(cameraContainerView)
|
|
}
|
|
if let originalCameraFrame = self.originalCameraFrame {
|
|
let scale = max(size.width / originalCameraFrame.width, size.height / originalCameraFrame.height)
|
|
transition.setPosition(layer: cameraContainerView.layer, position: center)
|
|
transition.setTransform(layer: cameraContainerView.layer, transform: CATransform3DMakeScale(scale, scale, 1.0))
|
|
}
|
|
}
|
|
if let imageView = self.imageView {
|
|
imageView.superview?.bringSubviewToFront(imageView)
|
|
imageView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
|
imageView.removeFromSuperview()
|
|
})
|
|
self.imageView = nil
|
|
}
|
|
if let videoLayer = self.videoLayer {
|
|
self.videoPlayer?.pause()
|
|
self.videoPlayer = nil
|
|
|
|
videoLayer.superlayer?.addSublayer(videoLayer)
|
|
videoLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
|
videoLayer.removeFromSuperlayer()
|
|
})
|
|
self.videoLayer = nil
|
|
}
|
|
if let snapshotView = self.snapshotView {
|
|
snapshotView.removeFromSuperview()
|
|
self.snapshotView = nil
|
|
}
|
|
if let previewLayer = self.previewLayer {
|
|
cameraContainerView?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { _ in
|
|
previewLayer.removeFromSuperlayer()
|
|
})
|
|
self.previewLayer = nil
|
|
}
|
|
case .empty:
|
|
if let cameraContainerView = self.cameraContainerView {
|
|
cameraContainerView.removeFromSuperview()
|
|
self.cameraContainerView = nil
|
|
}
|
|
if let snapshotView = self.snapshotView {
|
|
snapshotView.removeFromSuperview()
|
|
self.snapshotView = nil
|
|
}
|
|
var imageTransition = transition
|
|
if self.previewLayer == nil, let previewLayer = self.getPreviewLayer() {
|
|
imageTransition = .immediate
|
|
self.previewLayer = previewLayer
|
|
|
|
self.clippingView.layer.addSublayer(previewLayer)
|
|
}
|
|
if let imageView = self.imageView {
|
|
imageView.superview?.bringSubviewToFront(imageView)
|
|
imageView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
|
imageView.removeFromSuperview()
|
|
})
|
|
self.imageView = nil
|
|
}
|
|
if let videoLayer = self.videoLayer {
|
|
self.videoPlayer?.pause()
|
|
self.videoPlayer = nil
|
|
|
|
videoLayer.superlayer?.addSublayer(videoLayer)
|
|
videoLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
|
videoLayer.removeFromSuperlayer()
|
|
})
|
|
self.videoLayer = nil
|
|
}
|
|
if let previewLayer = self.previewLayer {
|
|
previewLayer.update(size: size, transition: imageTransition)
|
|
imageTransition.setFrame(layer: previewLayer, frame: CGRect(origin: .zero, size: size))
|
|
}
|
|
case let .image(image):
|
|
if let cameraContainerView = self.cameraContainerView {
|
|
cameraContainerView.removeFromSuperview()
|
|
self.cameraContainerView = nil
|
|
}
|
|
if let snapshotView = self.snapshotView {
|
|
snapshotView.removeFromSuperview()
|
|
self.snapshotView = nil
|
|
}
|
|
if let previewLayer = self.previewLayer {
|
|
previewLayer.dispose()
|
|
previewLayer.removeFromSuperlayer()
|
|
self.previewLayer = nil
|
|
}
|
|
|
|
var added = false
|
|
var imageTransition = transition
|
|
var imageView: UIImageView
|
|
if let current = self.imageView {
|
|
imageView = current
|
|
} else {
|
|
imageTransition = .immediate
|
|
imageView = UIImageView()
|
|
imageView.contentMode = .scaleAspectFill
|
|
self.imageView = imageView
|
|
self.contentView.addSubview(imageView)
|
|
added = true
|
|
}
|
|
imageView.image = image
|
|
|
|
let dimensions = image.size.aspectFilled(size)
|
|
imageTransition.setFrame(view: imageView, frame: CGRect(origin: .zero, size: dimensions))
|
|
|
|
if added || sizeUpdated {
|
|
self.contentView.bounds = CGRect(origin: .zero, size: dimensions)
|
|
self.scrollView.contentSize = dimensions
|
|
self.scrollView.resetZooming()
|
|
}
|
|
case let .video(asset, _, _, _):
|
|
if let cameraContainerView = self.cameraContainerView {
|
|
cameraContainerView.removeFromSuperview()
|
|
self.cameraContainerView = nil
|
|
}
|
|
var delayAppearance = false
|
|
if let snapshotView = self.snapshotView {
|
|
if snapshotView is UIImageView {
|
|
|
|
} else {
|
|
delayAppearance = true
|
|
}
|
|
Queue.mainQueue().after(0.2, {
|
|
snapshotView.removeFromSuperview()
|
|
})
|
|
self.snapshotView = nil
|
|
}
|
|
if let previewLayer = self.previewLayer {
|
|
previewLayer.dispose()
|
|
previewLayer.removeFromSuperlayer()
|
|
self.previewLayer = nil
|
|
}
|
|
|
|
var added = false
|
|
var imageTransition = transition
|
|
if self.videoLayer == nil {
|
|
imageTransition = .immediate
|
|
let playerItem = AVPlayerItem(asset: asset)
|
|
let player = AVPlayer(playerItem: playerItem)
|
|
player.isMuted = true
|
|
if self.didPlayToEndTimeObserver == nil {
|
|
self.didPlayToEndTimeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player.currentItem, queue: nil, using: { [weak self] notification in
|
|
if let self {
|
|
self.didPlayToEnd()
|
|
}
|
|
})
|
|
}
|
|
|
|
let videoLayer = AVPlayerLayer(player: player)
|
|
videoLayer.videoGravity = .resizeAspectFill
|
|
|
|
if delayAppearance {
|
|
videoLayer.opacity = 0.0
|
|
Queue.mainQueue().after(0.15, {
|
|
videoLayer.opacity = 1.0
|
|
})
|
|
}
|
|
|
|
self.videoLayer = videoLayer
|
|
self.videoPlayer = player
|
|
|
|
self.contentView.layer.addSublayer(videoLayer)
|
|
|
|
player.playImmediately(atRate: 1.0)
|
|
|
|
added = true
|
|
}
|
|
|
|
let dimensions = (asset.videoDimensions ?? CGSize(width: 1.0, height: 1.0)).aspectFilled(size)
|
|
if let videoLayer = self.videoLayer {
|
|
imageTransition.setFrame(layer: videoLayer, frame: CGRect(origin: .zero, size: dimensions))
|
|
}
|
|
|
|
if added || sizeUpdated {
|
|
self.contentView.bounds = CGRect(origin: .zero, size: dimensions)
|
|
self.scrollView.contentSize = dimensions
|
|
self.scrollView.resetZooming()
|
|
}
|
|
}
|
|
|
|
self.adjustPreviewZoom(updating: true)
|
|
|
|
transition.setFrame(view: self.extractedContainerView, frame: bounds)
|
|
transition.setFrame(view: self.extractedContainerView.contentView, frame: bounds)
|
|
transition.setBounds(view: self.clippingView, bounds: bounds)
|
|
transition.setPosition(view: self.clippingView, position: bounds.center)
|
|
self.extractedContainerView.contentRect = bounds
|
|
}
|
|
|
|
func animateIn(from size: CGSize, transition: ComponentTransition) {
|
|
guard let cameraContainerView = self.cameraContainerView, let originalCameraFrame = self.originalCameraFrame else {
|
|
return
|
|
}
|
|
|
|
self.extractedContainerView.contentView.clipsToBounds = false
|
|
self.clippingView.clipsToBounds = false
|
|
|
|
let scale = self.bounds.width / originalCameraFrame.width
|
|
transition.animateScale(view: cameraContainerView, from: 1.0, to: scale)
|
|
transition.animatePosition(view: cameraContainerView, from: originalCameraFrame.center, to: cameraContainerView.center, completion: { _ in
|
|
self.extractedContainerView.contentView.clipsToBounds = true
|
|
self.clippingView.clipsToBounds = true
|
|
})
|
|
}
|
|
|
|
func animateOut(to size: CGSize, transition: ComponentTransition, completion: @escaping () -> Void) {
|
|
guard let cameraContainerView = self.cameraContainerView, let originalCameraFrame = self.originalCameraFrame else {
|
|
return
|
|
}
|
|
|
|
self.extractedContainerView.contentView.clipsToBounds = false
|
|
self.clippingView.clipsToBounds = false
|
|
|
|
let scale = max(self.frame.width / originalCameraFrame.width, self.frame.height / originalCameraFrame.height)
|
|
cameraContainerView.transform = CGAffineTransform.identity
|
|
transition.animateScale(view: cameraContainerView, from: scale, to: 1.0)
|
|
transition.setPosition(view: cameraContainerView, position: CGPoint(x: size.width / 2.0, y: size.height / 2.0), completion: { _ in
|
|
self.extractedContainerView.contentView.clipsToBounds = true
|
|
self.clippingView.clipsToBounds = true
|
|
completion()
|
|
})
|
|
}
|
|
|
|
private func adjustPreviewZoom(updating: Bool = false) {
|
|
let minScale: CGFloat = 1.0
|
|
let maxScale: CGFloat = 3.5
|
|
|
|
if self.scrollView.minimumZoomScale != minScale {
|
|
self.scrollView.minimumZoomScale = minScale
|
|
}
|
|
if self.scrollView.maximumZoomScale != maxScale {
|
|
self.scrollView.maximumZoomScale = maxScale
|
|
}
|
|
|
|
let boundsSize = self.scrollView.frame.size
|
|
var contentFrame = self.contentView.frame
|
|
if boundsSize.width > contentFrame.size.width {
|
|
contentFrame.origin.x = (boundsSize.width - contentFrame.size.width) / 2.0
|
|
} else {
|
|
contentFrame.origin.x = 0.0
|
|
}
|
|
|
|
if boundsSize.height > contentFrame.size.height {
|
|
contentFrame.origin.y = (boundsSize.height - contentFrame.size.height) / 2.0
|
|
} else {
|
|
contentFrame.origin.y = 0.0
|
|
}
|
|
self.contentView.frame = contentFrame
|
|
}
|
|
|
|
func scrollViewDidZoom(_ scrollView: UIScrollView) {
|
|
self.adjustPreviewZoom()
|
|
}
|
|
|
|
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
|
|
self.adjustPreviewZoom()
|
|
|
|
if scrollView.zoomScale < 1.0 {
|
|
scrollView.setZoomScale(1.0, animated: true)
|
|
}
|
|
}
|
|
|
|
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
|
|
return self.contentView
|
|
}
|
|
}
|
|
|
|
private let context: AccountContext
|
|
private let collage: CameraCollage
|
|
private weak var camera: Camera?
|
|
private weak var cameraContainerView: UIView?
|
|
|
|
private var cameraVideoSource: CameraVideoSource?
|
|
private var cameraVideoDisposable: Disposable?
|
|
|
|
private let cameraVideoLayer = CameraVideoLayer()
|
|
private let cloneLayers: [MetalEngineSubjectLayer]
|
|
|
|
private var itemViews: [Int64: ItemView] = [:]
|
|
|
|
private var state: CameraCollage.State?
|
|
private var disposable: Disposable?
|
|
|
|
private var reorderRecognizer: ReorderGestureRecognizer?
|
|
private var reorderingItem: (id: Int64, initialPosition: CGPoint, position: CGPoint)?
|
|
|
|
private var tapRecognizer: UITapGestureRecognizer?
|
|
|
|
private var validLayout: CGSize?
|
|
|
|
var getOverlayViews: (() -> [UIView])?
|
|
|
|
var requestGridReduce: (() -> Void)?
|
|
|
|
var isEnabled: Bool = true
|
|
|
|
var result: Signal<CameraScreenImpl.Result, NoError> {
|
|
return self.collage.result(itemViews: self.itemViews)
|
|
}
|
|
|
|
init(context: AccountContext, collage: CameraCollage, camera: Camera?, cameraContainerView: UIView?) {
|
|
self.context = context
|
|
self.collage = collage
|
|
self.cameraContainerView = cameraContainerView
|
|
|
|
self.cloneLayers = (0 ..< 6).map { _ in MetalEngineSubjectLayer() }
|
|
self.cameraVideoLayer.blurredLayer.cloneLayers = self.cloneLayers
|
|
|
|
super.init(frame: .zero)
|
|
|
|
self.backgroundColor = .black
|
|
|
|
self.disposable = (collage.state
|
|
|> deliverOnMainQueue).start(next: { [weak self] state in
|
|
guard let self else {
|
|
return
|
|
}
|
|
let previousState = self.state
|
|
self.state = state
|
|
if let size = self.validLayout {
|
|
var transition: ComponentTransition = .spring(duration: 0.3)
|
|
if let previousState, previousState.grid == state.grid && previousState.innerProgress != state.innerProgress {
|
|
transition = .immediate
|
|
}
|
|
var progressUpdated = false
|
|
if let previousState, previousState.progress != state.progress {
|
|
progressUpdated = true
|
|
}
|
|
self.updateLayout(size: size, transition: transition)
|
|
|
|
if progressUpdated {
|
|
self.resetPlayback()
|
|
}
|
|
}
|
|
})
|
|
|
|
let reorderRecognizer = ReorderGestureRecognizer(
|
|
shouldBegin: { [weak self] point in
|
|
guard let self, let item = self.item(at: point) else {
|
|
return (allowed: false, requiresLongPress: false, item: nil)
|
|
}
|
|
|
|
return (allowed: true, requiresLongPress: true, item: item)
|
|
},
|
|
willBegin: { point in
|
|
},
|
|
began: { [weak self] item in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.setReorderingItem(item: item)
|
|
},
|
|
ended: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.setReorderingItem(item: nil)
|
|
},
|
|
moved: { [weak self] distance in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.moveReorderingItem(distance: distance)
|
|
},
|
|
isActiveUpdated: { _ in
|
|
}
|
|
)
|
|
reorderRecognizer.delegate = self
|
|
self.reorderRecognizer = reorderRecognizer
|
|
self.addGestureRecognizer(reorderRecognizer)
|
|
|
|
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap))
|
|
self.tapRecognizer = tapRecognizer
|
|
self.addGestureRecognizer(tapRecognizer)
|
|
|
|
if let cameraVideoSource = CameraVideoSource() {
|
|
self.cameraVideoLayer.video = cameraVideoSource.currentOutput
|
|
camera?.setPreviewOutput(cameraVideoSource.cameraVideoOutput)
|
|
self.cameraVideoSource = cameraVideoSource
|
|
|
|
self.cameraVideoDisposable = cameraVideoSource.addOnUpdated { [weak self] in
|
|
guard let self, let videoSource = self.cameraVideoSource, self.isEnabled else {
|
|
return
|
|
}
|
|
self.cameraVideoLayer.video = videoSource.currentOutput
|
|
}
|
|
}
|
|
|
|
let videoSize = CGSize(width: 160.0 * 2.0, height: 284.0 * 2.0)
|
|
self.cameraVideoLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: videoSize)
|
|
self.cameraVideoLayer.blurredLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: videoSize)
|
|
self.cameraVideoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(videoSize.width), height: Int(videoSize.height)), edgeInset: 2)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
preconditionFailure()
|
|
}
|
|
|
|
deinit {
|
|
self.disposable?.dispose()
|
|
self.cameraVideoDisposable?.dispose()
|
|
self.camera?.setPreviewOutput(nil)
|
|
}
|
|
|
|
func getPreviewLayer() -> PreviewLayer {
|
|
var contentLayer = MetalEngineSubjectLayer()
|
|
for layer in self.cloneLayers {
|
|
if layer.superlayer?.superlayer == nil {
|
|
contentLayer = layer
|
|
break
|
|
}
|
|
}
|
|
return PreviewLayer(contentLayer: contentLayer)
|
|
}
|
|
|
|
@objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {
|
|
self.reorderRecognizer?.isEnabled = false
|
|
self.reorderRecognizer?.isEnabled = true
|
|
|
|
let location = gestureRecognizer.location(in: self)
|
|
if let itemView = self.item(at: location) {
|
|
itemView.requestContextAction()
|
|
}
|
|
}
|
|
|
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
if otherGestureRecognizer is UITapGestureRecognizer {
|
|
return true
|
|
}
|
|
if otherGestureRecognizer is UIPanGestureRecognizer {
|
|
if gestureRecognizer === self.reorderRecognizer, ![.began, .changed].contains(gestureRecognizer.state) {
|
|
gestureRecognizer.isEnabled = false
|
|
gestureRecognizer.isEnabled = true
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func item(at point: CGPoint) -> ItemView? {
|
|
for (_, itemView) in self.itemViews {
|
|
if itemView.frame.contains(point), itemView.isReady {
|
|
return itemView
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func setReorderingItem(item: ItemView?) {
|
|
self.tapRecognizer?.isEnabled = false
|
|
self.tapRecognizer?.isEnabled = true
|
|
|
|
var mappedItem: (Int64, ItemView)?
|
|
if let item {
|
|
for (id, visibleItem) in self.itemViews {
|
|
if visibleItem === item {
|
|
mappedItem = (id, visibleItem)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if self.reorderingItem?.id != mappedItem?.0 {
|
|
if let (id, itemView) = mappedItem {
|
|
self.addSubview(itemView)
|
|
self.reorderingItem = (id, itemView.center, itemView.center)
|
|
} else {
|
|
self.reorderingItem = nil
|
|
}
|
|
if let size = self.validLayout {
|
|
self.updateLayout(size: size, transition: .spring(duration: 0.4))
|
|
}
|
|
}
|
|
}
|
|
|
|
func moveReorderingItem(distance: CGPoint) {
|
|
if let (id, initialPosition, _) = self.reorderingItem {
|
|
let targetPosition = CGPoint(x: initialPosition.x + distance.x, y: initialPosition.y + distance.y)
|
|
self.reorderingItem = (id, initialPosition, targetPosition)
|
|
if let size = self.validLayout {
|
|
self.updateLayout(size: size, transition: .immediate)
|
|
}
|
|
|
|
if let visibleReorderingItem = self.itemViews[id] {
|
|
for (visibleId, visibleItem) in self.itemViews {
|
|
if visibleItem === visibleReorderingItem {
|
|
continue
|
|
}
|
|
if visibleItem.frame.contains(targetPosition) {
|
|
self.collage.moveItem(fromId: id, toId: visibleId)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func stopPlayback() {
|
|
for (_, itemView) in self.itemViews {
|
|
itemView.stopPlayback()
|
|
}
|
|
}
|
|
|
|
func resetPlayback() {
|
|
for (_, itemView) in self.itemViews {
|
|
itemView.resetPlayback()
|
|
}
|
|
}
|
|
|
|
func maybeResetPlayback() {
|
|
var shouldResetPlayback = true
|
|
for (_, itemView) in self.itemViews {
|
|
if itemView.isPlaying {
|
|
shouldResetPlayback = false
|
|
break
|
|
}
|
|
}
|
|
if shouldResetPlayback {
|
|
self.resetPlayback()
|
|
}
|
|
}
|
|
|
|
func animateIn(transition: ComponentTransition) {
|
|
guard let size = self.validLayout, let (_, cameraItemView) = self.itemViews.first(where: { $0.value.isCamera }) else {
|
|
return
|
|
}
|
|
|
|
let targetFrame = cameraItemView.frame
|
|
let sourceFrame = CGRect(origin: .zero, size: size)
|
|
|
|
cameraItemView.frame = sourceFrame
|
|
transition.setFrame(view: cameraItemView, frame: targetFrame)
|
|
cameraItemView.animateIn(from: sourceFrame.size, transition: transition)
|
|
}
|
|
|
|
func animateOut(transition: ComponentTransition, completion: @escaping () -> Void) {
|
|
guard let size = self.validLayout else {
|
|
completion()
|
|
return
|
|
}
|
|
guard let (_, cameraItemView) = self.itemViews.first(where: { $0.value.isCamera }) else {
|
|
if let cameraContainerView = self.cameraContainerView {
|
|
cameraContainerView.transform = CGAffineTransform.identity
|
|
cameraContainerView.frame = CGRect(origin: .zero, size: size)
|
|
cameraContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
cameraContainerView.layer.animateScale(from: 0.02, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
|
|
completion()
|
|
})
|
|
self.addSubview(cameraContainerView)
|
|
}
|
|
return
|
|
}
|
|
|
|
cameraItemView.superview?.bringSubviewToFront(cameraItemView)
|
|
|
|
let targetFrame = CGRect(origin: .zero, size: size)
|
|
cameraItemView.animateOut(to: targetFrame.size, transition: transition, completion: completion)
|
|
transition.setFrame(view: cameraItemView, frame: targetFrame)
|
|
}
|
|
|
|
var presentController: ((ViewController) -> Void)?
|
|
func contextGesture(id: Int64, sourceView: ContextExtractedContentContainingView, gesture: ContextGesture?) {
|
|
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
|
|
|
var itemList: [ContextMenuItem] = []
|
|
if self.collage.cameraIndex == nil {
|
|
itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.Camera_CollageRetake, icon: { theme in
|
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Camera"), color: theme.contextMenu.primaryColor)
|
|
}, action: { [weak self] _, f in
|
|
f(.default)
|
|
|
|
self?.collage.retakeItem(id: id)
|
|
})))
|
|
}
|
|
|
|
if self.itemViews.count > 2 {
|
|
if itemList.count > 0 {
|
|
itemList.append(.separator)
|
|
}
|
|
|
|
itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.Camera_CollageDelete, textColor: .destructive, icon: { theme in
|
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
|
|
}, action: { [weak self] _, f in
|
|
f(.dismissWithoutContent)
|
|
|
|
self?.collage.deleteItem(id: id)
|
|
self?.requestGridReduce?()
|
|
})))
|
|
}
|
|
|
|
guard !itemList.isEmpty else {
|
|
return
|
|
}
|
|
|
|
let items = ContextController.Items(content: .list(itemList), tip: .collageReordering)
|
|
let controller = ContextController(
|
|
presentationData: presentationData.withUpdated(theme: defaultDarkColorPresentationTheme),
|
|
source: .extracted(CollageContextExtractedContentSource(contentView: sourceView)),
|
|
items: .single(items),
|
|
recognizer: nil,
|
|
gesture: gesture
|
|
)
|
|
controller.getOverlayViews = self.getOverlayViews
|
|
self.presentController?(controller)
|
|
}
|
|
|
|
func updateLayout(size: CGSize, transition: ComponentTransition) {
|
|
self.validLayout = size
|
|
guard let state = self.state else {
|
|
return
|
|
}
|
|
|
|
var validIds = Set<Int64>()
|
|
|
|
let rowHeight: CGFloat = ceil(size.height / CGFloat(state.rows.count))
|
|
|
|
var previousItemFrame: CGRect?
|
|
|
|
var itemFrame: CGRect = .zero
|
|
for row in state.rows {
|
|
let columnWidth: CGFloat = floor(size.width / CGFloat(row.items.count))
|
|
itemFrame = CGRect(origin: itemFrame.origin, size: CGSize(width: columnWidth, height: rowHeight))
|
|
|
|
for item in row.items {
|
|
let id = item.uniqueId
|
|
validIds.insert(id)
|
|
|
|
var effectiveItemFrame = itemFrame
|
|
let itemScale: CGFloat
|
|
let itemCornerRadius: CGFloat
|
|
if let reorderingItem = self.reorderingItem, item.uniqueId == reorderingItem.id {
|
|
itemScale = 0.9
|
|
itemCornerRadius = 12.0
|
|
effectiveItemFrame = itemFrame.size.centered(around: reorderingItem.position)
|
|
} else {
|
|
itemScale = 1.0
|
|
itemCornerRadius = 0.0
|
|
}
|
|
|
|
var itemTransition = transition
|
|
let itemView: ItemView
|
|
if let current = self.itemViews[id] {
|
|
itemView = current
|
|
previousItemFrame = itemFrame
|
|
} else {
|
|
itemView = ItemView(frame: effectiveItemFrame)
|
|
itemView.clipsToBounds = true
|
|
itemView.getPreviewLayer = { [weak self] in
|
|
return self?.getPreviewLayer()
|
|
}
|
|
itemView.didPlayToEnd = { [weak self] in
|
|
self?.maybeResetPlayback()
|
|
}
|
|
self.insertSubview(itemView, at: 0)
|
|
self.itemViews[id] = itemView
|
|
|
|
if !transition.animation.isImmediate, let previousItemFrame {
|
|
itemView.frame = previousItemFrame
|
|
} else {
|
|
itemTransition = .immediate
|
|
}
|
|
}
|
|
itemView.update(item: item, size: effectiveItemFrame.size, cameraContainerView: self.cameraContainerView, transition: itemTransition)
|
|
itemView.contextAction = { [weak self] id, sourceView, gesture in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.contextGesture(id: id, sourceView: sourceView, gesture: gesture)
|
|
}
|
|
|
|
itemTransition.setBounds(view: itemView, bounds: CGRect(origin: .zero, size: effectiveItemFrame.size))
|
|
itemTransition.setPosition(view: itemView, position: effectiveItemFrame.center)
|
|
itemTransition.setScale(view: itemView, scale: itemScale)
|
|
|
|
if !itemTransition.animation.isImmediate {
|
|
let cornerTransition: ComponentTransition
|
|
if itemCornerRadius > 0.0 {
|
|
cornerTransition = ComponentTransition(animation: .curve(duration: 0.1, curve: .linear))
|
|
} else {
|
|
cornerTransition = .easeInOut(duration: 0.4)
|
|
}
|
|
cornerTransition.setCornerRadius(layer: itemView.layer, cornerRadius: itemCornerRadius)
|
|
} else {
|
|
itemTransition.setCornerRadius(layer: itemView.layer, cornerRadius: itemCornerRadius)
|
|
}
|
|
|
|
itemFrame.origin.x += columnWidth
|
|
}
|
|
itemFrame.origin.x = 0.0
|
|
itemFrame.origin.y += rowHeight
|
|
}
|
|
|
|
var removeIds: [Int64] = []
|
|
for (id, itemView) in self.itemViews {
|
|
if !validIds.contains(id) {
|
|
removeIds.append(id)
|
|
transition.setAlpha(view: itemView, alpha: 0.0, completion: { [weak itemView] _ in
|
|
itemView?.removeFromSuperview()
|
|
})
|
|
}
|
|
}
|
|
for id in removeIds {
|
|
self.itemViews.removeValue(forKey: id)
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class ReorderGestureRecognizer: UIGestureRecognizer {
|
|
private let shouldBegin: (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, item: CameraCollageView.ItemView?)
|
|
private let willBegin: (CGPoint) -> Void
|
|
private let began: (CameraCollageView.ItemView) -> Void
|
|
private let ended: () -> Void
|
|
private let moved: (CGPoint) -> Void
|
|
private let isActiveUpdated: (Bool) -> Void
|
|
|
|
private var initialLocation: CGPoint?
|
|
private var longTapTimer: SwiftSignalKit.Timer?
|
|
private var longPressTimer: SwiftSignalKit.Timer?
|
|
|
|
private var itemView: CameraCollageView.ItemView?
|
|
|
|
public init(shouldBegin: @escaping (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, item: CameraCollageView.ItemView?), willBegin: @escaping (CGPoint) -> Void, began: @escaping (CameraCollageView.ItemView) -> Void, ended: @escaping () -> Void, moved: @escaping (CGPoint) -> Void, isActiveUpdated: @escaping (Bool) -> Void) {
|
|
self.shouldBegin = shouldBegin
|
|
self.willBegin = willBegin
|
|
self.began = began
|
|
self.ended = ended
|
|
self.moved = moved
|
|
self.isActiveUpdated = isActiveUpdated
|
|
|
|
super.init(target: nil, action: nil)
|
|
}
|
|
|
|
deinit {
|
|
self.longTapTimer?.invalidate()
|
|
self.longPressTimer?.invalidate()
|
|
}
|
|
|
|
private func startLongTapTimer() {
|
|
self.longTapTimer?.invalidate()
|
|
let longTapTimer = SwiftSignalKit.Timer(timeout: 0.25, repeat: false, completion: { [weak self] in
|
|
self?.longTapTimerFired()
|
|
}, queue: Queue.mainQueue())
|
|
self.longTapTimer = longTapTimer
|
|
longTapTimer.start()
|
|
}
|
|
|
|
private func stopLongTapTimer() {
|
|
self.itemView = nil
|
|
self.longTapTimer?.invalidate()
|
|
self.longTapTimer = nil
|
|
}
|
|
|
|
private func startLongPressTimer() {
|
|
self.longPressTimer?.invalidate()
|
|
let longPressTimer = SwiftSignalKit.Timer(timeout: 0.6, repeat: false, completion: { [weak self] in
|
|
self?.longPressTimerFired()
|
|
}, queue: Queue.mainQueue())
|
|
self.longPressTimer = longPressTimer
|
|
longPressTimer.start()
|
|
}
|
|
|
|
private func stopLongPressTimer() {
|
|
self.itemView = nil
|
|
self.longPressTimer?.invalidate()
|
|
self.longPressTimer = nil
|
|
}
|
|
|
|
override public func reset() {
|
|
super.reset()
|
|
|
|
self.itemView = nil
|
|
self.stopLongTapTimer()
|
|
self.stopLongPressTimer()
|
|
self.initialLocation = nil
|
|
|
|
self.isActiveUpdated(false)
|
|
}
|
|
|
|
private func longTapTimerFired() {
|
|
guard let location = self.initialLocation else {
|
|
return
|
|
}
|
|
|
|
self.longTapTimer?.invalidate()
|
|
self.longTapTimer = nil
|
|
|
|
self.willBegin(location)
|
|
}
|
|
|
|
private func longPressTimerFired() {
|
|
guard let _ = self.initialLocation else {
|
|
return
|
|
}
|
|
|
|
self.isActiveUpdated(true)
|
|
self.state = .began
|
|
self.longPressTimer?.invalidate()
|
|
self.longPressTimer = nil
|
|
self.longTapTimer?.invalidate()
|
|
self.longTapTimer = nil
|
|
if let itemView = self.itemView {
|
|
self.began(itemView)
|
|
}
|
|
self.isActiveUpdated(true)
|
|
}
|
|
|
|
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
super.touchesBegan(touches, with: event)
|
|
|
|
if self.numberOfTouches > 1 {
|
|
self.isActiveUpdated(false)
|
|
self.state = .failed
|
|
self.ended()
|
|
return
|
|
}
|
|
|
|
if self.state == .possible {
|
|
if let location = touches.first?.location(in: self.view) {
|
|
let (allowed, requiresLongPress, itemView) = self.shouldBegin(location)
|
|
if allowed {
|
|
self.isActiveUpdated(true)
|
|
|
|
self.itemView = itemView
|
|
self.initialLocation = location
|
|
if requiresLongPress {
|
|
self.startLongTapTimer()
|
|
self.startLongPressTimer()
|
|
} else {
|
|
self.state = .began
|
|
if let itemView = self.itemView {
|
|
self.began(itemView)
|
|
}
|
|
}
|
|
} else {
|
|
self.isActiveUpdated(false)
|
|
self.state = .failed
|
|
}
|
|
} else {
|
|
self.isActiveUpdated(false)
|
|
self.state = .failed
|
|
}
|
|
}
|
|
}
|
|
|
|
override public func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
super.touchesEnded(touches, with: event)
|
|
|
|
self.initialLocation = nil
|
|
|
|
self.stopLongTapTimer()
|
|
if self.longPressTimer != nil {
|
|
self.stopLongPressTimer()
|
|
self.isActiveUpdated(false)
|
|
self.state = .failed
|
|
}
|
|
if self.state == .began || self.state == .changed {
|
|
self.isActiveUpdated(false)
|
|
self.ended()
|
|
self.state = .failed
|
|
}
|
|
}
|
|
|
|
override public func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
super.touchesCancelled(touches, with: event)
|
|
|
|
self.initialLocation = nil
|
|
|
|
self.stopLongTapTimer()
|
|
if self.longPressTimer != nil {
|
|
self.isActiveUpdated(false)
|
|
self.stopLongPressTimer()
|
|
self.state = .failed
|
|
}
|
|
if self.state == .began || self.state == .changed {
|
|
self.isActiveUpdated(false)
|
|
self.ended()
|
|
self.state = .failed
|
|
}
|
|
}
|
|
|
|
override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
super.touchesMoved(touches, with: event)
|
|
|
|
if (self.state == .began || self.state == .changed), let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) {
|
|
self.state = .changed
|
|
let offset = CGPoint(x: location.x - initialLocation.x, y: location.y - initialLocation.y)
|
|
self.moved(offset)
|
|
} else if let touch = touches.first, let initialTapLocation = self.initialLocation, self.longPressTimer != nil {
|
|
let touchLocation = touch.location(in: self.view)
|
|
let dX = touchLocation.x - initialTapLocation.x
|
|
let dY = touchLocation.y - initialTapLocation.y
|
|
|
|
if dX * dX + dY * dY > 3.0 * 3.0 {
|
|
self.stopLongTapTimer()
|
|
self.stopLongPressTimer()
|
|
self.initialLocation = nil
|
|
self.isActiveUpdated(false)
|
|
self.state = .failed
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class CollageContextExtractedContentSource: ContextExtractedContentSource {
|
|
let keepInPlace: Bool = false
|
|
let ignoreContentTouches: Bool = false
|
|
let blurBackground: Bool = true
|
|
|
|
private let contentView: ContextExtractedContentContainingView
|
|
|
|
init(contentView: ContextExtractedContentContainingView) {
|
|
self.contentView = contentView
|
|
}
|
|
|
|
func takeView() -> ContextControllerTakeViewInfo? {
|
|
return ContextControllerTakeViewInfo(containingItem: .view(self.contentView), contentAreaInScreenSpace: UIScreen.main.bounds)
|
|
}
|
|
|
|
func putBack() -> ContextControllerPutBackViewInfo? {
|
|
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds)
|
|
}
|
|
}
|
|
|
|
private extension AVAsset {
|
|
var videoDimensions: CGSize? {
|
|
if let videoTrack = self.tracks(withMediaType: .video).first {
|
|
let size = videoTrack.naturalSize
|
|
let transform = videoTrack.preferredTransform
|
|
let isPortrait = transform.a == 0 && abs(transform.b) == 1 && abs(transform.c) == 1 && transform.d == 0
|
|
return isPortrait ? CGSize(width: size.height, height: size.width) : size
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private extension UIScrollView {
|
|
func resetZooming() {
|
|
guard let contentView = self.delegate?.viewForZooming?(in: self) else { return }
|
|
|
|
let scrollViewSize = self.bounds.size
|
|
let contentSize = contentView.frame.size
|
|
|
|
let offsetX = (scrollViewSize.width - contentSize.width) * 0.5
|
|
let offsetY = (scrollViewSize.height - contentSize.height) * 0.5
|
|
|
|
contentView.center = CGPoint(
|
|
x: scrollViewSize.width / 2.0 + self.contentOffset.x,
|
|
y: scrollViewSize.height / 2.0 + self.contentOffset.y
|
|
)
|
|
|
|
self.contentOffset = CGPoint(x: -offsetX, y: -offsetY)
|
|
}
|
|
|
|
var offsetFromCenter: CGPoint {
|
|
let contentCenterX = (self.contentSize.width - self.bounds.width) / 2.0
|
|
let contentCenterY = (self.contentSize.height - self.bounds.height) / 2.0
|
|
let contentCenter = CGPoint(x: contentCenterX, y: contentCenterY)
|
|
|
|
let currentOffset = self.contentOffset
|
|
let deltaX = currentOffset.x - contentCenter.x
|
|
let deltaY = currentOffset.y - contentCenter.y
|
|
|
|
return CGPoint(x: -(deltaX / self.bounds.width), y: (deltaY / self.bounds.height))
|
|
}
|
|
}
|