mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-05 14:02:48 +00:00
Camera and editor improvements
This commit is contained in:
parent
137e234310
commit
ceaa808fad
@ -73,6 +73,7 @@ swift_library(
|
||||
"//submodules/Components/BundleIconComponent:BundleIconComponent",
|
||||
"//submodules/TooltipUI",
|
||||
"//submodules/TelegramUI/Components/MediaEditor",
|
||||
"//submodules/Components/MetalImageView:MetalImageView",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -148,6 +148,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
|
||||
var cameraState = CameraState(mode: .photo, position: .unspecified, flashMode: .off, flashModeDidChange: false, recording: .none, duration: 0.0)
|
||||
var swipeHint: CaptureControlsComponent.SwipeHint = .none
|
||||
var isTransitioning = false
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
@ -267,6 +268,12 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
self.completion.invoke(.single(.video(path, transitionImage, PixelDimensions(width: 1080, height: 1920))))
|
||||
}
|
||||
}))
|
||||
self.isTransitioning = true
|
||||
Queue.mainQueue().after(0.8, {
|
||||
self.isTransitioning = false
|
||||
self.updated(transition: .immediate)
|
||||
})
|
||||
|
||||
self.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
|
||||
@ -290,7 +297,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
let zoomControl = Child(ZoomComponent.self)
|
||||
let flashButton = Child(CameraButton.self)
|
||||
let modeControl = Child(ModeComponent.self)
|
||||
let hintLabel = Child(MultilineTextComponent.self)
|
||||
let hintLabel = Child(HintLabelComponent.self)
|
||||
|
||||
let timeBackground = Child(RoundedRectangle.self)
|
||||
let timeLabel = Child(MultilineTextComponent.self)
|
||||
@ -308,7 +315,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
state?.updateCameraMode(mode)
|
||||
})
|
||||
|
||||
if case .none = state.cameraState.recording {
|
||||
if case .none = state.cameraState.recording, !state.isTransitioning {
|
||||
let cancelButton = cancelButton.update(
|
||||
component: CameraButton(
|
||||
content: AnyComponentWithIdentity(
|
||||
@ -420,6 +427,9 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
}
|
||||
|
||||
let shutterState: ShutterButtonState
|
||||
if state.isTransitioning {
|
||||
shutterState = .transition
|
||||
} else {
|
||||
switch state.cameraState.recording {
|
||||
case .handsFree:
|
||||
shutterState = .stopRecording
|
||||
@ -433,6 +443,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
shutterState = .video
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let captureControls = captureControls.update(
|
||||
component: CaptureControlsComponent(
|
||||
@ -505,7 +516,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
isVideoRecording = true
|
||||
}
|
||||
|
||||
if isVideoRecording {
|
||||
if isVideoRecording && !state.isTransitioning {
|
||||
let duration = Int(state.cameraState.duration)
|
||||
let durationString = String(format: "%02d:%02d", (duration / 60) % 60, duration % 60)
|
||||
let timeLabel = timeLabel.update(
|
||||
@ -541,7 +552,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
let hintText: String?
|
||||
switch state.swipeHint {
|
||||
case .none:
|
||||
hintText = nil
|
||||
hintText = " "
|
||||
case .zoom:
|
||||
hintText = "Swipe up to zoom"
|
||||
case .lock:
|
||||
@ -553,10 +564,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
}
|
||||
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
|
||||
),
|
||||
component: HintLabelComponent(text: hintText),
|
||||
availableSize: availableSize,
|
||||
transition: .immediate
|
||||
)
|
||||
@ -569,7 +577,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
if case .none = state.cameraState.recording {
|
||||
if case .none = state.cameraState.recording, !state.isTransitioning {
|
||||
let modeControl = modeControl.update(
|
||||
component: ModeComponent(
|
||||
availableModes: [.photo, .video],
|
||||
|
||||
@ -11,6 +11,7 @@ enum ShutterButtonState: Equatable {
|
||||
case video
|
||||
case stopRecording
|
||||
case holdRecording(progress: Float)
|
||||
case transition
|
||||
}
|
||||
|
||||
private let maximumShutterSize = CGSize(width: 96.0, height: 96.0)
|
||||
@ -141,6 +142,12 @@ private final class ShutterButtonContentComponent: Component {
|
||||
innerCornerRadius = innerSize.height / 2.0
|
||||
ringSize = CGSize(width: 92.0, height: 92.0)
|
||||
recordingProgress = progress
|
||||
case .transition:
|
||||
innerColor = videoRedColor
|
||||
innerSize = CGSize(width: 60.0, height: 60.0)
|
||||
innerCornerRadius = innerSize.height / 2.0
|
||||
ringSize = CGSize(width: 68.0, height: 68.0)
|
||||
recordingProgress = 0.0
|
||||
}
|
||||
|
||||
self.ringLayer.fillColor = UIColor.clear.cgColor
|
||||
@ -573,6 +580,7 @@ final class CaptureControlsComponent: Component {
|
||||
let buttonSideInset: CGFloat = 28.0
|
||||
//let buttonMaxOffset: CGFloat = 100.0
|
||||
|
||||
var isTransitioning = false
|
||||
var isRecording = false
|
||||
var isHolding = false
|
||||
if case .stopRecording = component.shutterState {
|
||||
@ -580,6 +588,8 @@ final class CaptureControlsComponent: Component {
|
||||
} else if case .holdRecording = component.shutterState {
|
||||
isRecording = true
|
||||
isHolding = true
|
||||
} else if case .transition = component.shutterState {
|
||||
isTransitioning = true
|
||||
}
|
||||
|
||||
let galleryButtonSize = self.galleryButtonView.update(
|
||||
@ -615,8 +625,8 @@ final class CaptureControlsComponent: Component {
|
||||
transition.setBounds(view: galleryButtonView, bounds: CGRect(origin: .zero, size: galleryButtonFrame.size))
|
||||
transition.setPosition(view: galleryButtonView, position: galleryButtonFrame.center)
|
||||
|
||||
transition.setScale(view: galleryButtonView, scale: isRecording ? 0.1 : 1.0)
|
||||
transition.setAlpha(view: galleryButtonView, alpha: isRecording ? 0.0 : 1.0)
|
||||
transition.setScale(view: galleryButtonView, scale: isRecording || isTransitioning ? 0.1 : 1.0)
|
||||
transition.setAlpha(view: galleryButtonView, alpha: isRecording || isTransitioning ? 0.0 : 1.0)
|
||||
}
|
||||
|
||||
let _ = self.lockView.update(
|
||||
@ -678,13 +688,16 @@ final class CaptureControlsComponent: Component {
|
||||
}
|
||||
transition.setBounds(view: flipButtonView, bounds: CGRect(origin: .zero, size: flipButtonFrame.size))
|
||||
transition.setPosition(view: flipButtonView, position: flipButtonFrame.center)
|
||||
|
||||
transition.setScale(view: flipButtonView, scale: isTransitioning ? 0.01 : 1.0)
|
||||
transition.setAlpha(view: flipButtonView, alpha: isTransitioning ? 0.0 : 1.0)
|
||||
}
|
||||
|
||||
var blobState: ShutterBlobView.BlobState
|
||||
switch component.shutterState {
|
||||
case .generic:
|
||||
blobState = .generic
|
||||
case .video:
|
||||
case .video, .transition:
|
||||
blobState = .video
|
||||
case .stopRecording:
|
||||
blobState = .stopVideo
|
||||
@ -732,6 +745,8 @@ final class CaptureControlsComponent: Component {
|
||||
}
|
||||
transition.setBounds(view: shutterButtonView, bounds: CGRect(origin: .zero, size: shutterButtonFrame.size))
|
||||
transition.setPosition(view: shutterButtonView, position: shutterButtonFrame.center)
|
||||
transition.setScale(view: shutterButtonView, scale: isTransitioning ? 0.01 : 1.0)
|
||||
transition.setAlpha(view: shutterButtonView, alpha: isTransitioning ? 0.0 : 1.0)
|
||||
}
|
||||
|
||||
let guideSpacing: CGFloat = 9.0
|
||||
|
||||
@ -2,6 +2,7 @@ import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import MultilineTextComponent
|
||||
|
||||
extension CameraMode {
|
||||
var title: String {
|
||||
@ -161,3 +162,83 @@ final class ModeComponent: Component {
|
||||
return view.update(component: self, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
final class HintLabelComponent: Component {
|
||||
let text: String
|
||||
|
||||
init(
|
||||
text: String
|
||||
) {
|
||||
self.text = text
|
||||
}
|
||||
|
||||
static func ==(lhs: HintLabelComponent, rhs: HintLabelComponent) -> Bool {
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private var component: HintLabelComponent?
|
||||
private var componentView = ComponentView<Empty>()
|
||||
|
||||
init() {
|
||||
super.init(frame: CGRect())
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func update(component: HintLabelComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
let previousComponent = self.component
|
||||
self.component = component
|
||||
|
||||
if let previousText = previousComponent?.text, !previousText.isEmpty && previousText != component.text {
|
||||
if let componentView = self.componentView.view, let snapshotView = componentView.snapshotView(afterScreenUpdates: false) {
|
||||
snapshotView.frame = componentView.frame
|
||||
self.addSubview(snapshotView)
|
||||
snapshotView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
|
||||
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
||||
snapshotView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
|
||||
self.componentView.view?.removeFromSuperview()
|
||||
self.componentView = ComponentView<Empty>()
|
||||
}
|
||||
|
||||
let textSize = self.componentView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: component.text.uppercased(), font: Font.with(size: 14.0, design: .camera, weight: .semibold), textColor: .white)),
|
||||
horizontalAlignment: .center
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
if let view = self.componentView.view {
|
||||
if view.superview == nil {
|
||||
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
|
||||
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.addSubview(view)
|
||||
}
|
||||
|
||||
view.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - textSize.width) / 2.0), y: 0.0), size: textSize)
|
||||
}
|
||||
|
||||
return CGSize(width: availableSize.width, height: textSize.height)
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import Metal
|
||||
import MetalKit
|
||||
import ComponentFlow
|
||||
import Display
|
||||
import MetalImageView
|
||||
|
||||
private final class PropertyAnimation<T: Interpolatable> {
|
||||
let from: T
|
||||
@ -63,6 +64,7 @@ private final class AnimatableProperty<T: Interpolatable> {
|
||||
}
|
||||
|
||||
func tick(timestamp: Double) -> Bool {
|
||||
|
||||
guard let animation = self.animation, case let .curve(duration, curve) = animation.animation else {
|
||||
return false
|
||||
}
|
||||
@ -73,8 +75,7 @@ private final class AnimatableProperty<T: Interpolatable> {
|
||||
case .easeInOut:
|
||||
t = listViewAnimationCurveEaseInOut(t)
|
||||
case .spring:
|
||||
t = listViewAnimationCurveEaseInOut(t)
|
||||
//t = listViewAnimationCurveSystem(t)
|
||||
t = lookupSpringValue(t)
|
||||
case let .custom(x1, y1, x2, y2):
|
||||
t = bezierPoint(CGFloat(x1), CGFloat(y1), CGFloat(x2), CGFloat(y2), t)
|
||||
}
|
||||
@ -88,7 +89,69 @@ private final class AnimatableProperty<T: Interpolatable> {
|
||||
}
|
||||
}
|
||||
|
||||
final class ShutterBlobView: MTKView, MTKViewDelegate {
|
||||
private func lookupSpringValue(_ t: CGFloat) -> CGFloat {
|
||||
let table: [(CGFloat, CGFloat)] = [
|
||||
(0.0, 0.0),
|
||||
(0.0625, 0.1123005598783493),
|
||||
(0.125, 0.31598418951034546),
|
||||
(0.1875, 0.5103585720062256),
|
||||
(0.25, 0.6650152802467346),
|
||||
(0.3125, 0.777747631072998),
|
||||
(0.375, 0.8557760119438171),
|
||||
(0.4375, 0.9079672694206238),
|
||||
(0.5, 0.942038357257843),
|
||||
(0.5625, 0.9638798832893372),
|
||||
(0.625, 0.9776856303215027),
|
||||
(0.6875, 0.9863143563270569),
|
||||
(0.75, 0.991658091545105),
|
||||
(0.8125, 0.9949421286582947),
|
||||
(0.875, 0.9969474077224731),
|
||||
(0.9375, 0.9981651306152344),
|
||||
(1.0, 1.0)
|
||||
]
|
||||
|
||||
for i in 0 ..< table.count - 2 {
|
||||
let lhs = table[i]
|
||||
let rhs = table[i + 1]
|
||||
|
||||
if t >= lhs.0 && t <= rhs.0 {
|
||||
let fraction = (t - lhs.0) / (rhs.0 - lhs.0)
|
||||
let value = lhs.1 + fraction * (rhs.1 - lhs.1)
|
||||
return value
|
||||
}
|
||||
}
|
||||
return 1.0
|
||||
// print("---start---")
|
||||
// for i in 0 ..< 16 {
|
||||
// let j = Double(i) * 1.0 / 16.0
|
||||
// print("\(j) \(listViewAnimationCurveSystem(j))")
|
||||
// }
|
||||
// print("---end---")
|
||||
}
|
||||
|
||||
private class ShutterBlobLayer: MetalImageLayer {
|
||||
override public init() {
|
||||
super.init()
|
||||
|
||||
self.renderer.imageUpdated = { [weak self] image in
|
||||
self?.contents = image
|
||||
}
|
||||
}
|
||||
|
||||
override public init(layer: Any) {
|
||||
super.init()
|
||||
|
||||
if let layer = layer as? ShutterBlobLayer {
|
||||
self.contents = layer.contents
|
||||
}
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
final class ShutterBlobView: UIView {
|
||||
enum BlobState {
|
||||
case generic
|
||||
case video
|
||||
@ -147,7 +210,6 @@ final class ShutterBlobView: MTKView, MTKViewDelegate {
|
||||
|
||||
private let commandQueue: MTLCommandQueue
|
||||
private let drawPassthroughPipelineState: MTLRenderPipelineState
|
||||
private var viewportDimensions = CGSize(width: 1, height: 1)
|
||||
|
||||
private var displayLink: SharedDisplayLinkDriver.Link?
|
||||
|
||||
@ -162,6 +224,10 @@ final class ShutterBlobView: MTKView, MTKViewDelegate {
|
||||
|
||||
private(set) var state: BlobState = .generic
|
||||
|
||||
static override var layerClass: AnyClass {
|
||||
return ShutterBlobLayer.self
|
||||
}
|
||||
|
||||
public init?(test: Bool) {
|
||||
let mainBundle = Bundle(for: ShutterBlobView.self)
|
||||
|
||||
@ -207,17 +273,13 @@ final class ShutterBlobView: MTKView, MTKViewDelegate {
|
||||
|
||||
self.drawPassthroughPipelineState = try! device.makeRenderPipelineState(descriptor: pipelineStateDescriptor)
|
||||
|
||||
super.init(frame: CGRect(), device: device)
|
||||
super.init(frame: CGRect())
|
||||
|
||||
(self.layer as! ShutterBlobLayer).renderer.device = device
|
||||
|
||||
self.isOpaque = false
|
||||
self.backgroundColor = .clear
|
||||
|
||||
self.colorPixelFormat = .bgra8Unorm
|
||||
self.framebufferOnly = true
|
||||
|
||||
self.isPaused = true
|
||||
self.delegate = self
|
||||
|
||||
self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] in
|
||||
self?.tick()
|
||||
}
|
||||
@ -232,10 +294,6 @@ final class ShutterBlobView: MTKView, MTKViewDelegate {
|
||||
self.displayLink?.invalidate()
|
||||
}
|
||||
|
||||
public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
|
||||
self.viewportDimensions = size
|
||||
}
|
||||
|
||||
func updateState(_ state: BlobState, transition: Transition = .immediate) {
|
||||
guard self.state != state else {
|
||||
return
|
||||
@ -297,40 +355,56 @@ final class ShutterBlobView: MTKView, MTKViewDelegate {
|
||||
self.draw()
|
||||
}
|
||||
|
||||
override public func draw(_ rect: CGRect) {
|
||||
self.redraw(drawable: self.currentDrawable!)
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
self.tick()
|
||||
}
|
||||
|
||||
private func redraw(drawable: MTLDrawable) {
|
||||
guard let commandBuffer = self.commandQueue.makeCommandBuffer() else {
|
||||
private func getNextDrawable(layer: MetalImageLayer, drawableSize: CGSize) -> MetalImageLayer.Drawable? {
|
||||
layer.renderer.drawableSize = drawableSize
|
||||
return layer.renderer.nextDrawable()
|
||||
}
|
||||
|
||||
func draw() {
|
||||
guard let layer = self.layer as? MetalImageLayer else {
|
||||
return
|
||||
}
|
||||
self.updateAnimations()
|
||||
|
||||
let drawableSize = CGSize(width: self.bounds.width * UIScreen.main.scale, height: self.bounds.height * UIScreen.main.scale)
|
||||
|
||||
guard let drawable = self.getNextDrawable(layer: layer, drawableSize: drawableSize) else {
|
||||
return
|
||||
}
|
||||
|
||||
let renderPassDescriptor = self.currentRenderPassDescriptor!
|
||||
let renderPassDescriptor = MTLRenderPassDescriptor()
|
||||
renderPassDescriptor.colorAttachments[0].texture = drawable.texture
|
||||
renderPassDescriptor.colorAttachments[0].loadAction = .clear
|
||||
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0.0)
|
||||
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0)
|
||||
|
||||
guard let commandBuffer = self.commandQueue.makeCommandBuffer() else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
|
||||
return
|
||||
}
|
||||
|
||||
let viewportDimensions = self.viewportDimensions
|
||||
renderEncoder.setViewport(MTLViewport(originX: 0.0, originY: 0.0, width: viewportDimensions.width, height: viewportDimensions.height, znear: -1.0, zfar: 1.0))
|
||||
renderEncoder.setViewport(MTLViewport(originX: 0.0, originY: 0.0, width: drawableSize.width, height: drawableSize.height, znear: -1.0, zfar: 1.0))
|
||||
renderEncoder.setRenderPipelineState(self.drawPassthroughPipelineState)
|
||||
|
||||
let w = Float(1)
|
||||
let h = Float(1)
|
||||
var vertices: [Float] = [
|
||||
w, -h,
|
||||
-w, -h,
|
||||
-w, h,
|
||||
w, -h,
|
||||
-w, h,
|
||||
w, h
|
||||
1, -1,
|
||||
-1, -1,
|
||||
-1, 1,
|
||||
1, -1,
|
||||
-1, 1,
|
||||
1, 1
|
||||
]
|
||||
renderEncoder.setVertexBytes(&vertices, length: 4 * vertices.count, index: 0)
|
||||
|
||||
var resolution = simd_uint2(UInt32(viewportDimensions.width), UInt32(viewportDimensions.height))
|
||||
var resolution = simd_uint2(UInt32(drawableSize.width), UInt32(drawableSize.height))
|
||||
renderEncoder.setFragmentBytes(&resolution, length: MemoryLayout<simd_uint2>.size * 2, index: 0)
|
||||
|
||||
var primaryParameters = simd_float4(
|
||||
@ -350,17 +424,17 @@ final class ShutterBlobView: MTKView, MTKViewDelegate {
|
||||
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6, instanceCount: 1)
|
||||
renderEncoder.endEncoding()
|
||||
|
||||
commandBuffer.present(drawable)
|
||||
|
||||
var storedDrawable: MetalImageLayer.Drawable? = drawable
|
||||
commandBuffer.addCompletedHandler { _ in
|
||||
DispatchQueue.main.async {
|
||||
autoreleasepool {
|
||||
storedDrawable?.present(completion: {})
|
||||
storedDrawable = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
commandBuffer.commit()
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
self.tick()
|
||||
}
|
||||
|
||||
func draw(in view: MTKView) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user