Merge branch 'charlesmchen/imageEditorDesign3'

pull/2/head
Matthew Chen 6 years ago
commit b2968d2bed

@ -6,7 +6,7 @@ import UIKit
@objc
public protocol ImageEditorBrushViewControllerDelegate: class {
func brushDidComplete()
func brushDidComplete(currentColor: ImageEditorColor)
}
// MARK: -
@ -21,7 +21,10 @@ public class ImageEditorBrushViewController: OWSViewController {
private let paletteView: ImageEditorPaletteView
private var brushGestureRecognizer: ImageEditorPanGestureRecognizer?
// We only want to let users undo changes made in this view.
// So we snapshot any older "operation id" and prevent
// users from undoing it.
private let firstUndoOperationId: String?
init(delegate: ImageEditorBrushViewControllerDelegate,
model: ImageEditorModel,
@ -30,6 +33,7 @@ public class ImageEditorBrushViewController: OWSViewController {
self.model = model
self.canvasView = ImageEditorCanvasView(model: model)
self.paletteView = ImageEditorPaletteView(currentColor: currentColor)
self.firstUndoOperationId = model.currentUndoOperationId()
super.init(nibName: nil, bundle: nil)
@ -55,15 +59,15 @@ public class ImageEditorBrushViewController: OWSViewController {
paletteView.delegate = self
self.view.addSubview(paletteView)
paletteView.autoVCenterInSuperview()
paletteView.autoPinEdge(toSuperviewEdge: .trailing, withInset: 20)
paletteView.autoPinEdge(toSuperviewEdge: .trailing, withInset: 0)
self.view.isUserInteractionEnabled = true
let brushGestureRecognizer = ImageEditorPanGestureRecognizer(target: self, action: #selector(handleBrushGesture(_:)))
brushGestureRecognizer.maximumNumberOfTouches = 1
brushGestureRecognizer.referenceView = canvasView.gestureReferenceView
brushGestureRecognizer.delegate = self
self.view.addGestureRecognizer(brushGestureRecognizer)
self.brushGestureRecognizer = brushGestureRecognizer
updateNavigationBar()
}
@ -86,8 +90,10 @@ public class ImageEditorBrushViewController: OWSViewController {
let doneButton = navigationBarButton(imageName: "image_editor_checkmark_full",
selector: #selector(didTapDone(sender:)))
// Prevent users from undo any changes made before entering the view.
let canUndo = model.canUndo() && firstUndoOperationId != model.currentUndoOperationId()
var navigationBarItems = [UIView]()
if model.canUndo() {
if canUndo {
navigationBarItems = [undoButton, doneButton]
} else {
navigationBarItems = [doneButton]
@ -113,7 +119,7 @@ public class ImageEditorBrushViewController: OWSViewController {
}
private func completeAndDismiss() {
self.delegate?.brushDidComplete()
self.delegate?.brushDidComplete(currentColor: paletteView.selectedValue)
self.dismiss(animated: false) {
// Do nothing.
@ -163,7 +169,7 @@ public class ImageEditorBrushViewController: OWSViewController {
// Apply the location history of the gesture so that the stroke reflects
// the touch's movement before the gesture recognized.
for location in gestureRecognizer.locations {
for location in gestureRecognizer.locationHistory {
tryToAppendStrokeSample(location)
}
@ -222,3 +228,13 @@ extension ImageEditorBrushViewController: ImageEditorPaletteViewDelegate {
// TODO:
}
}
// MARK: -
extension ImageEditorBrushViewController: UIGestureRecognizerDelegate {
@objc public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
// Ignore touches that begin inside the palette.
let location = touch.location(in: paletteView)
return !paletteView.bounds.contains(location)
}
}

@ -501,7 +501,7 @@ class ImageEditorCropViewController: OWSViewController {
Logger.verbose("")
guard let locationStart = gestureRecognizer.locationStart else {
guard let locationStart = gestureRecognizer.locationFirst else {
owsFailDebug("Missing locationStart.")
return
}
@ -666,7 +666,7 @@ class ImageEditorCropViewController: OWSViewController {
owsFailDebug("Missing pinchTransform.")
return
}
guard let oldLocationView = gestureRecognizer.locationStart else {
guard let oldLocationView = gestureRecognizer.locationFirst else {
owsFailDebug("Missing locationStart.")
return
}
@ -685,7 +685,7 @@ class ImageEditorCropViewController: OWSViewController {
}
private func cropRegion(forGestureRecognizer gestureRecognizer: ImageEditorPanGestureRecognizer) -> CropRegion? {
guard let location = gestureRecognizer.locationStart else {
guard let location = gestureRecognizer.locationFirst else {
owsFailDebug("Missing locationStart.")
return nil
}

@ -242,9 +242,12 @@ public class ImageEditorTransform: NSObject {
// (multiple times) to preserve/restore editor state.
private class ImageEditorOperation: NSObject {
let operationId: String
let contents: ImageEditorContents
required init(contents: ImageEditorContents) {
self.operationId = UUID().uuidString
self.contents = contents
}
}
@ -361,6 +364,14 @@ public class ImageEditorModel: NSObject {
return !redoStack.isEmpty
}
@objc
public func currentUndoOperationId() -> String? {
guard let operation = undoStack.last else {
return nil
}
return operation.operationId
}
// MARK: - Observers
private var observers = [Weak<ImageEditorModelObserver>]()

@ -97,9 +97,8 @@ public class ImageEditorPaletteView: UIView {
owsFailDebug("Missing image.")
}
addSubview(imageView)
// We use an invisible margin to expand the hot area of
// this control.
let margin: CGFloat = 8
// We use an invisible margin to expand the hot area of this control.
let margin: CGFloat = 20
imageView.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets(top: margin, left: margin, bottom: margin, right: margin))
selectionWrapper.layoutCallback = { [weak self] (view) in
@ -144,14 +143,49 @@ public class ImageEditorPaletteView: UIView {
}
private func value(for palettePhase: CGFloat) -> ImageEditorColor {
guard let image = imageView.image else {
owsFailDebug("Missing image.")
// We find the color in the palette's gradient that corresponds
// to the "phase".
//
// 0 = top of gradient, first color.
// 1 = bottom of gradient, last color.
struct GradientSegment {
let color0: UIColor
let color1: UIColor
let palettePhase0: CGFloat
let palettePhase1: CGFloat
}
var segments = [GradientSegment]()
let segmentCount = ImageEditorColor.gradientUIColors.count - 1
var prevColor: UIColor?
for color in ImageEditorColor.gradientUIColors {
if let color0 = prevColor {
let index = CGFloat(segments.count)
let color1 = color
let palettePhase0: CGFloat = index / CGFloat(segmentCount)
let palettePhase1: CGFloat = (index + 1) / CGFloat(segmentCount)
segments.append(GradientSegment(color0: color0, color1: color1, palettePhase0: palettePhase0, palettePhase1: palettePhase1))
}
prevColor = color
}
var bestSegment = segments.first
for segment in segments {
if palettePhase >= segment.palettePhase0 {
bestSegment = segment
}
}
guard let segment = bestSegment else {
owsFailDebug("Couldn't find matching segment.")
return ImageEditorColor.defaultColor()
}
guard let color = image.color(atLocation: CGPoint(x: CGFloat(image.size.width) * 0.5, y: CGFloat(image.size.height) * palettePhase)) else {
owsFailDebug("Missing color.")
guard palettePhase >= segment.palettePhase0,
palettePhase <= segment.palettePhase1 else {
owsFailDebug("Invalid segment.")
return ImageEditorColor.defaultColor()
}
let segmentPhase = palettePhase.inverseLerp(segment.palettePhase0, segment.palettePhase1).clamp01()
// If CAGradientLayer doesn't do naive RGB color interpolation,
// this won't be WYSIWYG.
let color = segment.color0.blend(with: segment.color1, alpha: segmentPhase)
return ImageEditorColor(color: color, palettePhase: palettePhase)
}
@ -170,7 +204,6 @@ public class ImageEditorPaletteView: UIView {
@objc
func didTouch(gesture: UIGestureRecognizer) {
Logger.verbose("gesture: \(NSStringForUIGestureRecognizerState(gesture.state))")
switch gesture.state {
case .began, .changed, .ended:
break
@ -201,55 +234,6 @@ public class ImageEditorPaletteView: UIView {
// MARK: -
extension UIImage {
func color(atLocation locationPoints: CGPoint) -> UIColor? {
guard let cgImage = cgImage else {
owsFailDebug("Missing cgImage.")
return nil
}
guard let dataProvider = cgImage.dataProvider else {
owsFailDebug("Could not create dataProvider.")
return nil
}
guard let pixelData = dataProvider.data else {
owsFailDebug("dataProvider has no data.")
return nil
}
let bytesPerPixel: Int = cgImage.bitsPerPixel / 8
guard bytesPerPixel == 4 else {
owsFailDebug("Invalid bytesPerPixel: \(bytesPerPixel).")
return nil
}
let imageWidth: Int = cgImage.width
let imageHeight: Int = cgImage.height
guard imageWidth > 0,
imageHeight > 0 else {
owsFailDebug("Invalid image size.")
return nil
}
// Convert the location from points to pixels and clamp to the image bounds.
let xPixels: Int = Int(round(locationPoints.x * self.scale)).clamp(0, imageWidth - 1)
let yPixels: Int = Int(round(locationPoints.y * self.scale)).clamp(0, imageHeight - 1)
let dataLength = (pixelData as Data).count
let data: UnsafePointer<UInt8> = CFDataGetBytePtr(pixelData)
let index: Int = (imageWidth * yPixels + xPixels) * bytesPerPixel
guard index >= 0, index < dataLength else {
owsFailDebug("Invalid index.")
return nil
}
let red = CGFloat(data[index]) / CGFloat(255.0)
let green = CGFloat(data[index+1]) / CGFloat(255.0)
let blue = CGFloat(data[index+2]) / CGFloat(255.0)
let alpha = CGFloat(data[index+3]) / CGFloat(255.0)
return UIColor(red: red, green: green, blue: blue, alpha: alpha)
}
}
// MARK: -
// The most permissive GR possible. Accepts any number of touches in any locations.
private class PaletteGestureRecognizer: UIGestureRecognizer {

@ -14,39 +14,57 @@ public class ImageEditorPanGestureRecognizer: UIPanGestureRecognizer {
public weak var referenceView: UIView?
// Capture the location history of this gesture.
public var locations = [CGPoint]()
public var locationHistory = [CGPoint]()
public var locationStart: CGPoint? {
return locations.first
public var locationFirst: CGPoint? {
return locationHistory.first
}
// MARK: - Touch Handling
@objc
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
updateLocationHistory(event: event)
guard let referenceView = referenceView else {
owsFailDebug("Missing view")
return
}
locations.append(location(in: referenceView))
super.touchesBegan(touches, with: event)
}
@objc
public override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
updateLocationHistory(event: event)
super.touchesMoved(touches, with: event)
}
@objc
public override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
updateLocationHistory(event: event)
super.touchesEnded(touches, with: event)
}
private func updateLocationHistory(event: UIEvent) {
guard let touches = event.allTouches,
touches.count > 0 else {
owsFailDebug("no touches.")
return
}
guard let referenceView = referenceView else {
owsFailDebug("Missing view")
return
}
locations.append(location(in: referenceView))
// Find the centroid.
var location = CGPoint.zero
for touch in touches {
location = location.plus(touch.location(in: referenceView))
}
location = location.times(CGFloat(1) / CGFloat(touches.count))
locationHistory.append(location)
}
public override func reset() {
super.reset()
locations.removeAll()
locationHistory.removeAll()
}
}

@ -187,9 +187,9 @@ public class ImageEditorTextViewController: OWSViewController, VAlignTextViewDel
paletteView.delegate = self
self.view.addSubview(paletteView)
paletteView.autoAlignAxis(.horizontal, toSameAxisOf: textView)
paletteView.autoPinEdge(toSuperviewEdge: .trailing, withInset: 20)
paletteView.autoPinEdge(toSuperviewEdge: .trailing, withInset: 0)
// This will determine the text view's size.
paletteView.autoPinEdge(.leading, to: .trailing, of: textView, withOffset: 8)
paletteView.autoPinEdge(.leading, to: .trailing, of: textView, withOffset: 0)
updateNavigationBar()
}

@ -24,7 +24,8 @@ public class ImageEditorView: UIView {
private let canvasView: ImageEditorCanvasView
// TODO: We could hang this on the model or make this static.
// TODO: We could hang this on the model or make this static
// if we wanted more color continuity.
private var currentColor = ImageEditorColor.defaultColor()
@objc
@ -273,7 +274,7 @@ public class ImageEditorView: UIView {
switch gestureRecognizer.state {
case .began:
guard let locationStart = gestureRecognizer.locationStart else {
guard let locationStart = gestureRecognizer.locationFirst else {
owsFailDebug("Missing locationStart.")
return
}
@ -293,7 +294,7 @@ public class ImageEditorView: UIView {
guard let textItem = movingTextItem else {
return
}
guard let locationStart = gestureRecognizer.locationStart else {
guard let locationStart = gestureRecognizer.locationFirst else {
owsFailDebug("Missing locationStart.")
return
}
@ -444,7 +445,8 @@ public class ImageEditorView: UIView {
let cropTool = ImageEditorCropViewController(delegate: self, model: model, srcImage: srcImage, previewImage: previewImage)
self.delegate?.imageEditor(presentFullScreenView: cropTool,
isTransparent: false)
}}
}
}
// MARK: -
@ -498,6 +500,8 @@ extension ImageEditorView: ImageEditorTextViewControllerDelegate {
} else {
model.append(item: newItem)
}
self.currentColor = color
}
public func textEditDidCancel() {
@ -520,6 +524,7 @@ extension ImageEditorView: ImageEditorCropViewControllerDelegate {
// MARK: -
extension ImageEditorView: ImageEditorBrushViewControllerDelegate {
public func brushDidComplete() {
public func brushDidComplete(currentColor: ImageEditorColor) {
self.currentColor = currentColor
}
}

@ -29,11 +29,24 @@ public extension UIViewController {
return
}
let spacing: CGFloat = 8
let stackView = UIStackView(arrangedSubviews: navigationBarItems)
stackView.axis = .horizontal
stackView.spacing = 8
stackView.spacing = spacing
stackView.alignment = .center
// Ensure layout works on older versions of iOS.
var stackSize = CGSize.zero
for item in navigationBarItems {
let itemSize = item.sizeThatFits(.zero)
stackSize.width += itemSize.width + spacing
stackSize.height = max(stackSize.height, itemSize.height)
}
if navigationBarItems.count > 0 {
stackSize.width -= spacing
}
stackView.frame = CGRect(origin: .zero, size: stackSize)
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: stackView)
}
}

Loading…
Cancel
Save