Clean up image editor.

This commit is contained in:
Matthew Chen 2019-02-28 14:23:11 -05:00
parent 97660e0a11
commit b64be3aa73
6 changed files with 172 additions and 229 deletions

View file

@ -4,16 +4,6 @@
import UIKit
//@objc
//public protocol ImageEditorViewDelegate: class {
// func imageEditor(presentFullScreenOverlay viewController: UIViewController,
// withNavigation: Bool)
// func imageEditorPresentCaptionView()
// func imageEditorUpdateNavigationBar()
//}
// MARK: -
@objc
public protocol ImageEditorBrushViewControllerDelegate: class {
func brushDidComplete()
@ -30,15 +20,17 @@ public class ImageEditorBrushViewController: OWSViewController {
private let canvasView: ImageEditorCanvasView
private let paletteView = ImageEditorPaletteView()
private let paletteView: ImageEditorPaletteView
private var brushGestureRecognizer: ImageEditorPanGestureRecognizer?
init(delegate: ImageEditorBrushViewControllerDelegate,
model: ImageEditorModel) {
model: ImageEditorModel,
currentColor: ImageEditorColor) {
self.delegate = delegate
self.model = model
self.canvasView = ImageEditorCanvasView(model: model)
self.paletteView = ImageEditorPaletteView(currentColor: currentColor)
super.init(nibName: nil, bundle: nil)
@ -55,6 +47,7 @@ public class ImageEditorBrushViewController: OWSViewController {
public override func loadView() {
self.view = UIView()
self.view.backgroundColor = .black
self.view.isOpaque = true
canvasView.configureSubviews()
self.view.addSubview(canvasView)
@ -63,7 +56,7 @@ public class ImageEditorBrushViewController: OWSViewController {
paletteView.delegate = self
self.view.addSubview(paletteView)
paletteView.autoVCenterInSuperview()
paletteView.autoPinEdge(toSuperviewEdge: .leading, withInset: 20)
paletteView.autoPinEdge(toSuperviewEdge: .trailing, withInset: 20)
self.view.isUserInteractionEnabled = true
@ -103,12 +96,6 @@ public class ImageEditorBrushViewController: OWSViewController {
updateNavigationBar(navigationBarItems: navigationBarItems)
}
private var currentColor: UIColor {
get {
return paletteView.selectedColor
}
}
// MARK: - Actions
@objc func didTapUndo(sender: UIButton) {
@ -168,7 +155,7 @@ public class ImageEditorBrushViewController: OWSViewController {
self.currentStrokeSamples.append(newSample)
}
let strokeColor = currentColor
let strokeColor = paletteView.selectedValue.color
// TODO: Tune stroke width.
let unitStrokeWidth = ImageEditorStrokeItem.defaultUnitStrokeWidth()

View file

@ -10,11 +10,60 @@ public protocol ImageEditorPaletteViewDelegate: class {
// MARK: -
@objc
public class ImageEditorColor: NSObject {
public let color: UIColor
// Colors are chosen from a spectrum of colors.
// This unit value represents the location of the
// color within that spectrum.
public let palettePhase: CGFloat
public var cgColor: CGColor {
return color.cgColor
}
public required init(color: UIColor, palettePhase: CGFloat) {
self.color = color
self.palettePhase = palettePhase
}
public class func defaultColor() -> ImageEditorColor {
return ImageEditorColor(color: UIColor(rgbHex: 0xffffff), palettePhase: 0)
}
public static var gradientUIColors: [UIColor] {
return [
UIColor(rgbHex: 0xffffff),
UIColor(rgbHex: 0xff0000),
UIColor(rgbHex: 0xff00ff),
UIColor(rgbHex: 0x0000ff),
UIColor(rgbHex: 0x00ffff),
UIColor(rgbHex: 0x00ff00),
UIColor(rgbHex: 0xffff00),
UIColor(rgbHex: 0xff5500),
UIColor(rgbHex: 0x000000)
]
}
public static var gradientCGColors: [CGColor] {
return gradientUIColors.map({ (color) in
return color.cgColor
})
}
}
// MARK: -
public class ImageEditorPaletteView: UIView {
public weak var delegate: ImageEditorPaletteViewDelegate?
public required init() {
public var selectedValue: ImageEditorColor
public required init(currentColor: ImageEditorColor) {
self.selectedValue = currentColor
super.init(frame: .zero)
createContents()
@ -27,9 +76,6 @@ public class ImageEditorPaletteView: UIView {
// MARK: - Views
// The actual default is selected later.
public var selectedColor = UIColor.white
private let imageView = UIImageView()
private let selectionView = UIView()
private let selectionWrapper = OWSLayerView()
@ -84,36 +130,36 @@ public class ImageEditorPaletteView: UIView {
// 0 = the color at the top of the image is selected.
// 1 = the color at the bottom of the image is selected.
private let selectionSize: CGFloat = 20
private var selectionAlpha: CGFloat = 0
private func selectColor(atLocationY y: CGFloat) {
selectionAlpha = y.inverseLerp(0, imageView.height(), shouldClamp: true)
let palettePhase = y.inverseLerp(0, imageView.height(), shouldClamp: true)
self.selectedValue = value(for: palettePhase)
updateState()
delegate?.selectedColorDidChange()
}
private func updateState() {
var selectedColor = UIColor.white
if let image = imageView.image {
if let imageColor = image.color(atLocation: CGPoint(x: CGFloat(image.size.width) * 0.5, y: CGFloat(image.size.height) * selectionAlpha)) {
selectedColor = imageColor
} else {
owsFailDebug("Couldn't determine image color.")
}
} else {
private func value(for palettePhase: CGFloat) -> ImageEditorColor {
guard let image = imageView.image else {
owsFailDebug("Missing image.")
return ImageEditorColor.defaultColor()
}
self.selectedColor = selectedColor
guard let color = image.color(atLocation: CGPoint(x: CGFloat(image.size.width) * 0.5, y: CGFloat(image.size.height) * palettePhase)) else {
owsFailDebug("Missing color.")
return ImageEditorColor.defaultColor()
}
return ImageEditorColor(color: color, palettePhase: palettePhase)
}
selectionView.backgroundColor = selectedColor
private func updateState() {
selectionView.backgroundColor = selectedValue.color
guard let selectionConstraint = selectionConstraint else {
owsFailDebug("Missing selectionConstraint.")
return
}
let selectionY = selectionWrapper.height() * selectionAlpha
let selectionY = selectionWrapper.height() * selectedValue.palettePhase
selectionConstraint.constant = selectionY
}
@ -142,17 +188,7 @@ public class ImageEditorPaletteView: UIView {
gradientView.layer.addSublayer(gradientLayer)
gradientLayer.frame = gradientBounds
// See: https://github.com/signalapp/Signal-Android/blob/master/res/values/arrays.xml#L267
gradientLayer.colors = [
UIColor(rgbHex: 0xffffff).cgColor,
UIColor(rgbHex: 0xff0000).cgColor,
UIColor(rgbHex: 0xff00ff).cgColor,
UIColor(rgbHex: 0x0000ff).cgColor,
UIColor(rgbHex: 0x00ffff).cgColor,
UIColor(rgbHex: 0x00ff00).cgColor,
UIColor(rgbHex: 0xffff00).cgColor,
UIColor(rgbHex: 0xff5500).cgColor,
UIColor(rgbHex: 0x000000).cgColor
]
gradientLayer.colors = ImageEditorColor.gradientCGColors
gradientLayer.startPoint = CGPoint.zero
gradientLayer.endPoint = CGPoint(x: 0, y: gradientSize.height)
gradientLayer.endPoint = CGPoint(x: 0, y: 1.0)

View file

@ -11,7 +11,7 @@ public class ImageEditorTextItem: ImageEditorItem {
public let text: String
@objc
public let color: UIColor
public let color: ImageEditorColor
@objc
public let font: UIFont
@ -60,7 +60,7 @@ public class ImageEditorTextItem: ImageEditorItem {
@objc
public init(text: String,
color: UIColor,
color: ImageEditorColor,
font: UIFont,
fontReferenceImageWidth: CGFloat,
unitCenter: ImageEditorSample = ImageEditorSample(x: 0.5, y: 0.5),
@ -81,7 +81,7 @@ public class ImageEditorTextItem: ImageEditorItem {
private init(itemId: String,
text: String,
color: UIColor,
color: ImageEditorColor,
font: UIFont,
fontReferenceImageWidth: CGFloat,
unitCenter: ImageEditorSample,
@ -101,17 +101,17 @@ public class ImageEditorTextItem: ImageEditorItem {
}
@objc
public class func empty(withColor color: UIColor, unitWidth: CGFloat, fontReferenceImageWidth: CGFloat) -> ImageEditorTextItem {
public class func empty(withColor color: ImageEditorColor, unitWidth: CGFloat, fontReferenceImageWidth: CGFloat) -> ImageEditorTextItem {
// TODO: Tune the default font size.
let font = UIFont.boldSystemFont(ofSize: 30.0)
return ImageEditorTextItem(text: "", color: color, font: font, fontReferenceImageWidth: fontReferenceImageWidth, unitWidth: unitWidth)
}
@objc
public func copy(withText newText: String) -> ImageEditorTextItem {
public func copy(withText newText: String, color newColor: ImageEditorColor) -> ImageEditorTextItem {
return ImageEditorTextItem(itemId: itemId,
text: newText,
color: color,
color: newColor,
font: font,
fontReferenceImageWidth: fontReferenceImageWidth,
unitCenter: unitCenter,

View file

@ -92,7 +92,7 @@ private class VAlignTextView: UITextView {
@objc
public protocol ImageEditorTextViewControllerDelegate: class {
func textEditDidComplete(textItem: ImageEditorTextItem, text: String?)
func textEditDidComplete(textItem: ImageEditorTextItem, text: String?, color: ImageEditorColor)
func textEditDidCancel()
}
@ -106,14 +106,24 @@ public class ImageEditorTextViewController: OWSViewController, VAlignTextViewDel
private let maxTextWidthPoints: CGFloat
private let textView = VAlignTextView(alignment: .bottom)
private let textView = VAlignTextView(alignment: .center)
private let model: ImageEditorModel
private let canvasView: ImageEditorCanvasView
private let paletteView: ImageEditorPaletteView
init(delegate: ImageEditorTextViewControllerDelegate,
model: ImageEditorModel,
textItem: ImageEditorTextItem,
maxTextWidthPoints: CGFloat) {
self.delegate = delegate
self.model = model
self.textItem = textItem
self.maxTextWidthPoints = maxTextWidthPoints
self.canvasView = ImageEditorCanvasView(model: model)
self.paletteView = ImageEditorPaletteView(currentColor: textItem.color)
super.init(nibName: nil, bundle: nil)
@ -131,43 +141,63 @@ public class ImageEditorTextViewController: OWSViewController, VAlignTextViewDel
super.viewWillAppear(animated)
textView.becomeFirstResponder()
self.view.layoutSubviews()
}
public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
textView.becomeFirstResponder()
self.view.layoutSubviews()
}
public override func loadView() {
self.view = UIView()
self.view.backgroundColor = UIColor(white: 0.5, alpha: 0.5)
self.view.backgroundColor = .black
self.view.isOpaque = true
canvasView.configureSubviews()
self.view.addSubview(canvasView)
canvasView.autoPinEdgesToSuperviewEdges()
let tintView = UIView()
tintView.backgroundColor = UIColor(white: 0, alpha: 0.33)
tintView.isOpaque = false
self.view.addSubview(tintView)
tintView.autoPinEdgesToSuperviewEdges()
tintView.layer.opacity = 0
UIView.animate(withDuration: 0.25, animations: {
tintView.layer.opacity = 1
}, completion: { (_) in
tintView.layer.opacity = 1
})
configureTextView()
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .stop,
target: self,
action: #selector(didTapBackButton))
self.view.layoutMargins = UIEdgeInsets(top: 16, left: 20, bottom: 16, right: 20)
self.view.addSubview(textView)
textView.autoPinTopToSuperviewMargin()
textView.autoHCenterInSuperview()
// In order to have text wrapping be as WYSIWYG as possible, we limit the text view
// to the max text width on the image.
// let maxTextWidthPoints = max(textItem.widthPoints, 200)
// textView.autoSetDimension(.width, toSize: maxTextWidthPoints, relation: .lessThanOrEqual)
// textView.autoPinEdge(toSuperviewMargin: .leading, relation: .greaterThanOrEqual)
// textView.autoPinEdge(toSuperviewMargin: .trailing, relation: .greaterThanOrEqual)
textView.autoPinEdge(toSuperviewMargin: .leading)
textView.autoPinEdge(toSuperviewMargin: .trailing)
self.autoPinView(toBottomOfViewControllerOrKeyboard: textView, avoidNotch: true)
// TODO: Honor old color state.
paletteView.delegate = self
self.view.addSubview(paletteView)
paletteView.autoVCenterInSuperview()
paletteView.autoPinEdge(toSuperviewEdge: .trailing, withInset: 20)
// This will determine the text view's size.
paletteView.autoPinEdge(.leading, to: .trailing, of: textView, withOffset: 8)
updateNavigationBar()
}
private func configureTextView() {
textView.text = textItem.text
textView.font = textItem.font
textView.textColor = textItem.color
textView.textColor = textItem.color.color
textView.isEditable = true
textView.backgroundColor = .clear
@ -186,23 +216,37 @@ public class ImageEditorTextViewController: OWSViewController, VAlignTextViewDel
textView.contentInset = .zero
}
private func updateNavigationBar() {
let undoButton = navigationBarButton(imageName: "image_editor_undo",
selector: #selector(didTapUndo(sender:)))
let doneButton = navigationBarButton(imageName: "image_editor_checkmark_full",
selector: #selector(didTapDone(sender:)))
let navigationBarItems = [undoButton, doneButton]
updateNavigationBar(navigationBarItems: navigationBarItems)
}
// MARK: - Events
@objc public func didTapBackButton() {
@objc func didTapUndo(sender: UIButton) {
Logger.verbose("")
// TODO: Honor color state.
self.delegate?.textEditDidCancel()
self.dismiss(animated: false) {
// Do nothing.
}
}
@objc func didTapDone(sender: UIButton) {
Logger.verbose("")
completeAndDismiss()
}
private func completeAndDismiss() {
// Before we take a screenshot, make sure selection state
// auto-complete suggestions, cursor don't affect screenshot.
textView.resignFirstResponder()
if textView.isFirstResponder {
owsFailDebug("Text view is still first responder.")
}
textView.selectedTextRange = nil
self.delegate?.textEditDidComplete(textItem: textItem, text: textView.text)
self.delegate?.textEditDidComplete(textItem: textItem, text: textView.text, color: paletteView.selectedValue)
self.dismiss(animated: false) {
// Do nothing.
@ -215,3 +259,11 @@ public class ImageEditorTextViewController: OWSViewController, VAlignTextViewDel
completeAndDismiss()
}
}
// MARK: -
extension ImageEditorTextViewController: ImageEditorPaletteViewDelegate {
public func selectedColorDidChange() {
self.textView.textColor = self.paletteView.selectedValue.color
}
}

View file

@ -25,30 +25,8 @@ public class ImageEditorView: UIView {
private let canvasView: ImageEditorCanvasView
private let paletteView = ImageEditorPaletteView()
enum EditorMode: String {
// This is the default mode. It is used for interacting with text items.
case none
case brush
case text
}
private var editorMode = EditorMode.none {
didSet {
AssertIsOnMainThread()
updateButtons()
updateGestureState()
delegate?.imageEditorUpdateNavigationBar()
}
}
private var currentColor: UIColor {
get {
return paletteView.selectedColor
}
}
// TODO: We could hang this on the model or make this static.
private var currentColor = ImageEditorColor.defaultColor()
@objc
public required init(model: ImageEditorModel, delegate: ImageEditorViewDelegate) {
@ -78,8 +56,6 @@ public class ImageEditorView: UIView {
self.addSubview(canvasView)
canvasView.autoPinEdgesToSuperviewEdges()
paletteView.delegate = self
self.isUserInteractionEnabled = true
let moveTextGestureRecognizer = ImageEditorPanGestureRecognizer(target: self, action: #selector(handleMoveTextGesture(_:)))
@ -102,60 +78,16 @@ public class ImageEditorView: UIView {
// editorGestureRecognizer.require(toFail: tapGestureRecognizer)
// editorGestureRecognizer.require(toFail: pinchGestureRecognizer)
updateGestureState()
return true
}
private func commitTextEditingChanges(textItem: ImageEditorTextItem, textView: UITextView) {
AssertIsOnMainThread()
guard let text = textView.text?.ows_stripped(),
text.count > 0 else {
model.remove(item: textItem)
return
}
// Model items are immutable; we _replace_ the item rather than modify it.
let newItem = textItem.copy(withText: text)
if model.has(itemForId: textItem.itemId) {
model.replace(item: newItem, suppressUndo: false)
} else {
model.append(item: newItem)
}
}
// TODO: Should this method be private?
@objc
public func addControls(to containerView: UIView,
viewController: UIViewController) {
containerView.addSubview(paletteView)
paletteView.autoVCenterInSuperview()
paletteView.autoPinLeadingToSuperviewMargin(withInset: 10)
updateButtons()
delegate?.imageEditorUpdateNavigationBar()
}
private func updateButtons() {
var hasPalette = false
switch editorMode {
case .text:
// TODO:
hasPalette = true
break
case .brush:
hasPalette = true
case .none:
hasPalette = false
break
}
paletteView.isHidden = !hasPalette
}
// MARK: - Navigation Bar
public func navigationBarItems() -> [UIView] {
@ -170,17 +102,10 @@ public class ImageEditorView: UIView {
let captionButton = navigationBarButton(imageName: "image_editor_caption",
selector: #selector(didTapCaption(sender:)))
switch editorMode {
case .text:
return []
case .brush:
return []
case .none:
if model.canUndo() {
return [undoButton, newTextButton, brushButton, cropButton, captionButton]
} else {
return [newTextButton, brushButton, cropButton, captionButton]
}
if model.canUndo() {
return [undoButton, newTextButton, brushButton, cropButton, captionButton]
} else {
return [newTextButton, brushButton, cropButton, captionButton]
}
}
@ -198,9 +123,7 @@ public class ImageEditorView: UIView {
@objc func didTapBrush(sender: UIButton) {
Logger.verbose("")
self.editorMode = .brush
let brushView = ImageEditorBrushViewController(delegate: self, model: model)
let brushView = ImageEditorBrushViewController(delegate: self, model: model, currentColor: currentColor)
self.delegate?.imageEditor(presentFullScreenOverlay: brushView,
withNavigation: true)
}
@ -233,44 +156,10 @@ public class ImageEditorView: UIView {
Logger.verbose("")
delegate?.imageEditorPresentCaptionView()
// // TODO:
// let maxTextWidthPoints = model.srcImageSizePixels.width * ImageEditorTextItem.kDefaultUnitWidth
// // let maxTextWidthPoints = canvasView.imageView.width() * ImageEditorTextItem.kDefaultUnitWidth
//
// let textEditor = ImageEditorTextViewController(delegate: self, textItem: textItem, maxTextWidthPoints: maxTextWidthPoints)
// self.delegate?.imageEditor(presentFullScreenOverlay: textEditor,
// withNavigation: true)
// TODO:
}
@objc func didTapDone(sender: UIButton) {
Logger.verbose("")
self.editorMode = .none
}
// MARK: - Gestures
private func updateGestureState() {
AssertIsOnMainThread()
switch editorMode {
case .none:
moveTextGestureRecognizer?.isEnabled = true
tapGestureRecognizer?.isEnabled = true
pinchGestureRecognizer?.isEnabled = true
case .brush:
// Brush strokes can start and end (and return from) outside the view.
moveTextGestureRecognizer?.isEnabled = false
tapGestureRecognizer?.isEnabled = false
pinchGestureRecognizer?.isEnabled = false
case .text:
moveTextGestureRecognizer?.isEnabled = false
tapGestureRecognizer?.isEnabled = false
pinchGestureRecognizer?.isEnabled = false
}
}
// MARK: - Tap Gesture
@ -483,7 +372,7 @@ public class ImageEditorView: UIView {
self.currentStrokeSamples.append(newSample)
}
let strokeColor = currentColor
let strokeColor = currentColor.color
// TODO: Tune stroke width.
let unitStrokeWidth = ImageEditorStrokeItem.defaultUnitStrokeWidth()
@ -527,13 +416,14 @@ public class ImageEditorView: UIView {
private func edit(textItem: ImageEditorTextItem) {
Logger.verbose("")
self.editorMode = .text
// TODO:
let maxTextWidthPoints = model.srcImageSizePixels.width * ImageEditorTextItem.kDefaultUnitWidth
// let maxTextWidthPoints = canvasView.imageView.width() * ImageEditorTextItem.kDefaultUnitWidth
let textEditor = ImageEditorTextViewController(delegate: self, textItem: textItem, maxTextWidthPoints: maxTextWidthPoints)
let textEditor = ImageEditorTextViewController(delegate: self,
model: model,
textItem: textItem,
maxTextWidthPoints: maxTextWidthPoints)
self.delegate?.imageEditor(presentFullScreenOverlay: textEditor,
withNavigation: true)
}
@ -543,8 +433,6 @@ public class ImageEditorView: UIView {
private func presentCropTool() {
Logger.verbose("")
self.editorMode = .none
guard let srcImage = canvasView.loadSrcImage() else {
owsFailDebug("Couldn't load src image.")
return
@ -573,10 +461,6 @@ extension ImageEditorView: UIGestureRecognizerDelegate {
owsFailDebug("Unexpected gesture.")
return false
}
guard editorMode == .none else {
// We only filter touches when in default mode.
return true
}
let location = touch.location(in: canvasView.gestureReferenceView)
let isInTextArea = self.textLayer(forLocation: location) != nil
@ -590,14 +474,10 @@ extension ImageEditorView: ImageEditorModelObserver {
public func imageEditorModelDidChange(before: ImageEditorContents,
after: ImageEditorContents) {
updateButtons()
delegate?.imageEditorUpdateNavigationBar()
}
public func imageEditorModelDidChange(changedItemIds: [String]) {
updateButtons()
delegate?.imageEditorUpdateNavigationBar()
}
}
@ -606,11 +486,9 @@ extension ImageEditorView: ImageEditorModelObserver {
extension ImageEditorView: ImageEditorTextViewControllerDelegate {
public func textEditDidComplete(textItem: ImageEditorTextItem, text: String?) {
public func textEditDidComplete(textItem: ImageEditorTextItem, text: String?, color: ImageEditorColor) {
AssertIsOnMainThread()
self.editorMode = .none
guard let text = text?.ows_stripped(),
text.count > 0 else {
if model.has(itemForId: textItem.itemId) {
@ -620,7 +498,7 @@ extension ImageEditorView: ImageEditorTextViewControllerDelegate {
}
// Model items are immutable; we _replace_ the item rather than modify it.
let newItem = textItem.copy(withText: text)
let newItem = textItem.copy(withText: text, color: color)
if model.has(itemForId: textItem.itemId) {
model.replace(item: newItem, suppressUndo: false)
} else {
@ -629,7 +507,6 @@ extension ImageEditorView: ImageEditorTextViewControllerDelegate {
}
public func textEditDidCancel() {
self.editorMode = .none
}
}
@ -648,16 +525,7 @@ extension ImageEditorView: ImageEditorCropViewControllerDelegate {
// MARK: -
extension ImageEditorView: ImageEditorPaletteViewDelegate {
public func selectedColorDidChange() {
// TODO:
}
}
// MARK: -
extension ImageEditorView: ImageEditorBrushViewControllerDelegate {
public func brushDidComplete() {
self.editorMode = .none
}
}

View file

@ -129,7 +129,7 @@ public extension CGFloat {
return CGFloatClamp(self, minValue, maxValue)
}
public func clamp01(_ minValue: CGFloat, _ maxValue: CGFloat) -> CGFloat {
public func clamp01() -> CGFloat {
return CGFloatClamp01(self)
}