From 5de59e1092e5af87c804aed66eaa7c774e36d313 Mon Sep 17 00:00:00 2001 From: Jonathan Cardasis Date: Fri, 19 Apr 2019 18:26:24 -0400 Subject: [PATCH] Refactor color handle layer positioning, pulling up layout logic to parent. Picker now uses touch delegate events instead of a gesture for faster recognition. Additional handle positioning improvements. --- Source/ChromaColorPicker.swift | 189 ++++++++++++------------- Source/ColorWheelView.swift | 20 +-- Source/Helpers/ChromaColorHandle.swift | 7 +- 3 files changed, 103 insertions(+), 113 deletions(-) diff --git a/Source/ChromaColorPicker.swift b/Source/ChromaColorPicker.swift index f7f3d77..610a27f 100644 --- a/Source/ChromaColorPicker.swift +++ b/Source/ChromaColorPicker.swift @@ -39,7 +39,14 @@ public class ChromaColorPicker: UIControl, ChromaControlStylable { } } - //public var handleSize: CGSize { /* TODO */ } + /// The size handles should be displayed at. + public var handleSize: CGSize = defaultHandleSize { + didSet { setNeedsLayout() } + } + + /// An extension to handles' hitboxes in the +Y direction. + /// Allows for handles to be grabbed more easily. + public var handleHitboxExtensionY: CGFloat = 10.0 /// Handles added to the color picker. private(set) public var handles: [ChromaColorHandle] = [] @@ -65,9 +72,9 @@ public class ChromaColorPicker: UIControl, ChromaControlStylable { updateBorderIfNeeded() handles.forEach { handle in - //let location = colorWheelView.location(of: handle.color) + let location = colorWheelView.location(of: handle.color) handle.frame.size = defaultHandleSize - //handle.center = location + positionHandle(handle, forColorLocation: location) } } @@ -75,16 +82,13 @@ public class ChromaColorPicker: UIControl, ChromaControlStylable { @discardableResult public func addHandle(at color: UIColor? = nil) -> ChromaColorHandle { - let handleColor = color ?? defaultHandleColorPosition let handle = ChromaColorHandle() - handle.color = handleColor - + handle.color = color ?? defaultHandleColorPosition addHandle(handle) return handle } public func addHandle(_ handle: ChromaColorHandle) { - addPanGesture(to: handle) handles.append(handle) colorWheelView.addSubview(handle) } @@ -101,49 +105,66 @@ public class ChromaColorPicker: UIControl, ChromaControlStylable { self.backgroundColor = UIColor.clear self.layer.masksToBounds = false setupColorWheelView() - //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) -// } + public override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { + let location = touch.location(in: self) + + for handle in handles { + if extendedHitFrame(for: handle).contains(location) { + currentHandle = handle + colorWheelView.bringSubviewToFront(handle) + animateHandleScale(handle, shouldGrow: true) + return true + } + } + return false + } + + 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. + let center = colorWheelView.center + 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)) + print("pos: \(positionOnColorWheelEdge)") + 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 + } + + public override func endTracking(_ touch: UITouch?, with event: UIEvent?) { + if let handle = currentHandle { + animateHandleScale(handle, shouldGrow: false) + } + 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) + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + // Self should handle all touch events, forwarding if needed. + return self } // MARK: Setup & Layout @@ -172,62 +193,8 @@ public class ChromaColorPicker: UIControl, ChromaControlStylable { colorWheelView.layer.borderWidth = borderWidth } - internal func setupGestures() { - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(colorWheelTapped(_:))) - colorWheelView.isUserInteractionEnabled = true - colorWheelView.addGestureRecognizer(tapGesture) - } - // 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 } @@ -239,7 +206,29 @@ public class ChromaColorPicker: UIControl, ChromaControlStylable { internal func informDelegateOfColorChange(on handle: ChromaColorHandle) { delegate?.colorPickerDidChooseColor(self, color: handle.color) } + + // 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 + + let transform = shouldGrow ? CGAffineTransform(scaleX: 1.25, y: 1.25) : CGAffineTransform(scaleX: 1, y: 1) + + UIView.animate(withDuration: 0.15, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.6, options: .curveEaseInOut, animations: { + handle.transform = transform + }, completion: nil) + } } internal let defaultHandleColorPosition: UIColor = .white -internal let defaultHandleSize: CGSize = CGSize(width: 42, height: 42) +internal let defaultHandleSize: CGSize = CGSize(width: 34, height: 42) diff --git a/Source/ColorWheelView.swift b/Source/ColorWheelView.swift index a9ecbb2..67a7933 100644 --- a/Source/ColorWheelView.swift +++ b/Source/ColorWheelView.swift @@ -37,6 +37,7 @@ public class ColorWheelView: UIView { /** Returns the (x,y) location of the color provided within the ColorWheelView. + Disregards color's brightness component. */ public func location(of color: UIColor) -> CGPoint { var hue: CGFloat = 0 @@ -86,6 +87,17 @@ public class ColorWheelView: UIView { return color } + /** + Returns whether or not the point is in the circular area of the color wheel. + */ + public func pointIsInColorWheel(_ point: CGPoint) -> Bool { + guard bounds.offsetBy(dx: 1, dy: 1).contains(point) else { return false } + + let distanceFromCenter: CGFloat = hypot(center.x - point.x, center.y - point.y) + let pointExistsInRadius: Bool = distanceFromCenter <= (radius - layer.borderWidth) + return pointExistsInRadius + } + // MARK: - Private internal let imageView = UIImageView() @@ -123,12 +135,4 @@ public class ColorWheelView: UIView { ]) return filter?.outputImage } - - internal func pointIsInColorWheel(_ point: CGPoint) -> Bool { - guard bounds.offsetBy(dx: 1, dy: 1).contains(point) else { return false } - - let distanceFromCenter: CGFloat = hypot(center.x - point.x, center.y - point.y) - let pointExistsInRadius: Bool = distanceFromCenter <= (radius - layer.borderWidth) - return pointExistsInRadius - } } diff --git a/Source/Helpers/ChromaColorHandle.swift b/Source/Helpers/ChromaColorHandle.swift index eb8da8c..a64e4b1 100644 --- a/Source/Helpers/ChromaColorHandle.swift +++ b/Source/Helpers/ChromaColorHandle.swift @@ -62,7 +62,6 @@ public class ChromaColorHandle: UIView, ChromaControlStylable { internal let handleShape = CAShapeLayer() internal func commonInit() { - backgroundColor = UIColor.red.withAlphaComponent(0.25) //DEBUG layer.addSublayer(handleShape) } @@ -87,14 +86,12 @@ public class ChromaColorHandle: UIView, ChromaControlStylable { } internal func layoutHandleShape() { - let size = CGSize(width: bounds.height * handleWidthHeightRatio, height: bounds.height) + let size = CGSize(width: bounds.width - borderWidth, height: bounds.height - borderWidth) 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.frame = CGRect(origin: CGPoint(x: bounds.midX - (size.width / 2), y: bounds.midY - (size.height / 2)), size: size) handleShape.fillColor = color.cgColor handleShape.strokeColor = borderColor.cgColor handleShape.lineWidth = borderWidth } } - -internal let handleWidthHeightRatio: CGFloat = 52.0 / 65