Various improvements

This commit is contained in:
Ilya Laktyushin
2024-04-06 15:01:03 +04:00
parent 088cdc0939
commit 0598c49a89
79 changed files with 2478 additions and 463 deletions

View File

@@ -16,6 +16,7 @@ import Photos
import LottieAnimationComponent
import MessageInputPanelComponent
import DustEffect
import PlainButtonComponent
private final class MediaCutoutScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@@ -37,6 +38,30 @@ private final class MediaCutoutScreenComponent: Component {
}
return true
}
final class State: ComponentState {
enum ImageKey: Hashable {
case done
}
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 .done:
image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Done"), color: .white)!
}
cachedImages[key] = image
return image
}
}
}
func makeState() -> State {
return State()
}
public final class View: UIView {
private let buttonsContainerView = UIView()
@@ -45,7 +70,7 @@ private final class MediaCutoutScreenComponent: Component {
private let cancelButton = ComponentView<Empty>()
private let label = ComponentView<Empty>()
private let doneButton = ComponentView<Empty>()
private let fadeView = UIView()
private var outlineViews: [StickerCutoutOutlineView] = []
@@ -90,10 +115,13 @@ private final class MediaCutoutScreenComponent: Component {
component.mediaEditor.processImage { [weak self] originalImage, _ in
cutoutImage(from: originalImage, values: nil, target: .point(point), includeExtracted: false, completion: { [weak self] results in
Queue.mainQueue().async {
if let self, let component = self.component, let result = results.first, let maskImage = result.maskImage {
if let self, let _ = self.component, let result = results.first, let maskImage = result.maskImage, let controller = self.environment?.controller() as? MediaCutoutScreen {
if case let .image(mask, _) = maskImage {
self.playDissolveAnimation()
component.mediaEditor.setSegmentationMask(mask)
component.mediaEditor.setSegmentationMask(mask, updateCutout: true)
if let maskData = mask.pngData() {
controller.drawingView.setup(withDrawing: maskData)
}
}
}
}
@@ -103,13 +131,62 @@ private final class MediaCutoutScreenComponent: Component {
HapticFeedback().impact(.medium)
}
var initialOutlineValue: Float?
func animateInFromEditor() {
guard let controller = self.environment?.controller() as? MediaCutoutScreen else {
return
}
let mediaEditor = controller.mediaEditor
self.initialOutlineValue = mediaEditor.getToolValue(.stickerOutline) as? Float
mediaEditor.setToolValue(.stickerOutline, value: nil)
mediaEditor.isSegmentationMaskEnabled = false
mediaEditor.setOnNextDisplay { [weak controller] in
if let controller {
controller.previewView.mask = controller.maskWrapperView
}
}
self.buttonsBackgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.label.view?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
if let view = self.doneButton.view {
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
}
guard [.erase, .restore].contains(controller.mode) else {
return
}
controller.drawingView.isUserInteractionEnabled = true
if case .restore = controller.mode {
let overlayView = controller.overlayView
let backgroundView = controller.backgroundView
overlayView.alpha = 1.0
overlayView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
backgroundView.alpha = 0.0
backgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
}
}
private var animatingOut = false
func animateOutToEditor(completion: @escaping () -> Void) {
guard let controller = self.environment?.controller() as? MediaCutoutScreen else {
return
}
let mediaEditor = controller.mediaEditor
if let drawingImage = controller.drawingView.drawingImage {
mediaEditor.setSegmentationMask(drawingImage, andEnable: true, updateCutout: false)
}
let initialOutlineValue = self.initialOutlineValue
mediaEditor.setOnNextDisplay { [weak controller, weak mediaEditor] in
controller?.previewView.mask = nil
if let initialOutlineValue {
mediaEditor?.setToolValue(.stickerOutline, value: initialOutlineValue)
}
}
self.animatingOut = true
self.cancelButton.view?.isHidden = true
@@ -123,7 +200,25 @@ private final class MediaCutoutScreenComponent: Component {
})
self.label.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
if let view = self.doneButton.view {
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2)
}
self.state?.updated()
guard [.erase, .restore].contains(controller.mode) else {
return
}
controller.drawingView.isUserInteractionEnabled = false
if case .restore = controller.mode {
let overlayView = controller.overlayView
let backgroundView = controller.backgroundView
overlayView.alpha = 0.0
overlayView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
backgroundView.alpha = 1.0
backgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
public func playDissolveAnimation() {
@@ -147,10 +242,22 @@ private final class MediaCutoutScreenComponent: Component {
controller.requestDismiss(animated: true)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
if let controller = self.environment?.controller() as? MediaCutoutScreen, [.erase, .restore].contains(controller.mode), result == self.previewContainerView {
return nil//controller.previewView.superview
}
return result
}
func update(component: MediaCutoutScreenComponent, availableSize: CGSize, state: State, environment: Environment<ViewControllerComponentContainer.Environment>, transition: Transition) -> CGSize {
let environment = environment[ViewControllerComponentContainer.Environment.self].value
self.environment = environment
guard let controller = environment.controller() as? MediaCutoutScreen else {
return .zero
}
let isFirstTime = self.component == nil
self.component = component
self.state = state
@@ -199,11 +306,8 @@ private final class MediaCutoutScreenComponent: Component {
size: CGSize(width: 33.0, height: 33.0)
)
),
action: {
guard let controller = environment.controller() as? MediaCutoutScreen else {
return
}
controller.requestDismiss(animated: true)
action: { [weak controller] in
controller?.requestDismiss(animated: true)
}
)),
environment: {},
@@ -220,9 +324,47 @@ private final class MediaCutoutScreenComponent: Component {
transition.setFrame(view: cancelButtonView, frame: cancelButtonFrame)
}
if case .cutout = controller.mode {
} else {
let doneButtonSize = self.doneButton.update(
transition: transition,
component: AnyComponent(Button(
content: AnyComponent(Image(
image: state.image(.done),
size: CGSize(width: 33.0, height: 33.0)
)),
action: { [weak controller] in
controller?.requestDismiss(animated: true)
}
)),
environment: {},
containerSize: CGSize(width: 44.0, height: 44.0)
)
let doneButtonFrame = CGRect(
origin: CGPoint(x: availableSize.width - buttonSideInset - doneButtonSize.width, y: buttonBottomInset),
size: doneButtonSize
)
if let doneButtonView = self.doneButton.view {
if doneButtonView.superview == nil {
self.buttonsContainerView.addSubview(doneButtonView)
}
transition.setFrame(view: doneButtonView, frame: doneButtonFrame)
}
}
let helpText: String
switch controller.mode {
case .cutout:
helpText = "Tap on an object to cut it out"
case .erase:
helpText = "Erase parts of this sticker"
case .restore:
helpText = "Restore parts of this sticker"
}
let labelSize = self.label.update(
transition: transition,
component: AnyComponent(Text(text: "Tap on an object to cut it out", font: Font.regular(17.0), color: .white)),
component: AnyComponent(Text(text: helpText, font: Font.regular(17.0), color: UIColor(rgb: 0x8d8d93))),
environment: {},
containerSize: CGSize(width: availableSize.width - 88.0, height: 44.0)
)
@@ -241,38 +383,41 @@ private final class MediaCutoutScreenComponent: Component {
transition.setFrame(view: self.buttonsBackgroundView, frame: CGRect(origin: .zero, size: buttonsContainerFrame.size))
transition.setFrame(view: self.previewContainerView, frame: previewContainerFrame)
for view in self.outlineViews {
transition.setFrame(view: view, frame: previewContainerFrame)
}
let frameWidth = floorToScreenPixels(previewContainerFrame.width * 0.97)
self.fadeView.frame = CGRect(x: floorToScreenPixels((previewContainerFrame.width - frameWidth) / 2.0), y: previewContainerFrame.minY + floorToScreenPixels((previewContainerFrame.height - frameWidth) / 2.0), width: frameWidth, height: frameWidth)
self.fadeView.layer.cornerRadius = frameWidth / 8.0
if isFirstTime {
let values = component.mediaEditor.values
component.mediaEditor.processImage { originalImage, editedImage in
cutoutImage(from: originalImage, editedImage: editedImage, values: values, target: .all, completion: { results in
Queue.mainQueue().async {
if !results.isEmpty {
for result in results {
if let extractedImage = result.extractedImage, let maskImage = result.maskImage {
if case let .image(image, _) = extractedImage, case let .image(_, mask) = maskImage {
let outlineView = StickerCutoutOutlineView(frame: self.previewContainerView.frame)
outlineView.update(image: image, maskImage: mask, size: self.previewContainerView.bounds.size, values: values)
self.insertSubview(outlineView, belowSubview: self.previewContainerView)
self.outlineViews.append(outlineView)
if case .cutout = controller.mode {
for view in self.outlineViews {
transition.setFrame(view: view, frame: previewContainerFrame)
}
let frameWidth = floorToScreenPixels(previewContainerFrame.width * 0.97)
self.fadeView.frame = CGRect(x: floorToScreenPixels((previewContainerFrame.width - frameWidth) / 2.0), y: previewContainerFrame.minY + floorToScreenPixels((previewContainerFrame.height - frameWidth) / 2.0), width: frameWidth, height: frameWidth)
self.fadeView.layer.cornerRadius = frameWidth / 8.0
if isFirstTime {
let values = component.mediaEditor.values
component.mediaEditor.processImage { originalImage, editedImage in
cutoutImage(from: originalImage, editedImage: editedImage, values: values, target: .all, completion: { results in
Queue.mainQueue().async {
if !results.isEmpty {
for result in results {
if let extractedImage = result.extractedImage, let maskImage = result.maskImage {
if case let .image(image, _) = extractedImage, case let .image(_, mask) = maskImage {
let outlineView = StickerCutoutOutlineView(frame: self.previewContainerView.frame)
outlineView.update(image: image, maskImage: mask, size: self.previewContainerView.bounds.size, values: values)
self.insertSubview(outlineView, belowSubview: self.previewContainerView)
self.outlineViews.append(outlineView)
}
}
}
self.state?.updated(transition: .easeInOut(duration: 0.4))
}
self.state?.updated(transition: .easeInOut(duration: 0.4))
}
}
})
})
}
} else {
transition.setAlpha(view: self.fadeView, alpha: !self.outlineViews.isEmpty ? 1.0 : 0.0)
}
} else {
transition.setAlpha(view: self.fadeView, alpha: !self.outlineViews.isEmpty ? 1.0 : 0.0)
}
return availableSize
}
@@ -282,12 +427,12 @@ private final class MediaCutoutScreenComponent: Component {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: Transition) -> CGSize {
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<ViewControllerComponentContainer.Environment>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public final class MediaCutoutScreen: ViewController {
final class MediaCutoutScreen: ViewController {
fileprivate final class Node: ViewControllerTracingNode, ASGestureRecognizerDelegate {
private weak var controller: MediaCutoutScreen?
private let context: AccountContext
@@ -336,6 +481,14 @@ public final class MediaCutoutScreen: ViewController {
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
if result === self.view {
return nil
}
return result
}
func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: Transition) {
guard let controller = self.controller else {
return
@@ -412,17 +565,42 @@ public final class MediaCutoutScreen: ViewController {
}
fileprivate let context: AccountContext
fileprivate let mode: Mode
fileprivate let mediaEditor: MediaEditor
fileprivate let maskWrapperView: UIView
fileprivate let previewView: MediaEditorPreviewView
fileprivate let drawingView: DrawingView
fileprivate let overlayView: UIView
fileprivate let backgroundView: UIView
public var dismissed: () -> Void = {}
var dismissed: () -> Void = {}
private var initialValues: MediaEditorValues
public init(context: AccountContext, mediaEditor: MediaEditor, previewView: MediaEditorPreviewView) {
enum Mode {
case cutout
case erase
case restore
}
init(
context: AccountContext,
mode: Mode,
mediaEditor: MediaEditor,
previewView: MediaEditorPreviewView,
maskWrapperView: UIView,
drawingView: DrawingView,
overlayView: UIView,
backgroundView: UIView
) {
self.context = context
self.mode = mode
self.mediaEditor = mediaEditor
self.previewView = previewView
self.maskWrapperView = maskWrapperView
self.drawingView = drawingView
self.overlayView = overlayView
self.backgroundView = backgroundView
self.initialValues = mediaEditor.values.makeCopy()
super.init(navigationBarPresentationData: nil)
@@ -431,13 +609,21 @@ public final class MediaCutoutScreen: ViewController {
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.statusBar.statusBarStyle = .White
if let toolState = drawingView.appliedToolState {
if case .erase = mode {
drawingView.updateToolState(toolState.withUpdatedColor(DrawingColor(color: .black)))
} else if case .restore = mode {
drawingView.updateToolState(toolState.withUpdatedColor(DrawingColor(color: .white)))
}
}
}
required public init(coder aDecoder: NSCoder) {
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func loadDisplayNode() {
override func loadDisplayNode() {
self.displayNode = Node(controller: self)
super.displayNodeDidLoad()
@@ -451,7 +637,7 @@ public final class MediaCutoutScreen: ViewController {
})
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
(self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: Transition(transition))