256 lines
8.9 KiB
Swift
256 lines
8.9 KiB
Swift
//
|
|
// RadioButton.swift
|
|
// PrivadoVPN
|
|
//
|
|
// Created by Murad Shabanov on 23.07.2021.
|
|
// Copyright © 2021 Privado LLC. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
public class RadioButton: RadioBaseButton {
|
|
|
|
public var backgroundColor: NSColor = .gray {
|
|
didSet {
|
|
self.setupLayer()
|
|
}
|
|
}
|
|
|
|
public var innerCircleColor: NSColor = .white {
|
|
didSet {
|
|
self.setupLayer()
|
|
}
|
|
}
|
|
|
|
public var outerCircleColor: NSColor = .white {
|
|
didSet {
|
|
self.setupLayer()
|
|
}
|
|
}
|
|
|
|
public override var isEnabled: Bool {
|
|
didSet {
|
|
self.backLayer.opacity = self.isEnabled ? 1.0 : 0.5
|
|
}
|
|
}
|
|
|
|
public var textFont: NSFont? = NSFont.systemFont(ofSize: 14) {
|
|
didSet {
|
|
self.setupLayer()
|
|
}
|
|
}
|
|
|
|
public var fontSize: CGFloat?
|
|
|
|
public var textYoffset: CGFloat = 8 {
|
|
didSet {
|
|
self.setupLayer()
|
|
}
|
|
}
|
|
|
|
public var radioBoxSize: CGSize?
|
|
|
|
public var innerSelectedColor: NSColor = .gray
|
|
|
|
public weak var delegate: RadioButtonDelegate?
|
|
|
|
public override func layout() {
|
|
super.layout()
|
|
self.layer?.frame = self.frame
|
|
self.calculateRadioboxRect(width: self.frame.width, height: self.frame.height)
|
|
self.backLayer.frame = CGRect(origin: .zero, size: CGSize(width: self.radioSquare.width, height: self.radioSquare.height))
|
|
self.innerCircle.frame = CGRect(origin: .zero, size: CGSize(width: self.radioSquare.width, height: self.radioSquare.height))
|
|
self.outerCircle.frame = CGRect(origin: .zero, size: CGSize(width: self.radioSquare.width, height: self.radioSquare.height))
|
|
self.textLayer?.frame = CGRect(x: self.radioSquare.width + 5, y: self.textYoffset, width: self.bounds.width - self.radioSquare.width, height: self.bounds.height)
|
|
}
|
|
|
|
private lazy var backLayer = CAShapeLayer()
|
|
|
|
private lazy var outerCircle = CAShapeLayer()
|
|
|
|
private lazy var innerCircle = CAShapeLayer()
|
|
|
|
private var textLayer: CATextLayer?
|
|
|
|
private var radioSquare = CGRect.zero
|
|
|
|
override init(frame frameRect: NSRect) {
|
|
super.init(frame: frameRect)
|
|
self.setup()
|
|
}
|
|
|
|
init(frame frameRect: NSRect, title: String) {
|
|
super.init(frame: frameRect)
|
|
self.textLayer = CATextLayer()
|
|
self.textLayer?.string = title
|
|
self.setup()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func setup() {
|
|
let layer = CALayer()
|
|
self.wantsLayer = true
|
|
self.layer = layer
|
|
self.layer?.bounds = CGRect(origin: .zero, size: CGSize(width: self.frame.width, height: self.frame.height))
|
|
self.layer?.addSublayer(backLayer)
|
|
self.backLayer.addSublayer(outerCircle)
|
|
self.backLayer.addSublayer(innerCircle)
|
|
if let titleLayer = self.textLayer {
|
|
self.backLayer.addSublayer(titleLayer)
|
|
}
|
|
|
|
super.setup()
|
|
}
|
|
|
|
public override func layoutSubtreeIfNeeded() {
|
|
self.calculateRadioboxRect(width: self.bounds.width, height: self.bounds.height)
|
|
self.setupBackLayer()
|
|
self.setupOuterCircleLayer()
|
|
self.setupInnerCircleLayer()
|
|
self.setupTitleLayer()
|
|
}
|
|
|
|
override func setupLayer() {
|
|
self.calculateRadioboxRect(width: self.bounds.width, height: self.bounds.height)
|
|
self.setupBackLayer()
|
|
self.setupOuterCircleLayer()
|
|
self.setupInnerCircleLayer()
|
|
self.setupTitleLayer()
|
|
super.setupLayer()
|
|
}
|
|
|
|
private func setupTitleLayer() {
|
|
guard let titleLayer = self.textLayer else { return }
|
|
titleLayer.foregroundColor = .white
|
|
titleLayer.backgroundColor = .clear
|
|
titleLayer.frame = CGRect(x: self.radioSquare.width + 5, y: self.textYoffset, width: self.bounds.width - self.radioSquare.width, height: self.bounds.height)
|
|
titleLayer.font = self.textFont
|
|
titleLayer.fontSize = self.fontSize ?? 160
|
|
}
|
|
|
|
private func setupBackLayer() {
|
|
let path = NSBezierPath()
|
|
let radius = (self.radioSquare.width / 2) * 0.75
|
|
path.appendArc(withCenter: self.radioSquare.center, radius: radius, startAngle: 0, endAngle: 360)
|
|
self.backLayer.frame = CGRect(origin: .zero, size: CGSize(width: self.radioSquare.width, height: self.radioSquare.height))
|
|
self.backLayer.path = path.CGPath
|
|
self.backLayer.fillColor = self.backgroundColor.cgColor
|
|
self.backLayer.lineWidth = 4
|
|
self.backLayer.opacity = self.isEnabled ? 1.0 : 0.5
|
|
}
|
|
|
|
private func setupOuterCircleLayer() {
|
|
let path = NSBezierPath()
|
|
let radius = (self.radioSquare.width / 2) * 0.75
|
|
path.appendArc(withCenter: self.radioSquare.center, radius: radius, startAngle: 0, endAngle: 360)
|
|
self.outerCircle.frame = CGRect(origin: .zero, size: CGSize(width: self.radioSquare.width, height: self.radioSquare.height))
|
|
self.outerCircle.path = path.CGPath
|
|
self.outerCircle.fillColor = self.outerCircleColor.cgColor
|
|
self.outerCircle.strokeColor = self.outerCircleColor.cgColor
|
|
self.outerCircle.lineWidth = 0
|
|
self.outerCircle.opacity = 0
|
|
}
|
|
|
|
private func setupInnerCircleLayer() {
|
|
let path = NSBezierPath()
|
|
let radius = (self.radioSquare.width / 4) * 0.75
|
|
path.appendArc(withCenter: self.radioSquare.center, radius: radius, startAngle: 0, endAngle: 360)
|
|
self.innerCircle.frame = CGRect(origin: .zero, size: CGSize(width: self.radioSquare.width, height: self.radioSquare.height))
|
|
self.innerCircle.path = path.CGPath
|
|
self.innerCircle.fillColor = self.innerCircleColor.cgColor
|
|
self.innerCircle.strokeColor = NSColor.clear.cgColor
|
|
self.innerCircle.opacity = 0
|
|
}
|
|
|
|
func animateSelection() {
|
|
let animation = CABasicAnimation(keyPath: "opacity")
|
|
animation.duration = 0.2
|
|
animation.fromValue = 0.0
|
|
animation.toValue = 1.0
|
|
animation.isRemovedOnCompletion = false
|
|
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
|
let colorAnimation = CABasicAnimation(keyPath: "fillColor")
|
|
colorAnimation.fromValue = self.backgroundColor
|
|
colorAnimation.toValue = self.innerSelectedColor
|
|
self.outerCircle.opacity = 1
|
|
self.innerCircle.opacity = 1
|
|
self.outerCircle.add(animation, forKey: "opacity")
|
|
self.innerCircle.add(animation, forKey: "opacity")
|
|
self.backLayer.add(colorAnimation, forKey: "fillColor")
|
|
self.backLayer.fillColor = self.innerSelectedColor.cgColor
|
|
}
|
|
|
|
func animateDeselction() {
|
|
let animation = CABasicAnimation(keyPath: "opacity")
|
|
animation.duration = 0.2
|
|
animation.fromValue = 1.0
|
|
animation.toValue = 0.0
|
|
animation.isRemovedOnCompletion = false
|
|
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
|
let colorAnimation = CABasicAnimation(keyPath: "fillColor")
|
|
colorAnimation.fromValue = self.innerSelectedColor
|
|
colorAnimation.toValue = self.backgroundColor
|
|
colorAnimation.duration = 0.2
|
|
self.outerCircle.opacity = 0
|
|
self.innerCircle.opacity = 0
|
|
self.outerCircle.add(animation, forKey: "opacity")
|
|
self.innerCircle.add(animation, forKey: "opacity")
|
|
self.backLayer.add(colorAnimation, forKey: "fillColor")
|
|
self.backLayer.fillColor = self.backgroundColor.cgColor
|
|
}
|
|
|
|
private func calculateRadioboxRect(width: CGFloat, height: CGFloat) {
|
|
|
|
var width = Int(width)
|
|
var height = Int(height)
|
|
|
|
if height > width {
|
|
height -= height - width
|
|
} else if height < width {
|
|
width -= width - height
|
|
}
|
|
|
|
if let square = self.radioBoxSize {
|
|
self.radioSquare = CGRect(origin: .zero, size: square)
|
|
} else {
|
|
self.radioSquare = CGRect(origin: .zero, size: CGSize(width: CGFloat(width), height: CGFloat(height)))
|
|
}
|
|
}
|
|
|
|
override func updateActiveLayer() {
|
|
self.animateSelection()
|
|
}
|
|
|
|
override func updateInactiveLayer() {
|
|
self.animateDeselction()
|
|
}
|
|
|
|
override func callDelegate() {
|
|
if self.isOn {
|
|
self.delegate?.radioButtonDidSelect(self)
|
|
} else {
|
|
self.delegate?.radioButtonDidDeselect(self)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - RadioButtonDelegate
|
|
public protocol RadioButtonDelegate: AnyObject {
|
|
|
|
/// Delegate called when radio button is Selected
|
|
///
|
|
/// - Parameter button: RadioButton
|
|
func radioButtonDidSelect(_ button: RadioButton)
|
|
|
|
/// Delegate called when radio button is deselected
|
|
/// It will call automatically when user choose different radio button than selected
|
|
///
|
|
/// - Parameter button: RadioButton
|
|
func radioButtonDidDeselect(_ button: RadioButton)
|
|
|
|
}
|