2016-08-11 21:40:04 +03:00
|
|
|
//
|
2019-11-09 00:17:06 +03:00
|
|
|
// ChromaColorPicker.swift
|
2019-04-12 04:38:30 +03:00
|
|
|
// ChromaColorPicker
|
2016-08-11 21:40:04 +03:00
|
|
|
//
|
2019-04-12 04:38:30 +03:00
|
|
|
// Created by Jon Cardasis on 3/10/19.
|
|
|
|
// Copyright © 2019 Jonathan Cardasis. All rights reserved.
|
2016-08-12 19:43:41 +03:00
|
|
|
//
|
2016-08-11 21:40:04 +03:00
|
|
|
|
|
|
|
import UIKit
|
|
|
|
|
2019-04-19 23:45:47 +03:00
|
|
|
public protocol ChromaColorPickerDelegate: class {
|
2019-05-02 17:29:52 +03:00
|
|
|
/// When a handle's value has changed.
|
|
|
|
func colorPickerHandleDidChange(_ colorPicker: ChromaColorPicker, handle: ChromaColorHandle, to color: UIColor)
|
2016-08-11 21:40:04 +03:00
|
|
|
}
|
|
|
|
|
2019-04-12 04:38:30 +03:00
|
|
|
|
|
|
|
@IBDesignable
|
2019-04-14 20:11:00 +03:00
|
|
|
public class ChromaColorPicker: UIControl, ChromaControlStylable {
|
2019-04-12 04:38:30 +03:00
|
|
|
|
2019-04-19 23:45:47 +03:00
|
|
|
public weak var delegate: ChromaColorPickerDelegate?
|
|
|
|
|
2019-04-20 22:36:22 +03:00
|
|
|
@IBInspectable public var borderWidth: CGFloat = 6.0 {
|
2019-09-09 02:16:58 +03:00
|
|
|
didSet { layoutNow() }
|
2019-04-12 06:08:00 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
@IBInspectable public var borderColor: UIColor = .white {
|
2019-09-09 02:16:58 +03:00
|
|
|
didSet { layoutNow() }
|
2019-04-12 06:08:00 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
@IBInspectable public var showsShadow: Bool = true {
|
2019-09-09 02:16:58 +03:00
|
|
|
didSet { layoutNow() }
|
2019-04-12 06:08:00 +03:00
|
|
|
}
|
|
|
|
|
2019-04-17 05:57:38 +03:00
|
|
|
/// A brightness slider attached via the `connect(_:)` method.
|
|
|
|
private(set) public weak var brightnessSlider: ChromaBrightnessSlider? {
|
|
|
|
didSet {
|
|
|
|
oldValue?.removeTarget(self, action: nil, for: .valueChanged)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-20 01:26:24 +03:00
|
|
|
/// The size handles should be displayed at.
|
|
|
|
public var handleSize: CGSize = defaultHandleSize {
|
2019-11-09 00:17:06 +03:00
|
|
|
didSet { setNeedsLayout() }
|
2019-04-20 01:26:24 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/// An extension to handles' hitboxes in the +Y direction.
|
|
|
|
/// Allows for handles to be grabbed more easily.
|
|
|
|
public var handleHitboxExtensionY: CGFloat = 10.0
|
2019-04-19 23:45:47 +03:00
|
|
|
|
|
|
|
/// Handles added to the color picker.
|
|
|
|
private(set) public var handles: [ChromaColorHandle] = []
|
|
|
|
|
|
|
|
/// The last active handle.
|
|
|
|
private(set) public var currentHandle: ChromaColorHandle?
|
|
|
|
|
2016-08-11 21:40:04 +03:00
|
|
|
//MARK: - Initialization
|
2019-04-12 06:08:00 +03:00
|
|
|
|
2016-08-24 23:05:39 +03:00
|
|
|
override public init(frame: CGRect) {
|
2016-08-11 21:40:04 +03:00
|
|
|
super.init(frame: frame)
|
|
|
|
self.commonInit()
|
|
|
|
}
|
|
|
|
|
2016-08-23 22:46:25 +03:00
|
|
|
required public init?(coder aDecoder: NSCoder) {
|
2016-08-11 21:40:04 +03:00
|
|
|
super.init(coder: aDecoder)
|
|
|
|
self.commonInit()
|
|
|
|
}
|
|
|
|
|
2019-04-12 04:38:30 +03:00
|
|
|
public override func layoutSubviews() {
|
|
|
|
super.layoutSubviews()
|
2019-04-12 06:08:00 +03:00
|
|
|
updateShadowIfNeeded()
|
|
|
|
updateBorderIfNeeded()
|
2019-04-19 23:45:47 +03:00
|
|
|
|
|
|
|
handles.forEach { handle in
|
2019-04-20 01:26:24 +03:00
|
|
|
let location = colorWheelView.location(of: handle.color)
|
2019-04-20 22:36:22 +03:00
|
|
|
handle.frame.size = handleSize
|
2019-04-20 01:26:24 +03:00
|
|
|
positionHandle(handle, forColorLocation: location)
|
2019-04-19 23:45:47 +03:00
|
|
|
}
|
2016-08-11 21:40:04 +03:00
|
|
|
}
|
|
|
|
|
2019-04-17 05:57:38 +03:00
|
|
|
// MARK: - Public
|
|
|
|
|
2019-04-19 23:45:47 +03:00
|
|
|
@discardableResult
|
2019-04-12 04:38:30 +03:00
|
|
|
public func addHandle(at color: UIColor? = nil) -> ChromaColorHandle {
|
2019-04-19 23:45:47 +03:00
|
|
|
let handle = ChromaColorHandle()
|
2019-04-20 01:26:24 +03:00
|
|
|
handle.color = color ?? defaultHandleColorPosition
|
2019-04-19 23:45:47 +03:00
|
|
|
addHandle(handle)
|
|
|
|
return handle
|
|
|
|
}
|
|
|
|
|
|
|
|
public func addHandle(_ handle: ChromaColorHandle) {
|
|
|
|
handles.append(handle)
|
|
|
|
colorWheelView.addSubview(handle)
|
2019-05-02 17:29:52 +03:00
|
|
|
brightnessSlider?.trackColor = handle.color
|
2020-04-13 17:50:17 +03:00
|
|
|
|
|
|
|
if currentHandle == nil {
|
|
|
|
currentHandle = handle
|
|
|
|
}
|
2016-08-11 21:40:04 +03:00
|
|
|
}
|
|
|
|
|
2019-04-17 05:57:38 +03:00
|
|
|
public func connect(_ slider: ChromaBrightnessSlider) {
|
|
|
|
slider.addTarget(self, action: #selector(brightnessSliderDidValueChange(_:)), for: .valueChanged)
|
|
|
|
brightnessSlider = slider
|
|
|
|
}
|
|
|
|
|
2019-04-19 23:45:47 +03:00
|
|
|
// MARK: - Control
|
|
|
|
|
2019-04-20 01:26:24 +03:00
|
|
|
public override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
|
2019-04-20 22:36:22 +03:00
|
|
|
let location = touch.location(in: colorWheelView)
|
2019-04-20 04:42:34 +03:00
|
|
|
|
2019-04-20 01:26:24 +03:00
|
|
|
for handle in handles {
|
|
|
|
if extendedHitFrame(for: handle).contains(location) {
|
|
|
|
colorWheelView.bringSubviewToFront(handle)
|
|
|
|
animateHandleScale(handle, shouldGrow: true)
|
2019-04-21 00:52:46 +03:00
|
|
|
|
|
|
|
if let slider = brightnessSlider {
|
|
|
|
slider.trackColor = handle.color.withBrightness(1)
|
|
|
|
slider.currentValue = slider.value(brightness: handle.color.brightness)
|
|
|
|
}
|
|
|
|
|
|
|
|
currentHandle = handle
|
2019-04-20 01:26:24 +03:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
2019-04-20 04:42:34 +03:00
|
|
|
|
2019-04-20 01:26:24 +03:00
|
|
|
public override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
|
|
|
|
var location = touch.location(in: colorWheelView)
|
|
|
|
guard let handle = currentHandle else { return false }
|
|
|
|
|
|
|
|
if !colorWheelView.pointIsInColorWheel(location) {
|
|
|
|
// Touch is outside color wheel and should map to outermost edge.
|
2019-04-20 22:36:22 +03:00
|
|
|
let center = colorWheelView.middlePoint
|
2019-04-20 01:26:24 +03:00
|
|
|
let radius = colorWheelView.radius
|
|
|
|
let angleToCenter = atan2(location.x - center.x, location.y - center.y)
|
|
|
|
let positionOnColorWheelEdge = CGPoint(x: center.x + radius * sin(angleToCenter),
|
|
|
|
y: center.y + radius * cos(angleToCenter))
|
|
|
|
location = positionOnColorWheelEdge
|
|
|
|
}
|
|
|
|
|
|
|
|
if let pixelColor = colorWheelView.pixelColor(at: location) {
|
|
|
|
let previousBrightness = handle.color.brightness
|
|
|
|
handle.color = pixelColor.withBrightness(previousBrightness)
|
|
|
|
positionHandle(handle, forColorLocation: location)
|
|
|
|
|
|
|
|
if let slider = brightnessSlider {
|
|
|
|
slider.trackColor = pixelColor
|
|
|
|
slider.currentValue = slider.value(brightness: previousBrightness)
|
|
|
|
}
|
|
|
|
|
|
|
|
informDelegateOfColorChange(on: handle)
|
|
|
|
sendActions(for: .valueChanged)
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
2019-04-20 04:42:34 +03:00
|
|
|
|
2019-04-20 01:26:24 +03:00
|
|
|
public override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
|
|
|
|
if let handle = currentHandle {
|
|
|
|
animateHandleScale(handle, shouldGrow: false)
|
|
|
|
}
|
2019-09-09 02:16:58 +03:00
|
|
|
sendActions(for: .touchUpInside)
|
2019-04-20 01:26:24 +03:00
|
|
|
}
|
2019-04-19 23:45:47 +03:00
|
|
|
|
2019-04-20 01:26:24 +03:00
|
|
|
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
|
|
// Self should handle all touch events, forwarding if needed.
|
2019-04-20 04:42:34 +03:00
|
|
|
let touchableBounds = bounds.insetBy(dx: -handleSize.width, dy: -handleSize.height)
|
|
|
|
return touchableBounds.contains(point) ? self : super.hitTest(point, with: event)
|
2019-04-19 23:45:47 +03:00
|
|
|
}
|
|
|
|
|
2019-04-20 04:42:34 +03:00
|
|
|
// MARK: - Private
|
|
|
|
|
|
|
|
internal let colorWheelView = ColorWheelView()
|
2019-04-20 22:36:22 +03:00
|
|
|
internal var colorWheelViewWidthConstraint: NSLayoutConstraint!
|
2019-04-20 04:42:34 +03:00
|
|
|
|
|
|
|
internal func commonInit() {
|
|
|
|
self.backgroundColor = UIColor.clear
|
|
|
|
setupColorWheelView()
|
|
|
|
}
|
2019-04-12 06:08:00 +03:00
|
|
|
|
2019-04-12 05:14:07 +03:00
|
|
|
internal func setupColorWheelView() {
|
|
|
|
colorWheelView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
addSubview(colorWheelView)
|
2019-04-20 22:36:22 +03:00
|
|
|
colorWheelViewWidthConstraint = colorWheelView.widthAnchor.constraint(equalTo: self.widthAnchor)
|
|
|
|
|
2019-04-12 04:38:30 +03:00
|
|
|
NSLayoutConstraint.activate([
|
2019-04-12 05:14:07 +03:00
|
|
|
colorWheelView.centerXAnchor.constraint(equalTo: self.centerXAnchor),
|
|
|
|
colorWheelView.centerYAnchor.constraint(equalTo: self.centerYAnchor),
|
2019-04-20 22:36:22 +03:00
|
|
|
colorWheelViewWidthConstraint,
|
2019-04-12 05:14:07 +03:00
|
|
|
colorWheelView.heightAnchor.constraint(equalTo: colorWheelView.widthAnchor),
|
2019-04-12 04:38:30 +03:00
|
|
|
])
|
|
|
|
}
|
2016-08-11 21:40:04 +03:00
|
|
|
|
2019-04-14 20:11:00 +03:00
|
|
|
func updateShadowIfNeeded() {
|
2019-04-12 06:08:00 +03:00
|
|
|
if showsShadow {
|
2019-04-14 20:11:00 +03:00
|
|
|
applyDropShadow(shadowProperties(forHeight: bounds.height))
|
2019-04-12 06:08:00 +03:00
|
|
|
} else {
|
|
|
|
removeDropShadow()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
internal func updateBorderIfNeeded() {
|
2019-04-20 22:36:22 +03:00
|
|
|
// Use view's background as a border so colorWheel subviews (handles)
|
|
|
|
// may appear above the border.
|
|
|
|
backgroundColor = borderWidth > 0 ? borderColor : .clear
|
|
|
|
layer.cornerRadius = bounds.height / 2.0
|
|
|
|
layer.masksToBounds = false
|
|
|
|
colorWheelViewWidthConstraint.constant = -borderWidth * 2.0
|
2016-08-11 21:40:04 +03:00
|
|
|
}
|
|
|
|
|
2019-04-12 06:08:00 +03:00
|
|
|
// MARK: Actions
|
2019-04-19 23:45:47 +03:00
|
|
|
|
2019-04-17 05:57:38 +03:00
|
|
|
@objc
|
|
|
|
internal func brightnessSliderDidValueChange(_ slider: ChromaBrightnessSlider) {
|
2019-04-19 23:45:47 +03:00
|
|
|
guard let currentHandle = currentHandle else { return }
|
2019-04-17 05:57:38 +03:00
|
|
|
|
2019-04-19 23:45:47 +03:00
|
|
|
currentHandle.color = slider.currentColor
|
|
|
|
informDelegateOfColorChange(on: currentHandle)
|
|
|
|
}
|
|
|
|
|
2019-05-02 17:29:52 +03:00
|
|
|
internal func informDelegateOfColorChange(on handle: ChromaColorHandle) { // TEMP:
|
|
|
|
delegate?.colorPickerHandleDidChange(self, handle: handle, to: handle.color)
|
2019-04-17 05:57:38 +03:00
|
|
|
}
|
2019-04-20 01:26:24 +03:00
|
|
|
|
|
|
|
// MARK: - Helpers
|
|
|
|
|
|
|
|
internal func extendedHitFrame(for handle: ChromaColorHandle) -> CGRect {
|
|
|
|
var frame = handle.frame
|
|
|
|
frame.size.height += handleHitboxExtensionY
|
|
|
|
return frame
|
|
|
|
}
|
|
|
|
|
|
|
|
internal func positionHandle(_ handle: ChromaColorHandle, forColorLocation location: CGPoint) {
|
|
|
|
handle.center = location.applying(CGAffineTransform.identity.translatedBy(x: 0, y: -handle.bounds.height / 2))
|
|
|
|
}
|
|
|
|
|
|
|
|
internal func animateHandleScale(_ handle: ChromaColorHandle, shouldGrow: Bool) {
|
|
|
|
if shouldGrow && handle.transform.d > 1 { return } // Already grown
|
2019-04-20 01:47:06 +03:00
|
|
|
let scalar: CGFloat = 1.25
|
2019-04-20 01:26:24 +03:00
|
|
|
|
2019-04-20 01:47:06 +03:00
|
|
|
var transform: CGAffineTransform = .identity
|
|
|
|
if shouldGrow {
|
|
|
|
let translateY = -handle.bounds.height * (scalar - 1) / 2
|
|
|
|
transform = CGAffineTransform(scaleX: scalar, y: scalar).translatedBy(x: 0, y: translateY)
|
|
|
|
}
|
2019-04-20 01:26:24 +03:00
|
|
|
|
|
|
|
UIView.animate(withDuration: 0.15, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.6, options: .curveEaseInOut, animations: {
|
|
|
|
handle.transform = transform
|
|
|
|
}, completion: nil)
|
|
|
|
}
|
2019-04-12 04:38:30 +03:00
|
|
|
}
|
2016-08-11 21:40:04 +03:00
|
|
|
|
2019-04-19 23:45:47 +03:00
|
|
|
internal let defaultHandleColorPosition: UIColor = .white
|
2019-05-02 17:29:52 +03:00
|
|
|
internal let defaultHandleSize: CGSize = CGSize(width: 42, height: 52)
|