Handle view is now drawn with inverted teardrop. Fixed color wheel issue obtaining incorrect colors of via subviews. ChromaColorPicker pan gesture implemented for handles.

This commit is contained in:
Jonathan Cardasis 2019-04-19 16:45:47 -04:00
parent ac92bef8c6
commit c701c6d109
7 changed files with 264 additions and 15 deletions

View File

@ -22,6 +22,7 @@
FC4387C922625C7000F739F1 /* SliderTrackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC4387C822625C7000F739F1 /* SliderTrackView.swift */; };
FC4387CB22625DA800F739F1 /* SliderHandleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC4387CA22625DA800F739F1 /* SliderHandleView.swift */; };
FC4387CD2262B82600F739F1 /* ChromaControlStylable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC4387CC2262B82600F739F1 /* ChromaControlStylable.swift */; };
FC4387DE226974FD00F739F1 /* UIColor+Brightness.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC4387DC226972EB00F739F1 /* UIColor+Brightness.swift */; };
FCCA42A5226022A800BE2FF9 /* ColorWheelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCCA42A4226022A800BE2FF9 /* ColorWheelView.swift */; };
FCCA42A7226023F000BE2FF9 /* ChromaColorHandle.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCCA42A6226023F000BE2FF9 /* ChromaColorHandle.swift */; };
FCCA42AA2260329900BE2FF9 /* UIKit+DropShadow.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCCA42A92260329900BE2FF9 /* UIKit+DropShadow.swift */; };
@ -83,6 +84,7 @@
FC4387C822625C7000F739F1 /* SliderTrackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SliderTrackView.swift; sourceTree = "<group>"; };
FC4387CA22625DA800F739F1 /* SliderHandleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SliderHandleView.swift; sourceTree = "<group>"; };
FC4387CC2262B82600F739F1 /* ChromaControlStylable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChromaControlStylable.swift; sourceTree = "<group>"; };
FC4387DC226972EB00F739F1 /* UIColor+Brightness.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Brightness.swift"; sourceTree = "<group>"; };
FCCA42A4226022A800BE2FF9 /* ColorWheelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorWheelView.swift; sourceTree = "<group>"; };
FCCA42A6226023F000BE2FF9 /* ChromaColorHandle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChromaColorHandle.swift; sourceTree = "<group>"; };
FCCA42A92260329900BE2FF9 /* UIKit+DropShadow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIKit+DropShadow.swift"; sourceTree = "<group>"; };
@ -204,6 +206,7 @@
isa = PBXGroup;
children = (
FCCA42A92260329900BE2FF9 /* UIKit+DropShadow.swift */,
FC4387DC226972EB00F739F1 /* UIColor+Brightness.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -366,6 +369,7 @@
FCEA4E272235AAA200C0A1B6 /* ChromaColorPicker.swift in Sources */,
FC4387CD2262B82600F739F1 /* ChromaControlStylable.swift in Sources */,
FCCA42AA2260329900BE2FF9 /* UIKit+DropShadow.swift in Sources */,
FC4387DE226974FD00F739F1 /* UIColor+Brightness.swift in Sources */,
FC4387C62262556600F739F1 /* ChromaBrightnessSlider.swift in Sources */,
FCCA42A7226023F000BE2FF9 /* ChromaColorHandle.swift in Sources */,
FCCA42A5226022A800BE2FF9 /* ColorWheelView.swift in Sources */,

View File

@ -19,13 +19,11 @@ class ViewController: UIViewController {
super.viewDidLoad()
setupColorPicker()
setupBrightnessSlider()
}
override func viewDidAppear(_ animated: Bool) {
print()
setupColorPickerHandles()
}
private func setupColorPicker() {
colorPicker.delegate = self
colorPicker.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(colorPicker)
@ -57,7 +55,20 @@ class ViewController: UIViewController {
brightnessSlider.heightAnchor.constraint(equalTo: brightnessSlider.widthAnchor, multiplier: brightnessSliderWidthHeightRatio)
])
}
private func setupColorPickerHandles() {
colorPicker.addHandle(at: .blue)
colorPicker.borderWidth = 0
}
}
extension ViewController: ChromaColorPickerDelegate {
func colorPickerDidChooseColor(_ colorPicker: ChromaColorPicker, color: UIColor) {
colorDisplayView.backgroundColor = color
}
}
private let defaultColorPickerSize = CGSize(width: 320, height: 320)
private let brightnessSliderWidthHeightRatio: CGFloat = 0.1

