Files
2022-04-25 07:28:13 +00:00

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)
}