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