Add preview view to the color palette control.

This commit is contained in:
Matthew Chen 2019-03-14 12:16:38 -04:00
parent 283741364c
commit 441c784146
3 changed files with 157 additions and 12 deletions

View File

@ -264,13 +264,3 @@ public class GalleryRailView: UIView, GalleryRailCellViewDelegate {
}
}
}
public extension CGSize {
var aspectRatio: CGFloat {
guard self.height > 0 else {
return 0
}
return self.width / self.height
}
}

View File

@ -62,6 +62,118 @@ public class ImageEditorColor: NSObject {
// MARK: -
private class PalettePreviewView: OWSLayerView {
private static let innerRadius: CGFloat = 32
private static let shadowMargin: CGFloat = 0
// The distance from the "inner circle" to the "teardrop".
private static let circleMargin: CGFloat = 3
private static let teardropTipRadius: CGFloat = 4
private static let teardropPointiness: CGFloat = 12
private let teardropColor = UIColor.white
public var selectedColor = UIColor.white {
didSet {
circleLayer.fillColor = selectedColor.cgColor
}
}
private let circleLayer: CAShapeLayer
private let teardropLayer: CAShapeLayer
override init() {
let circleLayer = CAShapeLayer()
let teardropLayer = CAShapeLayer()
self.circleLayer = circleLayer
self.teardropLayer = teardropLayer
super.init()
circleLayer.strokeColor = nil
teardropLayer.strokeColor = nil
// Layer order matters.
layer.addSublayer(teardropLayer)
layer.addSublayer(circleLayer)
teardropLayer.fillColor = teardropColor.cgColor
teardropLayer.shadowColor = UIColor.black.cgColor
teardropLayer.shadowRadius = 2.0
teardropLayer.shadowOpacity = 0.33
teardropLayer.shadowOffset = .zero
layoutCallback = { (view) in
PalettePreviewView.updateLayers(view: view,
circleLayer: circleLayer,
teardropLayer: teardropLayer)
}
// The bounding rect of the teardrop + shadow is non-trivial, so
// we use a generous size that reserves plenty of space.
//
// The size doesn't matter since this view is
// mostly transparent and isn't hot.
autoSetDimensions(to: CGSize(width: PalettePreviewView.innerRadius * 4,
height: PalettePreviewView.innerRadius * 4))
}
@available(*, unavailable, message: "use other init() instead.")
required public init?(coder aDecoder: NSCoder) {
notImplemented()
}
static func updateLayers(view: UIView,
circleLayer: CAShapeLayer,
teardropLayer: CAShapeLayer) {
let bounds = view.bounds
let outerRadius = innerRadius + circleMargin
let rightEdge = CGPoint(x: bounds.width,
y: bounds.height * 0.5)
let teardropTipCenter = rightEdge.minus(CGPoint(x: teardropTipRadius + shadowMargin, y: 0))
let circleCenter = teardropTipCenter.minus(CGPoint(x: teardropPointiness + innerRadius, y: 0))
// The "teardrop" shape is bounded by 2 circles, joined by their tangents.
//
// UIBezierPath can be used to draw this using 2 arcs, if we
// have the angle of the tangents.
//
// Finding the tangent between two circles of known distance + radius
// is pretty straightforward. We solve for the right triangle that
// defines the tangents and atan() that triangle to get the angle.
//
// 1. Find the length of the hypotenuse.
let circleCenterDistance = teardropTipCenter.minus(circleCenter).length
// 2. Fine the length of the first side.
let radiusDiff = outerRadius - teardropTipRadius
// 2. Fine the length of the second side.
let tangentLength = (circleCenterDistance.square - radiusDiff.square).squareRoot()
let angle = atan2(tangentLength, radiusDiff)
let teardropPath = UIBezierPath()
teardropPath.addArc(withCenter: circleCenter,
radius: outerRadius,
startAngle: +angle,
endAngle: -angle,
clockwise: true)
teardropPath.addArc(withCenter: teardropTipCenter,
radius: teardropTipRadius,
startAngle: -angle,
endAngle: +angle,
clockwise: true)
teardropLayer.path = teardropPath.cgPath
teardropLayer.frame = bounds
let innerCircleSize = CGSize(width: innerRadius * 2,
height: innerRadius * 2)
let circleFrame = CGRect(origin: circleCenter.minus(innerCircleSize.asPoint.times(0.5)),
size: innerCircleSize)
circleLayer.path = UIBezierPath(ovalIn: circleFrame).cgPath
circleLayer.frame = bounds
}
}
// MARK: -
public class ImageEditorPaletteView: UIView {
public weak var delegate: ImageEditorPaletteViewDelegate?
@ -89,6 +201,8 @@ public class ImageEditorPaletteView: UIView {
private let imageWrapper = OWSLayerView()
private let shadowView = UIView()
private var selectionConstraint: NSLayoutConstraint?
private let previewView = PalettePreviewView()
private var previewConstraint: NSLayoutConstraint?
private func createContents() {
self.backgroundColor = .clear
@ -140,6 +254,14 @@ public class ImageEditorPaletteView: UIView {
selectionConstraint.autoInstall()
self.selectionConstraint = selectionConstraint
previewView.isHidden = true
addSubview(previewView)
previewView.autoPinEdge(.trailing, to: .leading, of: imageView, withOffset: -24)
let previewConstraint = NSLayoutConstraint(item: previewView,
attribute: .centerY, relatedBy: .equal, toItem: imageWrapper, attribute: .top, multiplier: 1, constant: 0)
previewConstraint.autoInstall()
self.previewConstraint = previewConstraint
isUserInteractionEnabled = true
addGestureRecognizer(PaletteGestureRecognizer(target: self, action: #selector(didTouch)))
@ -208,6 +330,7 @@ public class ImageEditorPaletteView: UIView {
private func updateState() {
selectionView.backgroundColor = selectedValue.color
previewView.selectedColor = selectedValue.color
guard let selectionConstraint = selectionConstraint else {
owsFailDebug("Missing selectionConstraint.")
@ -215,6 +338,12 @@ public class ImageEditorPaletteView: UIView {
}
let selectionY = imageWrapper.height() * selectedValue.palettePhase
selectionConstraint.constant = selectionY
guard let previewConstraint = previewConstraint else {
owsFailDebug("Missing previewConstraint.")
return
}
previewConstraint.constant = selectionY
}
// MARK: Events
@ -222,9 +351,12 @@ public class ImageEditorPaletteView: UIView {
@objc
func didTouch(gesture: UIGestureRecognizer) {
switch gesture.state {
case .began, .changed, .ended:
break
case .began, .changed:
previewView.isHidden = false
case .ended:
previewView.isHidden = true
default:
previewView.isHidden = true
return
}

View File

@ -149,6 +149,10 @@ public extension CGFloat {
public func fuzzyEquals(_ other: CGFloat, tolerance: CGFloat = 0.001) -> Bool {
return abs(self - other) < tolerance
}
public var square: CGFloat {
return self * self
}
}
public extension Int {
@ -222,6 +226,25 @@ public extension CGPoint {
return (x.fuzzyEquals(other.x, tolerance: tolerance) &&
y.fuzzyEquals(other.y, tolerance: tolerance))
}
public static func tan(angle: CGFloat) -> CGPoint {
return CGPoint(x: sin(angle),
y: cos(angle))
}
}
public extension CGSize {
var aspectRatio: CGFloat {
guard self.height > 0 else {
return 0
}
return self.width / self.height
}
var asPoint: CGPoint {
return CGPoint(x: width, y: height)
}
}
public extension CGRect {