Camera and media editor improvements

This commit is contained in:
Ilya Laktyushin
2023-05-12 21:14:19 +04:00
96 changed files with 8029 additions and 3232 deletions

View File

@@ -16,15 +16,18 @@ public final class MediaRecordingPanelComponent: Component {
public let audioRecorder: ManagedAudioRecorder?
public let videoRecordingStatus: InstantVideoControllerRecordingStatus?
public let cancelFraction: CGFloat
public let insets: UIEdgeInsets
public init(
audioRecorder: ManagedAudioRecorder?,
videoRecordingStatus: InstantVideoControllerRecordingStatus?,
cancelFraction: CGFloat
cancelFraction: CGFloat,
insets: UIEdgeInsets
) {
self.audioRecorder = audioRecorder
self.videoRecordingStatus = videoRecordingStatus
self.cancelFraction = cancelFraction
self.insets = insets
}
public static func ==(lhs: MediaRecordingPanelComponent, rhs: MediaRecordingPanelComponent) -> Bool {
@@ -37,6 +40,9 @@ public final class MediaRecordingPanelComponent: Component {
if lhs.cancelFraction != rhs.cancelFraction {
return false
}
if lhs.insets != rhs.insets {
return false
}
return true
}
@@ -234,7 +240,7 @@ public final class MediaRecordingPanelComponent: Component {
if indicatorView.superview == nil {
self.addSubview(indicatorView)
}
transition.setFrame(view: indicatorView, frame: CGRect(origin: CGPoint(x: 3.0, y: floor((availableSize.height - indicatorSize.height) * 0.5)), size: indicatorSize))
transition.setFrame(view: indicatorView, frame: CGRect(origin: CGPoint(x: 3.0, y: component.insets.top + floor((availableSize.height - component.insets.top - component.insets.bottom - indicatorSize.height) * 0.5)), size: indicatorSize))
}
let timerTextSize = self.timerText.update(
@@ -248,7 +254,7 @@ public final class MediaRecordingPanelComponent: Component {
self.addSubview(timerTextView)
timerTextView.layer.anchorPoint = CGPoint(x: 0.0, y: 0.5)
}
let timerTextFrame = CGRect(origin: CGPoint(x: 38.0, y: floor((availableSize.height - timerTextSize.height) * 0.5)), size: timerTextSize)
let timerTextFrame = CGRect(origin: CGPoint(x: 38.0, y: component.insets.top + floor((availableSize.height - component.insets.top - component.insets.bottom - timerTextSize.height) * 0.5)), size: timerTextSize)
transition.setPosition(view: timerTextView, position: CGPoint(x: timerTextFrame.minX, y: timerTextFrame.midY))
timerTextView.bounds = CGRect(origin: CGPoint(), size: timerTextFrame.size)
}
@@ -266,7 +272,7 @@ public final class MediaRecordingPanelComponent: Component {
containerSize: CGSize(width: max(30.0, availableSize.width - 100.0), height: 44.0)
)
var textFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - cancelTextSize.width) * 0.5), y: floor((availableSize.height - cancelTextSize.height) * 0.5)), size: cancelTextSize)
var textFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - cancelTextSize.width) * 0.5), y: component.insets.top + floor((availableSize.height - component.insets.top - component.insets.bottom - cancelTextSize.height) * 0.5)), size: cancelTextSize)
let bandingStart: CGFloat = 0.0
let bandedOffset = abs(component.cancelFraction) - bandingStart

View File

