Add color palette to image editor.

pull/2/head
Matthew Chen 7 years ago
parent 530a07f8ca
commit de27ed8728

@ -11,6 +11,7 @@
3403B95D20EA9527001A1F44 /* OWSContactShareButtonsView.m in Sources */ = {isa = PBXBuildFile; fileRef = 3403B95B20EA9526001A1F44 /* OWSContactShareButtonsView.m */; };
34074F61203D0CBE004596AE /* OWSSounds.m in Sources */ = {isa = PBXBuildFile; fileRef = 34074F5F203D0CBD004596AE /* OWSSounds.m */; };
34074F62203D0CBE004596AE /* OWSSounds.h in Headers */ = {isa = PBXBuildFile; fileRef = 34074F60203D0CBE004596AE /* OWSSounds.h */; settings = {ATTRIBUTES = (Public, ); }; };
34080EFE2225F96D0087E99F /* ImageEditorPaletteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34080EFD2225F96D0087E99F /* ImageEditorPaletteView.swift */; };
340B02BA1FA0D6C700F9CFEC /* ConversationViewItemTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 340B02B91FA0D6C700F9CFEC /* ConversationViewItemTest.m */; };
340FC8A9204DAC8D007AEB0F /* NotificationSettingsOptionsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC87B204DAC8C007AEB0F /* NotificationSettingsOptionsViewController.m */; };
340FC8AA204DAC8D007AEB0F /* NotificationSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC87C204DAC8C007AEB0F /* NotificationSettingsViewController.m */; };
@ -635,6 +636,7 @@
3403B95C20EA9527001A1F44 /* OWSContactShareButtonsView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSContactShareButtonsView.h; sourceTree = "<group>"; };
34074F5F203D0CBD004596AE /* OWSSounds.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSSounds.m; sourceTree = "<group>"; };
34074F60203D0CBE004596AE /* OWSSounds.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSSounds.h; sourceTree = "<group>"; };
34080EFD2225F96D0087E99F /* ImageEditorPaletteView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageEditorPaletteView.swift; sourceTree = "<group>"; };
340B02B61F9FD31800F9CFEC /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = translations/he.lproj/Localizable.strings; sourceTree = "<group>"; };
340B02B91FA0D6C700F9CFEC /* ConversationViewItemTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConversationViewItemTest.m; sourceTree = "<group>"; };
340FC87B204DAC8C007AEB0F /* NotificationSettingsOptionsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NotificationSettingsOptionsViewController.m; sourceTree = "<group>"; };
@ -1913,6 +1915,7 @@
34BBC84E220B8A0100857249 /* ImageEditorCropViewController.swift */,
34BBC852220C7AD900857249 /* ImageEditorItem.swift */,
34BEDB0D21C405B0007B0EAE /* ImageEditorModel.swift */,
34080EFD2225F96D0087E99F /* ImageEditorPaletteView.swift */,
34BBC85C220D19D600857249 /* ImageEditorPanGestureRecognizer.swift */,
34BBC84C220B2D0800857249 /* ImageEditorPinchGestureRecognizer.swift */,
34BBC854220C7ADA00857249 /* ImageEditorStrokeItem.swift */,
@ -3333,6 +3336,7 @@
34AC09DF211B39B100997B47 /* OWSNavigationController.m in Sources */,
34074F61203D0CBE004596AE /* OWSSounds.m in Sources */,
34BEDB1721C80BCA007B0EAE /* OWSAnyTouchGestureRecognizer.m in Sources */,
34080EFE2225F96D0087E99F /* ImageEditorPaletteView.swift in Sources */,
34B6A909218B8824007C4606 /* OWS112TypingIndicatorsMigration.swift in Sources */,
4C3E245D21F2B395000AE092 /* DirectionalPanGestureRecognizer.swift in Sources */,
346129B51FD1F7E800532771 /* OWSProfileManager.m in Sources */,

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "Screen Shot 2019-02-26 at 1.57.23 PM.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@ -0,0 +1,258 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
import UIKit
public protocol ImageEditorPaletteViewDelegate: class {
func selectedColorDidChange()
}
// MARK: -
public class ImageEditorPaletteView: UIView {
public weak var delegate: ImageEditorPaletteViewDelegate?
public required init() {
super.init(frame: .zero)
createContents()
}
@available(*, unavailable, message: "use other init() instead.")
required public init?(coder aDecoder: NSCoder) {
notImplemented()
}
// 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()
private var selectionConstraint: NSLayoutConstraint?
private func createContents() {
self.backgroundColor = .clear
self.isOpaque = false
if let image = UIImage(named: "image_editor_palette") {
imageView.image = image
} else {
owsFailDebug("Missing image.")
}
addSubview(imageView)
// We use an invisible margin to expand the hot area of
// this control.
let margin: CGFloat = 8
// TODO: Review sizing when there's an asset.
imageView.autoSetDimensions(to: CGSize(width: 8, height: 200))
imageView.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets(top: margin, left: margin, bottom: margin, right: margin))
selectionWrapper.layoutCallback = { [weak self] (view) in
guard let strongSelf = self else {
return
}
strongSelf.updateState(fireEvent: false)
}
imageView.addSubview(selectionWrapper)
selectionWrapper.autoPinEdgesToSuperviewEdges()
selectionView.addBorder(with: .white)
selectionView.layer.cornerRadius = selectionSize / 2
selectionView.autoSetDimensions(to: CGSize(width: selectionSize, height: selectionSize))
selectionWrapper.addSubview(selectionView)
selectionView.autoHCenterInSuperview()
isUserInteractionEnabled = true
addGestureRecognizer(PaletteGestureRecognizer(target: self, action: #selector(didTouch)))
updateState(fireEvent: false)
}
// 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)
updateState(fireEvent: true)
}
private func updateState(fireEvent: Bool) {
var selectedColor = UIColor.white
if let image = imageView.image,
let cgImage = image.cgImage {
if let imageColor = image.color(atLocation: CGPoint(x: CGFloat(cgImage.width) * 0.5, y: CGFloat(cgImage.height) * selectionAlpha)) {
selectedColor = imageColor
} else {
owsFailDebug("Couldn't determine image color.")
}
} else {
owsFailDebug("Missing image.")
}
self.selectedColor = selectedColor
selectionView.backgroundColor = selectedColor
// There must be a better way to pin the selection view's location,
// but I can't find it.
self.selectionConstraint?.autoRemove()
let selectionY = selectionWrapper.height() * selectionAlpha
let selectionConstraint = NSLayoutConstraint(item: selectionView,
attribute: .centerY, relatedBy: .equal, toItem: selectionWrapper, attribute: .top, multiplier: 1, constant: selectionY)
selectionConstraint.autoInstall()
self.selectionConstraint = selectionConstraint
if fireEvent {
self.delegate?.selectedColorDidChange()
}
}
// MARK: Events
@objc
func didTouch(gesture: UIGestureRecognizer) {
Logger.verbose("gesture: \(NSStringForUIGestureRecognizerState(gesture.state))")
switch gesture.state {
case .began, .changed, .ended:
break
default:
return
}
let location = gesture.location(in: imageView)
selectColor(atLocationY: location.y)
}
}
// 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 {
@objc
public override func canPrevent(_ preventedGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
@objc
public override func canBePrevented(by preventingGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
@objc
public override func shouldRequireFailure(of otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
@objc
public override func shouldBeRequiredToFail(by otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
@objc
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
handle(event: event)
}
@objc
public override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
handle(event: event)
}
@objc
public override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
handle(event: event)
}
@objc
public override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
handle(event: event)
}
private func handle(event: UIEvent) {
var hasValidTouch = false
if let allTouches = event.allTouches {
for touch in allTouches {
switch touch.phase {
case .began, .moved, .stationary:
hasValidTouch = true
default:
break
}
}
}
if hasValidTouch {
switch self.state {
case .possible:
self.state = .began
case .began, .changed:
self.state = .changed
default:
self.state = .failed
}
} else {
switch self.state {
case .began, .changed:
self.state = .ended
default:
self.state = .failed
}
}
}
}