View File

@ -69,6 +69,12 @@ public class ChromaBrightnessSlider: UIControl, ChromaControlStylable {
colorPicker.connect(self)
}
/// Returns the relative value on the slider [0.0, 1.0] for the given color brightness ([0.0, 1.0]).
public func value(brightness: CGFloat) -> CGFloat {
let clamedBrightness = max(0, min(brightness, 1.0))
return 1.0 - clamedBrightness
}
// MARK: - Control
public override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
@ -178,7 +184,10 @@ public class ChromaBrightnessSlider: UIControl, ChromaControlStylable {
}
internal func updateTrackViewGradient(for color: UIColor) {
CATransaction.begin()
CATransaction.setDisableActions(true)
sliderTrackView.gradientValues = (color, .black)
CATransaction.commit()
}
internal func moveHandle(to value: CGFloat) {

View File

@ -9,7 +9,7 @@
import UIKit
import Accelerate
public protocol ChromaColorPickerDelegate {
public protocol ChromaColorPickerDelegate: class {
/// When the control has changed
func colorPickerDidChooseColor(_ colorPicker: ChromaColorPicker, color: UIColor)
}
@ -18,6 +18,8 @@ public protocol ChromaColorPickerDelegate {
@IBDesignable
public class ChromaColorPicker: UIControl, ChromaControlStylable {
public weak var delegate: ChromaColorPickerDelegate?
@IBInspectable public var borderWidth: CGFloat = 8.0 {
didSet { setNeedsLayout() }
}
@ -37,6 +39,14 @@ public class ChromaColorPicker: UIControl, ChromaControlStylable {
}
}
//public var handleSize: CGSize { /* TODO */ }
/// Handles added to the color picker.
private(set) public var handles: [ChromaColorHandle] = []
/// The last active handle.
private(set) public var currentHandle: ChromaColorHandle?
//MARK: - Initialization
override public init(frame: CGRect) {
@ -53,12 +63,30 @@ public class ChromaColorPicker: UIControl, ChromaControlStylable {
super.layoutSubviews()
updateShadowIfNeeded()
updateBorderIfNeeded()
handles.forEach { handle in
//let location = colorWheelView.location(of: handle.color)
handle.frame.size = defaultHandleSize
//handle.center = location
}
}
// MARK: - Public
@discardableResult
public func addHandle(at color: UIColor? = nil) -> ChromaColorHandle {
return ChromaColorHandle(color: color ?? defaultHandleColorPosition)
let handleColor = color ?? defaultHandleColorPosition
let handle = ChromaColorHandle()
handle.color = handleColor
addHandle(handle)
return handle
}
public func addHandle(_ handle: ChromaColorHandle) {
addPanGesture(to: handle)
handles.append(handle)
colorWheelView.addSubview(handle)
}
public func connect(_ slider: ChromaBrightnessSlider) {
@ -73,9 +101,51 @@ public class ChromaColorPicker: UIControl, ChromaControlStylable {
self.backgroundColor = UIColor.clear
self.layer.masksToBounds = false
setupColorWheelView()
setupGestures()
//setupGestures()
}
// MARK: - Control
// public override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
// let location = touch.location(in: self)
//
// for handle in handles {
// if handle.frame.contains(location) {
// print("tapped on handle")
// currentHandle = handle
// return true
// }
// }
// return false
// }
//
// public override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
// let location = touch.location(in: colorWheelView)
// guard let handle = currentHandle else { return false }
//
// handle.center = location
// if let selectedColor = colorWheelView.pixelColor(at: location) {
// print(location)
// handle.color = selectedColor
//
// delegate?.colorPickerDidChooseColor(self, color: selectedColor)
// }
//
// sendActions(for: .valueChanged)
// return true
// }
//
// public override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
// sendActions(for: .editingDidEnd)
// }
internal func addPanGesture(to handle: ChromaColorHandle) {
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handleWasMoved(_:)))
panGesture.maximumNumberOfTouches = 1
panGesture.minimumNumberOfTouches = 1
handle.addGestureRecognizer(panGesture)
}
// MARK: Setup & Layout
internal func setupColorWheelView() {
@ -110,17 +180,66 @@ public class ChromaColorPicker: UIControl, ChromaControlStylable {
// MARK: Actions
@objc
internal func handleWasMoved(_ gesture: UIPanGestureRecognizer) {
if let touchedView = gesture.view {
colorWheelView.bringSubviewToFront(touchedView)
}
switch (gesture.state) {
case .began:
currentHandle = gesture.view as? ChromaColorHandle
case .changed:
let location = gesture.location(in: colorWheelView)
if let handle = currentHandle, let pixelColor = colorWheelView.pixelColor(at: location) {
let previousBrightness = handle.color.brightness
print(pixelColor)
handle.color = pixelColor.withBrightness(previousBrightness)
handle.center = location
if let slider = brightnessSlider {
slider.trackColor = pixelColor
slider.currentValue = slider.value(brightness: previousBrightness)
}
informDelegateOfColorChange(on: handle)
}
self.sendActions(for: .touchDragInside)
case .ended:
/* Shrink Animation */
//self.executeHandleShrinkAnimation()
break
default:
break
}
}
@objc
internal func colorWheelTapped(_ gesture: UITapGestureRecognizer) {
let location = gesture.location(in: colorWheelView)
let pixelColor = colorWheelView.pixelColor(at: location)
print(pixelColor)
print(location)
delegate?.colorPickerDidChooseColor(self, color: pixelColor!)
}
@objc
internal func brightnessSliderDidValueChange(_ slider: ChromaBrightnessSlider) {
guard let currentHandle = currentHandle else { return }
currentHandle.color = slider.currentColor
informDelegateOfColorChange(on: currentHandle)
}
internal func informDelegateOfColorChange(on handle: ChromaColorHandle) {
delegate?.colorPickerDidChooseColor(self, color: handle.color)
}
}
internal let defaultHandleColorPosition: UIColor = .black
internal let defaultHandleColorPosition: UIColor = .white
internal let defaultHandleSize: CGSize = CGSize(width: 42, height: 42)

View File

@ -74,12 +74,12 @@ public class ColorWheelView: UIView {
}
context.translateBy(x: -point.x, y: -point.y)
layer.render(in: context)
imageView.layer.render(in: context)
let color = UIColor(
red: CGFloat(pixel[0]) / 255.0,
green: CGFloat(pixel[1]) / 255.0,
blue: CGFloat(pixel[2]) / 255.0,
alpha: CGFloat(pixel[3]) / 255.0
alpha: 1.0
)
pixel.deallocate()

View File

@ -0,0 +1,30 @@
//
// UIColor+Brightness.swift
// Example
//
// Created by Jon Cardasis on 4/18/19.
// Copyright © 2019 Jonathan Cardasis. All rights reserved.
//
import UIKit
internal extension UIColor {
/// Returns a color with the specified brightness component.
internal func withBrightness(_ value: CGFloat) -> UIColor {
var hue: CGFloat = 0
var saturation: CGFloat = 0
var alpha: CGFloat = 0
let brightness = max(0, min(value, 1))
getHue(&hue, saturation: &saturation, brightness: nil, alpha: &alpha)
return UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: alpha)
}
/// The value of the brightness component.
internal var brightness: CGFloat {
var brightness: CGFloat = 0
getHue(nil, saturation: nil, brightness: &brightness, alpha: nil)
return brightness
}
}

View File

@ -8,17 +8,93 @@
import UIKit
public class ChromaColorHandle {
public class ChromaColorHandle: UIView, ChromaControlStylable {
/// Current selected color of the handle.
fileprivate(set) var color: UIColor
public var color: UIColor = .black {
didSet { setNeedsLayout() }
}
/// An image to display above the handle.
var popoverImage: UIImage?
public var popoverImage: UIImage?
/// A view to display above the handle. Overrides any provided `popoverImage`.
var popoverView: UIView?
public var popoverView: UIView?
init(color: UIColor) {
public var borderWidth: CGFloat = 3.0 {
didSet { setNeedsLayout() }
}
public var borderColor: UIColor = .white {
didSet { setNeedsLayout() }
}
public var showsShadow: Bool = true {
didSet { setNeedsLayout() }
}
// MARK: - Initialization
public convenience init(color: UIColor) {
self.init(frame: .zero)
self.color = color
commonInit()
}
public override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
public override func layoutSubviews() {
super.layoutSubviews()
layoutHandleShape()
layer.masksToBounds = false
}
// MARK: - Private
internal let handleShape = CAShapeLayer()
internal func commonInit() {
backgroundColor = UIColor.red.withAlphaComponent(0.25) //DEBUG
layer.addSublayer(handleShape)
}
internal func updateShadowIfNeeded() {
if showsShadow {
let shadowProps = shadowProperties(forHeight: bounds.height)
applyDropShadow(shadowProps)
} else {
removeDropShadow()
}
}
internal func makeHandlePath(frame: CGRect) -> CGPath {
let path = UIBezierPath()
path.move(to: CGPoint(x: frame.minX + 0.5 * frame.width, y: frame.minY + 1 * frame.height))
path.addCurve(to: CGPoint(x: frame.minX + 1 * frame.width, y: frame.minY + 0.40310 * frame.height), controlPoint1: CGPoint(x: frame.minX + 0.83333 * frame.width, y: frame.minY + 0.80216 * frame.height), controlPoint2: CGPoint(x: frame.minX + 1 * frame.width, y: frame.minY + 0.60320 * frame.height))
path.addCurve(to: CGPoint(x: frame.minX + 0.5 * frame.width, y: frame.minY * frame.height), controlPoint1: CGPoint(x: frame.minX + 1 * frame.width, y: frame.minY + 0.18047 * frame.height), controlPoint2: CGPoint(x: frame.minX + 0.77614 * frame.width, y: frame.minY * frame.height))
path.addCurve(to: CGPoint(x: frame.minX * frame.width, y: frame.minY + 0.40310 * frame.height), controlPoint1: CGPoint(x: frame.minX + 0.22386 * frame.width, y: frame.minY * frame.height), controlPoint2: CGPoint(x: frame.minX * frame.width, y: frame.minY + 0.18047 * frame.height))
path.addCurve(to: CGPoint(x: frame.minX + 0.50000 * frame.width, y: frame.minY + 1 * frame.height), controlPoint1: CGPoint(x: frame.minX * frame.width, y: frame.minY + 0.60837 * frame.height), controlPoint2: CGPoint(x: frame.minX + 0.16667 * frame.width, y: frame.minY + 0.80733 * frame.height))
path.close()
return path.cgPath
}
internal func layoutHandleShape() {
let size = CGSize(width: bounds.height * handleWidthHeightRatio, height: bounds.height)
handleShape.path = makeHandlePath(frame: CGRect(origin: .zero, size: size))
handleShape.frame = CGRect(origin: CGPoint(x: bounds.midX - (size.width / 2), y: bounds.midY - size.height), size: size)
handleShape.fillColor = color.cgColor
handleShape.strokeColor = borderColor.cgColor
handleShape.lineWidth = borderWidth
}
}
internal let handleWidthHeightRatio: CGFloat = 52.0 / 65