Swiftgram/TelegramUI/WallpaperColorPickerNode.swift
Ilya Laktyushin a43f219f13 UI fixes
2019-01-17 22:05:49 +04:00

368 lines
15 KiB
Swift

import Foundation
import AsyncDisplayKit
import SwiftSignalKit
import Display
private let shadowImage: UIImage = {
return generateImage(CGSize(width: 45.0, height: 45.0), opaque: false, scale: nil, rotatedContext: { size, context in
context.setBlendMode(.clear)
context.setFillColor(UIColor.clear.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.normal)
context.setShadow(offset: CGSize(width: 0.0, height: 1.5), blur: 4.5, color: UIColor(rgb: 0x000000, alpha: 0.5).cgColor)
context.setFillColor(UIColor.black.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: 3.0 + UIScreenPixel, dy: 3.0 + UIScreenPixel))
})!
}()
private let pointerImage: UIImage = {
return generateImage(CGSize(width: 12.0, height: 42.0), opaque: false, scale: nil, rotatedContext: { size, context in
context.setBlendMode(.clear)
context.setFillColor(UIColor.clear.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.normal)
let lineWidth: CGFloat = 1.0
context.setFillColor(UIColor.black.cgColor)
context.setStrokeColor(UIColor.white.cgColor)
context.setLineWidth(lineWidth)
context.setLineCap(.round)
let pointerHeight: CGFloat = 6.0
context.move(to: CGPoint(x: lineWidth / 2.0, y: lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width - lineWidth / 2.0, y: lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width / 2.0, y: lineWidth / 2.0 + pointerHeight))
context.closePath()
context.drawPath(using: .fillStroke)
context.move(to: CGPoint(x: lineWidth / 2.0, y: size.height - lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width / 2.0, y: size.height - lineWidth / 2.0 - pointerHeight))
context.addLine(to: CGPoint(x: size.width - lineWidth / 2.0, y: size.height - lineWidth / 2.0))
context.closePath()
context.drawPath(using: .fillStroke)
})!
}()
private final class HSVParameter: NSObject {
let hue: CGFloat
let saturation: CGFloat
let value: CGFloat
init(hue: CGFloat, saturation: CGFloat, value: CGFloat) {
self.hue = hue
self.saturation = saturation
self.value = value
super.init()
}
}
private final class WallpaperColorKnobNode: ASDisplayNode {
var hsv: (CGFloat, CGFloat, CGFloat) = (0.0, 0.0, 1.0) {
didSet {
self.setNeedsDisplay()
}
}
override init() {
super.init()
self.isOpaque = false
self.displaysAsynchronously = true
self.isUserInteractionEnabled = false
}
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
return HSVParameter(hue: self.hsv.0, saturation: self.hsv.1, value: self.hsv.2)
}
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
guard let parameters = parameters as? HSVParameter else {
return
}
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
context.draw(shadowImage.cgImage!, in: bounds)
context.setBlendMode(.normal)
context.setFillColor(UIColor.white.cgColor)
context.fillEllipse(in: bounds.insetBy(dx: 3.0, dy: 3.0))
let color = UIColor(hue: parameters.hue, saturation: parameters.saturation, brightness: parameters.value, alpha: 1.0)
context.setFillColor(color.cgColor)
context.fillEllipse(in: bounds.insetBy(dx: 5.0 - UIScreenPixel, dy: 5.0 - UIScreenPixel))
}
}
private final class WallpaperColorHueSaturationNode: ASDisplayNode {
var value: CGFloat = 1.0 {
didSet {
self.setNeedsDisplay()
}
}
override init() {
super.init()
self.isOpaque = true
self.displaysAsynchronously = true
}
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
return HSVParameter(hue: 1.0, saturation: 1.0, value: self.value)
}
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
guard let parameters = parameters as? HSVParameter else {
return
}
let context = UIGraphicsGetCurrentContext()!
let colorSpace = CGColorSpaceCreateDeviceRGB()
let colors = [UIColor(rgb: 0xff0000).cgColor, UIColor(rgb: 0xffff00).cgColor, UIColor(rgb: 0x00ff00).cgColor, UIColor(rgb: 0x00ffff).cgColor, UIColor(rgb: 0x0000ff).cgColor, UIColor(rgb: 0xff00ff).cgColor, UIColor(rgb: 0xff0000).cgColor]
var locations: [CGFloat] = [0.0, 0.16667, 0.33333, 0.5, 0.66667, 0.83334, 1.0]
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: bounds.width, y: 0.0), options: CGGradientDrawingOptions())
let overlayColors = [UIColor(rgb: 0xffffff, alpha: 0.0).cgColor, UIColor(rgb: 0xffffff).cgColor]
var overlayLocations: [CGFloat] = [0.0, 1.0]
let overlayGradient = CGGradient(colorsSpace: colorSpace, colors: overlayColors as CFArray, locations: &overlayLocations)!
context.drawLinearGradient(overlayGradient, start: CGPoint(), end: CGPoint(x: 0.0, y: bounds.height), options: CGGradientDrawingOptions())
context.setFillColor(UIColor(rgb: 0x000000, alpha: 1.0 - parameters.value).cgColor)
context.fill(bounds)
}
}
private final class WallpaperColorBrightnessNode: ASDisplayNode {
var hsv: (CGFloat, CGFloat, CGFloat) = (0.0, 1.0, 1.0) {
didSet {
self.setNeedsDisplay()
}
}
override init() {
super.init()
self.isOpaque = true
self.displaysAsynchronously = true
}
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
return HSVParameter(hue: self.hsv.0, saturation: self.hsv.1, value: self.hsv.2)
}
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
guard let parameters = parameters as? HSVParameter else {
return
}
let context = UIGraphicsGetCurrentContext()!
let colorSpace = CGColorSpaceCreateDeviceRGB()
context.setFillColor(UIColor(white: parameters.value, alpha: 1.0).cgColor)
context.fill(bounds)
let path = UIBezierPath(roundedRect: bounds, cornerRadius: bounds.height / 2.0)
context.addPath(path.cgPath)
context.setFillColor(UIColor.white.cgColor)
context.fillPath()
let innerPath = UIBezierPath(roundedRect: bounds.insetBy(dx: 1.0, dy: 1.0), cornerRadius: bounds.height / 2.0)
context.addPath(innerPath.cgPath)
context.clip()
let color = UIColor(hue: parameters.hue, saturation: parameters.saturation, brightness: 1.0, alpha: 1.0)
let colors = [color.cgColor, UIColor.black.cgColor]
var locations: [CGFloat] = [0.0, 1.0]
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: bounds.width, y: 0.0), options: CGGradientDrawingOptions())
}
}
final class WallpaperColorPickerNode: ASDisplayNode {
private let brightnessNode: WallpaperColorBrightnessNode
private let brightnessKnobNode: ASImageNode
private let colorNode: WallpaperColorHueSaturationNode
private let colorKnobNode: WallpaperColorKnobNode
private var validLayout: CGSize?
var colorHSV: (CGFloat, CGFloat, CGFloat) = (0.0, 1.0, 1.0)
var color: UIColor {
get {
return UIColor(hue: self.colorHSV.0, saturation: self.colorHSV.1, brightness: self.colorHSV.2, alpha: 1.0)
}
set {
var hue: CGFloat = 0.0
var saturation: CGFloat = 0.0
var value: CGFloat = 0.0
let newHSV: (CGFloat, CGFloat, CGFloat)
if newValue.getHue(&hue, saturation: &saturation, brightness: &value, alpha: nil) {
newHSV = (hue, saturation, value)
} else {
newHSV = (0.0, 0.0, 1.0)
}
if newHSV != self.colorHSV {
self.colorHSV = newHSV
self.update()
}
}
}
var colorChanged: ((UIColor) -> Void)?
override init() {
self.brightnessNode = WallpaperColorBrightnessNode()
self.brightnessNode.hitTestSlop = UIEdgeInsetsMake(-16.0, -16.0, -16.0, -16.0)
self.brightnessKnobNode = ASImageNode()
self.brightnessKnobNode.image = pointerImage
self.colorNode = WallpaperColorHueSaturationNode()
self.colorNode.hitTestSlop = UIEdgeInsetsMake(-16.0, -16.0, -16.0, -16.0)
self.colorKnobNode = WallpaperColorKnobNode()
super.init()
self.backgroundColor = .white
self.addSubnode(self.brightnessNode)
self.addSubnode(self.brightnessKnobNode)
self.addSubnode(self.colorNode)
self.addSubnode(self.colorKnobNode)
self.update()
}
override func didLoad() {
super.didLoad()
let colorPanRecognizer = UIPanGestureRecognizer(target: self, action: #selector(WallpaperColorPickerNode.colorPan))
self.colorNode.view.addGestureRecognizer(colorPanRecognizer)
let colorTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(WallpaperColorPickerNode.colorTap))
self.colorNode.view.addGestureRecognizer(colorTapRecognizer)
let brightnessPanRecognizer = UIPanGestureRecognizer(target: self, action: #selector(WallpaperColorPickerNode.brightnessPan))
self.brightnessNode.view.addGestureRecognizer(brightnessPanRecognizer)
}
private func update() {
self.backgroundColor = UIColor(white: self.colorHSV.2, alpha: 1.0)
self.colorNode.value = self.colorHSV.2
self.brightnessNode.hsv = self.colorHSV
self.colorKnobNode.hsv = self.colorHSV
}
func updateKnobLayout(size: CGSize, panningColor: Bool, transition: ContainedViewLayoutTransition) {
let knobSize = CGSize(width: 45.0, height: 45.0)
let colorHeight: CGFloat = size.height - 66.0
var colorKnobFrame = CGRect(x: -knobSize.width / 2.0 + size.width * self.colorHSV.0, y: -knobSize.height / 2.0 + (colorHeight * (1.0 - self.colorHSV.1)), width: knobSize.width, height: knobSize.height)
var origin = colorKnobFrame.origin
if !panningColor {
origin = CGPoint(x: max(0.0, min(origin.x, size.width - knobSize.width)), y: max(0.0, min(origin.y, colorHeight - knobSize.height)))
} else {
origin = origin.offsetBy(dx: 0.0, dy: -32.0)
}
colorKnobFrame.origin = origin
transition.updateFrame(node: self.colorKnobNode, frame: colorKnobFrame)
let inset: CGFloat = 42.0
let brightnessKnobSize = CGSize(width: 12.0, height: 42.0)
let brightnessKnobFrame = CGRect(x: inset - brightnessKnobSize.width / 2.0 + (size.width - inset * 2.0) * (1.0 - self.colorHSV.2), y: size.height - 61.0, width: brightnessKnobSize.width, height: brightnessKnobSize.height)
transition.updateFrame(node: self.brightnessKnobNode, frame: brightnessKnobFrame)
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayout = size
transition.updateFrame(node: self.colorNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height - 66.0))
let inset: CGFloat = 42.0
transition.updateFrame(node: self.brightnessNode, frame: CGRect(x: inset, y: size.height - 55.0, width: size.width - inset * 2.0, height: 29.0))
self.updateKnobLayout(size: size, panningColor: false, transition: transition)
}
@objc private func colorTap(_ recognizer: UITapGestureRecognizer) {
guard let size = self.validLayout, recognizer.state == .recognized else {
return
}
let location = recognizer.location(in: recognizer.view)
let newHue = max(0.0, min(1.0, location.x / size.width))
let newSaturation = max(0.0, min(1.0, (1.0 - location.y / (size.height - 66.0))))
self.colorHSV.0 = newHue
self.colorHSV.1 = newSaturation
self.updateKnobLayout(size: size, panningColor: false, transition: .immediate)
self.update()
self.colorChanged?(self.color)
}
@objc private func colorPan(_ recognizer: UIPanGestureRecognizer) {
guard let size = self.validLayout else {
return
}
let location = recognizer.location(in: recognizer.view)
let transition = recognizer.translation(in: recognizer.view)
if recognizer.state == .began {
let newHue = max(0.0, min(1.0, location.x / size.width))
let newSaturation = max(0.0, min(1.0, (1.0 - location.y / (size.height - 66.0))))
self.colorHSV.0 = newHue
self.colorHSV.1 = newSaturation
} else {
let newHue = max(0.0, min(1.0, self.colorHSV.0 + transition.x / size.width))
let newSaturation = max(0.0, min(1.0, self.colorHSV.1 - transition.y / (size.height - 66.0)))
self.colorHSV.0 = newHue
self.colorHSV.1 = newSaturation
}
switch recognizer.state {
case .began:
self.updateKnobLayout(size: size, panningColor: true, transition: .immediate)
case .changed:
self.updateKnobLayout(size: size, panningColor: true, transition: .immediate)
recognizer.setTranslation(CGPoint(), in: recognizer.view)
case .ended:
self.updateKnobLayout(size: size, panningColor: false, transition: .animated(duration: 0.3, curve: .easeInOut))
default:
break
}
self.update()
self.colorChanged?(self.color)
}
@objc private func brightnessPan(_ recognizer: UIPanGestureRecognizer) {
guard let size = self.validLayout else {
return
}
let transition = recognizer.translation(in: recognizer.view)
let brightnessWidth: CGFloat = size.width - 42.0 * 2.0
let newValue = max(0.0, min(1.0, self.colorHSV.2 - transition.x / brightnessWidth))
self.colorHSV.2 = newValue
switch recognizer.state {
case .changed:
self.updateKnobLayout(size: size, panningColor: false, transition: .immediate)
recognizer.setTranslation(CGPoint(), in: recognizer.view)
case .ended:
self.updateKnobLayout(size: size, panningColor: false, transition: .immediate)
default:
break
}
self.update()
self.colorChanged?(self.color)
}
}