@ -23,6 +23,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
@ -37,8 +39,11 @@ public class ImageEditorView: UIView {
}
}
private static let defaultColor = UIColor.white
private var currentColor = ImageEditorView.defaultColor
private var currentColor: UIColor {
get {
return paletteView.selectedColor
}
}
@objc
public required init(model: ImageEditorModel, delegate: ImageEditorViewDelegate) {
@ -71,6 +76,8 @@ public class ImageEditorView: UIView {
self.addSubview(canvasView)
canvasView.autoPinEdgesToSuperviewEdges()
paletteView.delegate = self
self.isUserInteractionEnabled = true
let moveTextGestureRecognizer = ImageEditorPanGestureRecognizer(target: self, action: #selector(handleMoveTextGesture(_:)))
@ -129,6 +136,7 @@ public class ImageEditorView: UIView {
private let newTextButton = UIButton(type: .custom)
private var allButtons = [UIButton]()
// TODO: Should this method be private?
@objc
public func addControls(to containerView: UIView) {
configure(button: undoButton,
@ -151,11 +159,7 @@ public class ImageEditorView: UIView {
label: "Text",
selector: #selector(didTapNewText(sender:)))
let redButton = colorButton(color: UIColor.red)
let whiteButton = colorButton(color: UIColor.white)
let blackButton = colorButton(color: UIColor.black)
allButtons = [brushButton, cropButton, undoButton, redoButton, newTextButton, redButton, whiteButton, blackButton]
allButtons = [brushButton, cropButton, undoButton, redoButton, newTextButton]
let stackView = UIStackView(arrangedSubviews: allButtons)
stackView.axis = .vertical
@ -166,6 +170,10 @@ public class ImageEditorView: UIView {
stackView.autoAlignAxis(toSuperviewAxis: .horizontal)
stackView.autoPinTrailingToSuperviewMargin(withInset: 10)
containerView.addSubview(paletteView)
paletteView.autoVCenterInSuperview()
paletteView.autoPinLeadingToSuperviewMargin(withInset: 10)
updateButtons()
}
@ -180,17 +188,6 @@ public class ImageEditorView: UIView {
button.addTarget(self, action: selector, for: .touchUpInside)
}
private func colorButton(color: UIColor) -> UIButton {
let button = OWSButton { [weak self] in
self?.didSelectColor(color)
}
let size: CGFloat = 20
let swatch = UIImage(color: color, size: CGSize(width: size, height: size))
button.setImage(swatch, for: .normal)
button.addBorder(with: UIColor.white)
return button
}
private func updateButtons() {
undoButton.isEnabled = model.canUndo()
redoButton.isEnabled = model.canRedo()
@ -262,12 +259,6 @@ public class ImageEditorView: UIView {
updateButtons()
}
@objc func didSelectColor(_ color: UIColor) {
Logger.verbose("")
currentColor = color
}
// MARK: - Gestures
private func updateGestureState() {
@ -679,3 +670,11 @@ extension ImageEditorView: ImageEditorCropViewControllerDelegate {
// TODO:
}
}
// MARK: -
extension ImageEditorView: ImageEditorPaletteViewDelegate {
public func selectedColorDidChange() {
// TODO:
}
}

@ -125,6 +125,14 @@ extension UIView {
}
public extension CGFloat {
public func clamp(_ minValue: CGFloat, _ maxValue: CGFloat) -> CGFloat {
return CGFloatClamp(self, minValue, maxValue)
}
public func clamp01(_ minValue: CGFloat, _ maxValue: CGFloat) -> CGFloat {
return CGFloatClamp01(self)
}
// Linear interpolation
public func lerp(_ minValue: CGFloat, _ maxValue: CGFloat) -> CGFloat {
return CGFloatLerp(minValue, maxValue, self)
@ -139,6 +147,14 @@ public extension CGFloat {
public static let halfPi: CGFloat = CGFloat.pi * 0.5
}
public extension Int {
public func clamp(_ minValue: Int, _ maxValue: Int) -> Int {
assert(minValue <= maxValue)
return Swift.max(minValue, Swift.min(maxValue, self))
}
}
public extension CGPoint {
public func toUnitCoordinates(viewBounds: CGRect, shouldClamp: Bool) -> CGPoint {
return CGPoint(x: (x - viewBounds.origin.x).inverseLerp(0, viewBounds.width, shouldClamp: shouldClamp),

Loading…
Cancel
Save