@@ -35,6 +35,8 @@ public final class MessageInputPanelComponent: Component {
public let reactionAction: ((UIView) -> Void)?
public let audioRecorder: ManagedAudioRecorder?
public let videoRecordingStatus: InstantVideoControllerRecordingStatus?
public let displayGradient: Bool
public let bottomInset: CGFloat
public init(
externalState: ExternalState,
@@ -49,7 +51,9 @@ public final class MessageInputPanelComponent: Component {
attachmentAction: (() -> Void)?,
reactionAction: ((UIView) -> Void)?,
audioRecorder: ManagedAudioRecorder?,
videoRecordingStatus: InstantVideoControllerRecordingStatus?
videoRecordingStatus: InstantVideoControllerRecordingStatus?,
displayGradient: Bool,
bottomInset: CGFloat
) {
self.externalState = externalState
self.context = context
@@ -64,6 +68,8 @@ public final class MessageInputPanelComponent: Component {
self.reactionAction = reactionAction
self.audioRecorder = audioRecorder
self.videoRecordingStatus = videoRecordingStatus
self.displayGradient = displayGradient
self.bottomInset = bottomInset
}
public static func ==(lhs: MessageInputPanelComponent, rhs: MessageInputPanelComponent) -> Bool {
@@ -91,6 +97,12 @@ public final class MessageInputPanelComponent: Component {
if lhs.videoRecordingStatus !== rhs.videoRecordingStatus {
return false
}
if lhs.displayGradient != rhs.displayGradient {
return false
}
if lhs.bottomInset != rhs.bottomInset {
return false
}
return true
}
@@ -99,8 +111,12 @@ public final class MessageInputPanelComponent: Component {
}
public final class View: UIView {
private let fieldBackgroundView: UIImageView
private let fieldBackgroundEffectView: UIVisualEffectView
private let fieldBackgroundView: BlurredBackgroundView
private let vibrancyEffectView: UIVisualEffectView
private let gradientView: UIImageView
private let bottomGradientView: UIView
private let placeholder = ComponentView<Empty>()
private let textField = ComponentView<Empty>()
private let textFieldExternalState = TextFieldComponent.ExternalState()
@@ -120,10 +136,23 @@ public final class MessageInputPanelComponent: Component {
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.fieldBackgroundView = UIImageView()
self.fieldBackgroundEffectView = UIVisualEffectView()
self.fieldBackgroundView = BlurredBackgroundView(color: UIColor(white: 0.0, alpha: 0.5), enableBlur: true)
let style: UIBlurEffect.Style = .dark
let blurEffect = UIBlurEffect(style: style)
let vibrancyEffect = UIVibrancyEffect(blurEffect: blurEffect)
let vibrancyEffectView = UIVisualEffectView(effect: vibrancyEffect)
self.vibrancyEffectView = vibrancyEffectView
self.gradientView = UIImageView()
self.bottomGradientView = UIView()
super.init(frame: frame)
self.addSubview(self.bottomGradientView)
self.addSubview(self.gradientView)
self.fieldBackgroundView.addSubview(self.vibrancyEffectView)
self.addSubview(self.fieldBackgroundView)
}
required init?(coder: NSCoder) {
@@ -152,35 +181,47 @@ public final class MessageInputPanelComponent: Component {
}
func update(component: MessageInputPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let baseHeight: CGFloat = 44.0
var insets = UIEdgeInsets(top: 5.0, left: 7.0, bottom: 5.0, right: 7.0)
var insets = UIEdgeInsets(top: 14.0, left: 7.0, bottom: 6.0, right: 7.0)
if let _ = component.attachmentAction {
insets.left = 41.0
}
if let _ = component.setMediaRecordingActive {
insets.right = 41.0
}
let fieldCornerRadius: CGFloat = 16.0
let baseFieldHeight: CGFloat = 40.0
self.component = component
self.state = state
let hasMediaRecording = component.audioRecorder != nil || component.videoRecordingStatus != nil
var placeholderAlignment: NSTextAlignment
switch component.style {
case .story:
if self.fieldBackgroundView.superview == nil {
self.fieldBackgroundView.image = generateStretchableFilledCircleImage(diameter: fieldCornerRadius * 2.0, color: nil, strokeColor: UIColor(white: 1.0, alpha: 0.16), strokeWidth: 1.0, backgroundColor: nil)
self.insertSubview(self.fieldBackgroundView, at: 0)
}
placeholderAlignment = .natural
case .editor:
if self.fieldBackgroundEffectView.superview == nil {
self.fieldBackgroundEffectView.clipsToBounds = true
self.fieldBackgroundEffectView.layer.cornerRadius = fieldCornerRadius
self.fieldBackgroundEffectView.effect = UIBlurEffect(style: .dark)
self.insertSubview(self.fieldBackgroundEffectView, at: 0)
}
placeholderAlignment = .center
let topGradientHeight: CGFloat = 32.0
if self.gradientView.image == nil {
let baseAlpha: CGFloat = 0.7
self.gradientView.image = generateImage(CGSize(width: insets.left + insets.right + baseFieldHeight, height: topGradientHeight + insets.top + baseFieldHeight + insets.bottom), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
var locations: [CGFloat] = []
var colors: [CGColor] = []
let numStops = 10
for i in 0 ..< numStops {
let step = 1.0 - CGFloat(i) / CGFloat(numStops - 1)
locations.append((1.0 - step))
let alphaStep: CGFloat = pow(step, 1.5)
colors.append(UIColor.black.withAlphaComponent(alphaStep * baseAlpha).cgColor)
}
if let gradient = CGGradient(colorsSpace: context.colorSpace, colors: colors as CFArray, locations: &locations) {
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: size.height), end: CGPoint(x: 0.0, y: 0.0), options: CGGradientDrawingOptions())
}
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: insets.left, y: topGradientHeight + insets.top), size: CGSize(width: baseFieldHeight, height: baseFieldHeight)).insetBy(dx: 3.0, dy: 3.0))
})?.resizableImage(withCapInsets: UIEdgeInsets(top: topGradientHeight + insets.top + baseFieldHeight * 0.5, left: insets.left + baseFieldHeight * 0.5, bottom: insets.bottom + baseFieldHeight * 0.5, right: insets.right + baseFieldHeight * 0.5))
self.bottomGradientView.backgroundColor = UIColor.black.withAlphaComponent(baseAlpha)
}
let availableTextFieldSize = CGSize(width: availableSize.width - insets.left - insets.right, height: availableSize.height - insets.top - insets.bottom)
@@ -190,8 +231,18 @@ public final class MessageInputPanelComponent: Component {
transition: .immediate,
component: AnyComponent(TextFieldComponent(
externalState: self.textFieldExternalState,
placeholder: component.placeholder,
placeholderAlignment: placeholderAlignment
placeholder: ""
)),
environment: {},
containerSize: availableTextFieldSize
)
let placeholderSize = self.placeholder.update(
transition: .immediate,
component: AnyComponent(Text(
text: component.placeholder,
font: Font.regular(17.0),
color: .white
)),
environment: {},
containerSize: availableTextFieldSize
@@ -201,13 +252,33 @@ public final class MessageInputPanelComponent: Component {
}
let fieldFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: availableSize.width - insets.left - insets.right, height: textFieldSize.height))
transition.setFrame(view: self.vibrancyEffectView, frame: CGRect(origin: CGPoint(), size: fieldFrame.size))
transition.setAlpha(view: self.vibrancyEffectView, alpha: (component.audioRecorder != nil || component.videoRecordingStatus != nil) ? 0.0 : 1.0)
transition.setFrame(view: self.fieldBackgroundView, frame: fieldFrame)
transition.setAlpha(view: self.fieldBackgroundView, alpha: (component.audioRecorder != nil || component.videoRecordingStatus != nil) ? 0.0 : 1.0)
self.fieldBackgroundView.update(size: fieldFrame.size, cornerRadius: baseFieldHeight * 0.5, transition: transition.containedViewLayoutTransition)
transition.setFrame(view: self.fieldBackgroundEffectView, frame: fieldFrame)
transition.setAlpha(view: self.fieldBackgroundEffectView, alpha: (component.audioRecorder != nil || component.videoRecordingStatus != nil) ? 0.0 : 1.0)
//let rightFieldInset: CGFloat = 34.0
let gradientFrame = CGRect(origin: CGPoint(x: 0.0, y: -topGradientHeight), size: CGSize(width: availableSize.width, height: topGradientHeight + fieldFrame.maxY + insets.bottom))
transition.setFrame(view: self.gradientView, frame: gradientFrame)
transition.setFrame(view: self.bottomGradientView, frame: CGRect(origin: CGPoint(x: 0.0, y: gradientFrame.maxY), size: CGSize(width: availableSize.width, height: component.bottomInset)))
transition.setAlpha(view: self.gradientView, alpha: component.displayGradient ? 1.0 : 0.0)
transition.setAlpha(view: self.bottomGradientView, alpha: component.displayGradient ? 1.0 : 0.0)
let placeholderOriginX: CGFloat
if self.textFieldExternalState.isEditing || component.style == .story {
placeholderOriginX = 16.0
} else {
placeholderOriginX = floorToScreenPixels((availableSize.width - placeholderSize.width) / 2.0)
}
let placeholderFrame = CGRect(origin: CGPoint(x: placeholderOriginX, y: floor((fieldFrame.height - placeholderSize.height) * 0.5)), size: placeholderSize)
if let placeholderView = self.placeholder.view {
if placeholderView.superview == nil {
placeholderView.layer.anchorPoint = CGPoint()
self.vibrancyEffectView.contentView.addSubview(placeholderView)
}
transition.setPosition(view: placeholderView, position: placeholderFrame.origin)
placeholderView.bounds = CGRect(origin: CGPoint(), size: placeholderFrame.size)
}
let size = CGSize(width: availableSize.width, height: textFieldSize.height + insets.top + insets.bottom)
@@ -230,15 +301,15 @@ public final class MessageInputPanelComponent: Component {
action: {
attachmentAction()
}
).minSize(CGSize(width: 41.0, height: baseHeight))),
).minSize(CGSize(width: 41.0, height: baseFieldHeight))),
environment: {},
containerSize: CGSize(width: 41.0, height: baseHeight)
containerSize: CGSize(width: 41.0, height: baseFieldHeight)
)
if let attachmentButtonView = self.attachmentButton.view {
if attachmentButtonView.superview == nil {
self.addSubview(attachmentButtonView)
}
transition.setFrame(view: attachmentButtonView, frame: CGRect(origin: CGPoint(x: floor((insets.left - attachmentButtonSize.width) * 0.5), y: size.height - baseHeight + floor((baseHeight - attachmentButtonSize.height) * 0.5)), size: attachmentButtonSize))
transition.setFrame(view: attachmentButtonView, frame: CGRect(origin: CGPoint(x: floor((insets.left - attachmentButtonSize.width) * 0.5), y: size.height - insets.bottom - baseFieldHeight + floor((baseFieldHeight - attachmentButtonSize.height) * 0.5)), size: attachmentButtonSize))
}
}
@@ -312,7 +383,7 @@ public final class MessageInputPanelComponent: Component {
} else {
inputActionButtonOriginX = size.width
}
transition.setFrame(view: inputActionButtonView, frame: CGRect(origin: CGPoint(x: inputActionButtonOriginX, y: size.height - baseHeight + floorToScreenPixels((baseHeight - inputActionButtonSize.height) * 0.5)), size: inputActionButtonSize))
transition.setFrame(view: inputActionButtonView, frame: CGRect(origin: CGPoint(x: inputActionButtonOriginX, y: size.height - insets.bottom - baseFieldHeight + floorToScreenPixels((baseFieldHeight - inputActionButtonSize.height) * 0.5)), size: inputActionButtonSize))
}
var fieldIconNextX = fieldFrame.maxX - 2.0
@@ -342,7 +413,7 @@ public final class MessageInputPanelComponent: Component {
transition.setPosition(view: stickerButtonView, position: stickerIconFrame.center)
transition.setBounds(view: stickerButtonView, bounds: CGRect(origin: CGPoint(), size: stickerIconFrame.size))
transition.setAlpha(view: stickerButtonView, alpha: self.textFieldExternalState.hasText ? 0.0 : 1.0)
transition.setAlpha(view: stickerButtonView, alpha: (self.textFieldExternalState.hasText || hasMediaRecording) ? 0.0 : 1.0)
transition.setScale(view: stickerButtonView, scale: self.textFieldExternalState.hasText ? 0.1 : 1.0)
fieldIconNextX -= stickerButtonSize.width + 2.0
@@ -375,23 +446,18 @@ public final class MessageInputPanelComponent: Component {
transition.setPosition(view: reactionButtonView, position: reactionIconFrame.center)
transition.setBounds(view: reactionButtonView, bounds: CGRect(origin: CGPoint(), size: reactionIconFrame.size))
transition.setAlpha(view: reactionButtonView, alpha: self.textFieldExternalState.hasText ? 0.0 : 1.0)
transition.setAlpha(view: reactionButtonView, alpha: (self.textFieldExternalState.hasText || hasMediaRecording) ? 0.0 : 1.0)
transition.setScale(view: reactionButtonView, scale: self.textFieldExternalState.hasText ? 0.1 : 1.0)
fieldIconNextX -= reactionButtonSize.width + 2.0
}
}
/*if let image = self.reactionIconView.image {
let stickerIconFrame = CGRect(origin: CGPoint(x: fieldIconNextX - image.size.width, y: fieldFrame.minY + floor((fieldFrame.height - image.size.height) * 0.5)), size: image.size)
transition.setPosition(view: self.reactionIconView, position: stickerIconFrame.center)
transition.setBounds(view: self.reactionIconView, bounds: CGRect(origin: CGPoint(), size: stickerIconFrame.size))
transition.setAlpha(view: self.reactionIconView, alpha: self.textFieldExternalState.hasText ? 0.0 : 1.0)
transition.setScale(view: self.reactionIconView, scale: self.textFieldExternalState.hasText ? 0.1 : 1.0)
fieldIconNextX -= image.size.width + 4.0
}*/
self.fieldBackgroundView.updateColor(color: self.textFieldExternalState.isEditing || component.style == .editor ? UIColor(white: 0.0, alpha: 0.5) : UIColor(white: 1.0, alpha: 0.09), transition: transition.containedViewLayoutTransition)
transition.setAlpha(view: self.fieldBackgroundView, alpha: hasMediaRecording ? 0.0 : 1.0)
if let placeholderView = self.placeholder.view {
placeholderView.isHidden = self.textFieldExternalState.hasText
}
component.externalState.isEditing = self.textFieldExternalState.isEditing
component.externalState.hasText = self.textFieldExternalState.hasText
@@ -419,7 +485,8 @@ public final class MessageInputPanelComponent: Component {
component: AnyComponent(MediaRecordingPanelComponent(
audioRecorder: component.audioRecorder,
videoRecordingStatus: component.videoRecordingStatus,
cancelFraction: self.mediaCancelFraction
cancelFraction: self.mediaCancelFraction,
insets: insets
)),
environment: {},
containerSize: size