Commit 52dd9554 by Daniel Dahan Committed by GitHub

Merge pull request #1004 from OrkhanAlikhanov/development

RadioButton/CheckButton and RadioButtonGroup/CheckButtonGroup
parents 6cb79199 68c47fb9
......@@ -174,6 +174,12 @@
96E3C39A1D3A1CC20086A024 /* ErrorTextField.swift in Headers */ = {isa = PBXBuildFile; fileRef = 961F18E71CD93E3E008927C5 /* ErrorTextField.swift */; settings = {ATTRIBUTES = (Public, ); }; };
96E3C39C1D3A1CC20086A024 /* Offset.swift in Headers */ = {isa = PBXBuildFile; fileRef = 968C99461D377849000074FF /* Offset.swift */; settings = {ATTRIBUTES = (Public, ); }; };
96F1A5531F24F17A001D8CAF /* TabsController.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96E09DC71F2287E50000B121 /* TabsController.swift */; settings = {ATTRIBUTES = (Public, ); }; };
9DE84D721FF0252600586C8B /* RadioButtonGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DE84D6F1FF0252500586C8B /* RadioButtonGroup.swift */; };
9DE84D731FF0252600586C8B /* BaseButtonGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DE84D701FF0252500586C8B /* BaseButtonGroup.swift */; };
9DE84D741FF0252600586C8B /* CheckButtonGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DE84D711FF0252500586C8B /* CheckButtonGroup.swift */; };
9DF352421FED20C900B2A11B /* BaseIconLayerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DF352411FED20C900B2A11B /* BaseIconLayerButton.swift */; };
9DF352441FED20ED00B2A11B /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DF352431FED20ED00B2A11B /* RadioButton.swift */; };
9DF352461FED210000B2A11B /* CheckButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DF352451FED210000B2A11B /* CheckButton.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
......@@ -285,6 +291,12 @@
96E09DC71F2287E50000B121 /* TabsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabsController.swift; sourceTree = "<group>"; };
96E3C3931D397AE90086A024 /* Material+UIView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Material+UIView.swift"; sourceTree = "<group>"; };
96F1DC871D654FDF0025F925 /* Material+CALayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Material+CALayer.swift"; sourceTree = "<group>"; };
9DE84D6F1FF0252500586C8B /* RadioButtonGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadioButtonGroup.swift; sourceTree = "<group>"; };
9DE84D701FF0252500586C8B /* BaseButtonGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseButtonGroup.swift; sourceTree = "<group>"; };
9DE84D711FF0252500586C8B /* CheckButtonGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckButtonGroup.swift; sourceTree = "<group>"; };
9DF352411FED20C900B2A11B /* BaseIconLayerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseIconLayerButton.swift; sourceTree = "<group>"; };
9DF352431FED20ED00B2A11B /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = "<group>"; };
9DF352451FED210000B2A11B /* CheckButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckButton.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXGroup section */
......@@ -520,6 +532,7 @@
96264BE41D833C8400576F37 /* Bar */,
962DDD081D6FBBD0001C307C /* BottomTabBar */,
96BCB8031CB40F4B00C806FE /* Button */,
9DE84D6E1FF0250E00586C8B /* ButtonGroup */,
96BCB8021CB40F3B00C806FE /* Card */,
961154CA1F32999000A78D74 /* Chip */,
96BCB8051CB40F9C00C806FE /* Collection */,
......@@ -609,6 +622,9 @@
96BCB7601CB40DC500C806FE /* FlatButton.swift */,
96BCB7931CB40DC500C806FE /* RaisedButton.swift */,
9658F2161CD6FA4700B902C1 /* IconButton.swift */,
9DF352411FED20C900B2A11B /* BaseIconLayerButton.swift */,
9DF352431FED20ED00B2A11B /* RadioButton.swift */,
9DF352451FED210000B2A11B /* CheckButton.swift */,
);
name = Button;
sourceTree = "<group>";
......@@ -732,6 +748,16 @@
name = Animation;
sourceTree = "<group>";
};
9DE84D6E1FF0250E00586C8B /* ButtonGroup */ = {
isa = PBXGroup;
children = (
9DE84D701FF0252500586C8B /* BaseButtonGroup.swift */,
9DE84D6F1FF0252500586C8B /* RadioButtonGroup.swift */,
9DE84D711FF0252500586C8B /* CheckButtonGroup.swift */,
);
name = ButtonGroup;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
......@@ -919,6 +945,7 @@
buildActionMask = 2147483647;
files = (
961E6BE21DDA2AF3004E6C93 /* Screen.swift in Sources */,
9DF352441FED20ED00B2A11B /* RadioButton.swift in Sources */,
965E81261DD4D7C800D61E4B /* Material+NSMutableAttributedString.swift in Sources */,
965E80FF1DD4D5C800D61E4B /* BottomNavigationController.swift in Sources */,
965E81031DD4D5C800D61E4B /* CollectionView.swift in Sources */,
......@@ -926,6 +953,7 @@
965E81071DD4D5C800D61E4B /* CollectionViewLayout.swift in Sources */,
965E81081DD4D5C800D61E4B /* CollectionReusableView.swift in Sources */,
965E81091DD4D5C800D61E4B /* DataSourceItem.swift in Sources */,
9DF352461FED210000B2A11B /* CheckButton.swift in Sources */,
965E810A1DD4D5C800D61E4B /* Font.swift in Sources */,
965E810B1DD4D5C800D61E4B /* RobotoFont.swift in Sources */,
965E810C1DD4D5C800D61E4B /* DynamicFontType.swift in Sources */,
......@@ -940,6 +968,7 @@
965E81181DD4D5C800D61E4B /* Snackbar.swift in Sources */,
965E81191DD4D5C800D61E4B /* SnackbarController.swift in Sources */,
9618006D1F4D384200CD77A1 /* Material+UIViewController.swift in Sources */,
9DE84D741FF0252600586C8B /* CheckButtonGroup.swift in Sources */,
965E811A1DD4D5C800D61E4B /* StatusBarController.swift in Sources */,
965E811B1DD4D5C800D61E4B /* Switch.swift in Sources */,
965E811C1DD4D5C800D61E4B /* TabBar.swift in Sources */,
......@@ -951,6 +980,7 @@
965E80E71DD4C55200D61E4B /* Material+UIView.swift in Sources */,
965E80E81DD4C55200D61E4B /* Material+CALayer.swift in Sources */,
965E80E91DD4C55200D61E4B /* Material+String.swift in Sources */,
9DE84D731FF0252600586C8B /* BaseButtonGroup.swift in Sources */,
965E80F71DD4D59500D61E4B /* Card.swift in Sources */,
965E80EA1DD4C55200D61E4B /* Material+UIFont.swift in Sources */,
965E80EB1DD4C55200D61E4B /* Material+UIImage.swift in Sources */,
......@@ -958,8 +988,10 @@
965E80ED1DD4C55200D61E4B /* Material+UIWindow.swift in Sources */,
961527B91F3A509900E8B2AC /* ChipBarController.swift in Sources */,
965E80E41DD4C53300D61E4B /* PulseView.swift in Sources */,
9DF352421FED20C900B2A11B /* BaseIconLayerButton.swift in Sources */,
966C17731F0439F600D3E83C /* Material+MotionAnimation.swift in Sources */,
965E80E51DD4C53300D61E4B /* PulseAnimation.swift in Sources */,
9DE84D721FF0252600586C8B /* RadioButtonGroup.swift in Sources */,
965E80FE1DD4D59500D61E4B /* ToolbarController.swift in Sources */,
96328B971E05C0BB009A4C90 /* TableView.swift in Sources */,
965E80F81DD4D59500D61E4B /* ImageCard.swift in Sources */,
......
//
// BaseButtonGroup.swift
// Material
//
// Created by Orkhan Alikhanov on 12/24/17.
// Copyright © 2017 CosmicMind, Inc. All rights reserved.
//
import UIKit
open class BaseButtonGroup<T: Button>: View {
/// Holds reference to buttons within the group.
open var buttons: [T] = [] {
didSet {
oldValue.forEach {
$0.removeFromSuperview()
$0.removeTarget(self, action: #selector(didTap(_:)), for: .touchUpInside)
}
prepareButtons()
grid.views = buttons
grid.axis.rows = buttons.count
}
}
/// Initializes group with the provided buttons.
///
/// - Parameter buttons: Array of buttons.
public convenience init(buttons: [T]) {
self.init(frame: .zero)
defer { self.buttons = buttons } // defer allows didSet to be called
}
open override func prepare() {
super.prepare()
grid.axis.direction = .vertical
grid.axis.columns = 1
}
open override var intrinsicContentSize: CGSize { return sizeThatFits(bounds.size) }
open override func sizeThatFits(_ size: CGSize) -> CGSize {
let size = CGSize(width: size.width == 0 ? .greatestFiniteMagnitude : size.width, height: size.height == 0 ? .greatestFiniteMagnitude : size.height)
let availableW = size.width - grid.contentEdgeInsets.left - grid.contentEdgeInsets.right - grid.layoutEdgeInsets.left - grid.layoutEdgeInsets.right
let maxW = buttons.reduce(0) { max($0, $1.sizeThatFits(.init(width: availableW, height: .greatestFiniteMagnitude)).width) }
let h = buttons.reduce(0) { $0 + $1.sizeThatFits(.init(width: maxW, height: .greatestFiniteMagnitude)).height }
+ grid.contentEdgeInsets.top + grid.contentEdgeInsets.bottom
+ grid.layoutEdgeInsets.top + grid.layoutEdgeInsets.bottom
+ CGFloat(buttons.count - 1) * grid.interimSpace
return CGSize(width: maxW + grid.contentEdgeInsets.left + grid.contentEdgeInsets.right + grid.layoutEdgeInsets.left + grid.layoutEdgeInsets.right, height: min(h, size.height))
}
open override func layoutSubviews() {
super.layoutSubviews()
grid.reload()
}
open func didTap(button: T, at index: Int) { }
@objc
private func didTap(_ sender: Button) {
guard let sender = sender as? T,
let index = buttons.index(of: sender)
else { return }
didTap(button: sender, at: index)
}
}
private extension BaseButtonGroup {
func prepareButtons() {
buttons.forEach {
addSubview($0)
$0.removeTarget(nil, action: nil, for: .allEvents)
$0.addTarget(self, action: #selector(didTap(_:)), for: .touchUpInside)
}
}
}
//
// BaseIconLayerButton.swift
// Material
//
// Created by Orkhan Alikhanov on 12/22/18.
// Copyright © 2017 CosmicMind. All rights reserved.
//
import UIKit
import Motion
/// Implements common logic for CheckButton and RadioButton
open class BaseIconLayerButton: Button {
class var iconLayer: BaseIconLayer { fatalError("Has to be implemented by subclasses") }
lazy var iconLayer: BaseIconLayer = type(of: self).iconLayer
/// A Boolean value indicating whether the button is in the selected state
///
/// Use `setSelected(_:, animated:)` if the state change needs to be animated
open override var isSelected: Bool {
didSet {
iconLayer.setSelected(isSelected, animated: false)
}
}
/// A Boolean value indicating whether the control is enabled.
open override var isEnabled: Bool {
didSet {
iconLayer.isEnabled = isEnabled
}
}
/// Sets the color of the icon to use for the specified state.
///
/// - Parameters:
/// - color: The color of the icon to use for the specified state.
/// - state: The state that uses the specified color. Supports only (.normal, .selected, .disabled)
open func setIconColor(_ color: UIColor, for state: UIControlState) {
switch state {
case .normal:
iconLayer.normalColor = color
case .selected:
iconLayer.selectedColor = color
case .disabled:
iconLayer.disabledColor = color
default:
fatalError("unsupported state")
}
}
/// Returns the icon color used for a state.
///
/// - Parameter state: The state that uses the icon color. Supports only (.normal, .selected, .disabled)
/// - Returns: The color of the title for the specified state.
open func iconColor(for state: UIControlState) -> UIColor {
switch state {
case .normal:
return iconLayer.normalColor
case .selected:
return iconLayer.selectedColor
case .disabled:
return iconLayer.disabledColor
default:
fatalError("unsupported state")
}
}
/// A Boolean value indicating whether the button is being animated
open var isAnimating: Bool { return iconLayer.isAnimating }
/// Sets the `selected` state of the button, optionally animating the transition.
///
/// - Parameters:
/// - isSelected: A Boolean value indicating new `selected` state
/// - animated: true if the state change should be animated, otherwise false.
open func setSelected(_ isSelected: Bool, animated: Bool) {
guard !isAnimating else { return }
iconLayer.setSelected(isSelected, animated: animated)
self.isSelected = isSelected
}
open override func prepare() {
super.prepare()
layer.addSublayer(iconLayer)
contentHorizontalAlignment = .left // default was .center
reloadImage()
}
open override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// pulse.animation set to .none so that when we call `super.touchesBegan`
// pulse will not expand as there is a `guard` against .none case
pulse.animation = .none
super.touchesBegan(touches, with: event)
pulse.animation = .point
// expand pulse from the center of iconLayer/visualLayer (`point` is relative to self.view/self.layer)
pulse.expand(point: iconLayer.frame.center)
}
open override func layoutSubviews() {
super.layoutSubviews()
// positioning iconLayer
let insets = iconEdgeInsets
iconLayer.frame.size = CGSize(width: iconSize, height: iconSize)
iconLayer.frame.origin = CGPoint(x: imageView!.frame.minX + insets.left, y: imageView!.frame.minY + insets.top)
// visualLayer is the layer where pulse layer is expanding.
// So we position it at the center of iconLayer, and make it
// small circle, so that the expansion of pulse layer is clipped off
let w = iconSize + insets.left + insets.right
let h = iconSize + insets.top + insets.bottom
let pulseSize = min(w, h)
visualLayer.bounds.size = CGSize(width: pulseSize, height: pulseSize)
visualLayer.frame.center = iconLayer.frame.center
visualLayer.cornerRadius = pulseSize / 2
}
/// Size of the icon
///
/// This property affects `intrinsicContentSize` and `sizeThatFits(_:)`
/// Use `iconEdgeInsets` to set margins.
open var iconSize: CGFloat = 16 {
didSet {
reloadImage()
}
}
/// The *outset* margins for the rectangle around the button’s icon.
///
/// You can specify a different value for each of the four margins (top, left, bottom, right)
/// This property affects `intrinsicContentSize` and `sizeThatFits(_:)` and position of the icon
/// within the rectangle.
///
/// You can use `iconSize` and this property, or `titleEdgeInsets` and `contentEdgeInsets` to position
/// the icon however you want.
/// For negative values, behavior is undefined. Default is `5.0` for all four margins
open var iconEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5) {
didSet {
reloadImage()
}
}
/// This might be considered as a hackish way, but it's just manipulation
/// UIButton considers size of the `currentImage` to determine `intrinsicContentSize`
/// and `sizeThatFits(_:)`, and to position `titleLabel`.
/// So, we make use of this property (by setting transparent image) to make room for our icon
/// without making much effort (like playing with `titleEdgeInsets` and `contentEdgeInsets`)
/// Size of the image equals to `iconSize` plus corresponsing `iconEdgeInsets` values
private func reloadImage() {
let insets = iconEdgeInsets
let w = iconSize + insets.left + insets.right
let h = iconSize + insets.top + insets.bottom
UIGraphicsBeginImageContext(CGSize(width: w, height: h))
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.image = image
}
}
// MARK: - BaseIconLayer
internal class BaseIconLayer: CALayer {
var selectedColor = Color.blue.base
var normalColor = Color.lightGray
var disabledColor = Color.gray
func prepareForFirstAnimation() {}
func firstAnimation() {}
func prepareForSecondAnimation() {}
func secondAnimation() {}
private(set) var isAnimating = false
private(set) var isSelected = false
var isEnabled = true {
didSet {
selectedColor = { selectedColor }()
normalColor = { normalColor }()
disabledColor = { disabledColor }()
}
}
override init() {
super.init()
prepare()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
prepare()
}
func prepare() {
normalColor = { normalColor }() // calling didSet
selectedColor = { selectedColor }() // calling didSet
}
func setSelected(_ isSelected: Bool, animated: Bool) {
guard self.isSelected != isSelected, !isAnimating else { return }
self.isSelected = isSelected
if animated {
animate()
} else {
Motion.disable {
prepareForFirstAnimation()
firstAnimation()
prepareForSecondAnimation()
secondAnimation()
}
}
}
private func animate() {
guard !isAnimating else { return }
prepareForFirstAnimation()
Motion.animate(duration: Constants.partialDuration, timingFunction: .easeInOut, animations: {
self.isAnimating = true
self.firstAnimation()
}, completion: {
Motion.disable {
self.prepareForSecondAnimation()
}
Motion.delay(Constants.partialDuration * Constants.delayFactor) {
Motion.animate(duration: Constants.partialDuration, timingFunction: .easeInOut, animations: {
self.secondAnimation()
}, completion: { self.isAnimating = false })
}
})
}
var sideLength: CGFloat { return frame.height }
struct Constants {
static let totalDuration = 0.5
static let delayFactor = 0.33
static let partialDuration = totalDuration / (1.0 + delayFactor + 1.0)
}
}
// MARK: - Helper extension
private extension CGRect {
var center: CGPoint {
get {
return CGPoint(x: minX + width / 2 , y: minY + height / 2)
}
set {
origin = CGPoint(x: newValue.x - width / 2, y: newValue.y - height / 2)
}
}
}
internal extension CALayer {
/// Animates the propery of CALayer from current value to the specified value
/// and does not reset to the initial value after the animation finishes
///
/// - Parameters:
/// - keyPath: Keypath to the animatable property of the layer
/// - to: Final value of the property
/// - dur: Duration of the animation in seconds. Defaults to 0, which results in taking the duration from enclosing CATransaction, or .25 seconds
func animate(_ keyPath: String, to: CGFloat, dur: TimeInterval = 0) {
let animation = CABasicAnimation(keyPath: keyPath)
animation.timingFunction = .easeIn
animation.fromValue = self.value(forKeyPath: keyPath) // from current value
animation.duration = dur
setValue(to, forKeyPath: keyPath)
self.add(animation, forKey: nil)
}
}
internal extension CATransform3D {
static var identity: CATransform3D {
return CATransform3DIdentity
}
}
//
// CheckButton.swift
// Material
//
// Created by Orkhan Alikhanov on 12/22/18.
// Copyright © 2017 CosmicMind. All rights reserved.
//
import UIKit
open class CheckButton: BaseIconLayerButton {
class override var iconLayer: BaseIconLayer { return CheckBoxLayer() }
/// Color of the checkmark (✓)
open var checkmarkColor: UIColor {
get {
return (iconLayer as! CheckBoxLayer).checkmarkColor
}
set {
(iconLayer as! CheckBoxLayer).checkmarkColor = newValue
}
}
open override func prepare() {
super.prepare()
addTarget(self, action: #selector(didTap), for: .touchUpInside)
}
@objc
private func didTap() {
guard !isAnimating else { return }
setSelected(!isSelected, animated: true)
}
}
internal class CheckBoxLayer: BaseIconLayer {
var checkmarkColor: UIColor = .white {
didSet {
checkMarkLeftLayer.strokeColor = checkmarkColor.cgColor
checkMarkRightLayer.strokeColor = checkmarkColor.cgColor
}
}
let borderLayer = CALayer()
let checkMarkLeftLayer = CAShapeLayer()
let checkMarkRightLayer = CAShapeLayer()
let checkMarkLayer = CALayer()
override var selectedColor: UIColor {
didSet {
guard isSelected, isEnabled else { return }
borderLayer.borderColor = selectedColor.cgColor
borderLayer.backgroundColor = selectedColor.cgColor
}
}
override var normalColor: UIColor {
didSet {
guard !isSelected, isEnabled else { return }
borderLayer.borderColor = normalColor.cgColor
}
}
override var disabledColor: UIColor {
didSet {
guard !isEnabled else { return }
borderLayer.borderColor = disabledColor.cgColor
if isSelected { borderLayer.backgroundColor = disabledColor.cgColor }
}
}
open override func prepare() {
super.prepare()
addSublayer(borderLayer)
addSublayer(checkMarkLayer)
checkMarkLayer.addSublayer(checkMarkLeftLayer)
checkMarkLayer.addSublayer(checkMarkRightLayer)
checkMarkLeftLayer.lineCap = kCALineCapSquare
checkMarkRightLayer.lineCap = kCALineCapSquare
checkMarkLeftLayer.strokeEnd = 0
checkMarkRightLayer.strokeEnd = 0
checkmarkColor = { checkmarkColor }() // calling didSet
}
override func prepareForFirstAnimation() {
borderLayer.borderColor = (isEnabled ? (isSelected ? selectedColor : normalColor) : disabledColor).cgColor
if isSelected {
borderLayer.borderWidth = borderLayerNormalBorderWidth
} else {
borderLayer.backgroundColor = (isEnabled ? normalColor : disabledColor).cgColor
checkMarkLeftLayer.strokeEnd = 1
checkMarkRightLayer.strokeEnd = 1
}
checkMarkLayer.transform = .identity
}
override func firstAnimation() {
borderLayer.transform = borderLayerScaleToShrink
checkMarkLayer.transform = borderLayerScaleToShrink
if isSelected {
borderLayer.animate(#keyPath(CALayer.borderWidth), to: borderLayerFullBorderWidth)
} else {
checkMarkLeftLayer.animate(#keyPath(CAShapeLayer.strokeEnd), to: 0)
checkMarkRightLayer.animate(#keyPath(CAShapeLayer.strokeEnd), to: 0)
checkMarkLayer.transform = CATransform3DMakeTranslation(sideLength / 2 - checkMarkStartPoint.x, -(checkMarkStartPoint.y - sideLength / 2), 0)
}
}
override func prepareForSecondAnimation() {
borderLayer.backgroundColor = (isSelected ? (isEnabled ? selectedColor : disabledColor) : .clear).cgColor
if isSelected {
borderLayer.borderWidth = borderLayerNormalBorderWidth
checkMarkLeftLayer.strokeEnd = 0.0001
checkMarkRightLayer.strokeEnd = 0.0001
checkMarkLayer.opacity = 0
checkMarkLayer.animate(#keyPath(CALayer.opacity), to: 1, dur: Constants.totalDuration * 0.1)
} else {
borderLayer.borderWidth = borderLayerCenterDotBorderWidth
}
}
override func secondAnimation() {
borderLayer.transform = .identity
checkMarkLayer.transform = .identity
if isSelected {
checkMarkLeftLayer.animate(#keyPath(CAShapeLayer.strokeEnd), to: 1)
checkMarkRightLayer.animate(#keyPath(CAShapeLayer.strokeEnd), to: 1)
} else {
borderLayer.animate(#keyPath(CALayer.borderWidth), to: borderLayerNormalBorderWidth)
}
}
override func layoutSublayers() {
super.layoutSublayers()
guard !isAnimating else { return }
let s = CGSize(width: sideLength, height: sideLength)
borderLayer.frame.size = s
checkMarkLayer.frame.size = s
checkMarkLeftLayer.frame.size = s
checkMarkRightLayer.frame.size = s
checkMarkLeftLayer.path = checkMarkPathLeft.cgPath
checkMarkRightLayer.path = checkMarkPathRigth.cgPath
checkMarkLeftLayer.lineWidth = lineWidth
checkMarkRightLayer.lineWidth = lineWidth
borderLayer.borderWidth = borderLayerNormalBorderWidth
borderLayer.cornerRadius = borderLayerCornerRadius
}
}
private extension CheckBoxLayer {
var borderLayerFullBorderWidth: CGFloat { return sideLength / 2 * 1.1 } //without multipling 1.1 a weird plus sign (+) appears sometimes.
var borderLayerCenterDotBorderWidth: CGFloat { return sideLength / 2 * 0.87 }
var borderLayerNormalBorderWidth: CGFloat { return sideLength * 0.1 }
var borderLayerCornerRadius: CGFloat { return sideLength * 0.1 }
var borderLayerScalePercentageToShrink: CGFloat { return 0.9 }
var borderLayerScaleToShrink: CATransform3D {
return CATransform3DMakeScale(borderLayerScalePercentageToShrink, borderLayerScalePercentageToShrink, 1)
}
var checkMarkStartPoint: CGPoint {
return CGPoint(x: sideLength * 14 / 36, y: sideLength * 25 / 36)
}
var checkMarkRightEndPoint: CGPoint {
return CGPoint(x: sideLength - (sideLength * 6 / 36), y: sideLength * 9 / 36)
}
var checkMarkLeftEndPoint: CGPoint {
return CGPoint(x: sideLength * 6 / 36, y: sideLength * 18 / 36)
}
var checkMarkPathRigth: UIBezierPath {
let path = UIBezierPath()
path.move(to: checkMarkStartPoint)
path.addLine(to: checkMarkRightEndPoint)
return path
}
var checkMarkPathLeft: UIBezierPath {
let path = UIBezierPath()
path.move(to: checkMarkStartPoint)
path.addLine(to: checkMarkLeftEndPoint)
return path
}
var lineWidth: CGFloat { return sideLength * 0.1 }
}
//
// CheckButtonGroup.swift
// Material
//
// Created by Orkhan Alikhanov on 12/24/17.
// Copyright © 2017 CosmicMind, Inc. All rights reserved.
//
/// Lays out provided check buttons within itself.
///
/// Unlike RadioButtonGroup, checking one check button that belongs to a check group *does not* unchecks any previously checked
/// check button within the same group. Intially, all of the check buttons are unchecked.
///
/// The buttons are laid out by `Grid` system, so that changing properites of grid instance
/// (e.g interimSpace) are reflected.
open class CheckButtonGroup: BaseButtonGroup<CheckButton> {
/// Initializes CheckButtonGroup with an array of check buttons each having
/// title equal to corresponding string in the `titles` parameter.
///
/// - Parameter titles: An array of title strings
public convenience init(titles: [String]) {
let buttons = titles.map { CheckButton(title: $0) }
self.init(buttons: buttons)
}
/// Returns all selected check buttons within the group
/// or empty array if none is seleceted.
open var selecetedButtons: [CheckButton] {
return buttons.filter { $0.isSelected }
}
/// Returns indexes of all selected check buttons within the group
/// or empty array if none is seleceted.
open var selectedIndices: [Int] {
return selecetedButtons.map { buttons.index(of: $0)! }
}
open override func didTap(button: CheckButton, at index: Int) {
button.setSelected(!button.isSelected, animated: true)
}
}
//
// RadioButton.swift
// Material
//
// Created by Orkhan Alikhanov on 12/22/18.
// Copyright © 2017 CosmicMind. All rights reserved.
//
import UIKit
open class RadioButton: BaseIconLayerButton {
class override var iconLayer: BaseIconLayer { return RadioBoxLayer() }
open override func prepare() {
super.prepare()
addTarget(self, action: #selector(didTap), for: .touchUpInside)
}
@objc
private func didTap() {
setSelected(true, animated: true)
}
}
internal class RadioBoxLayer: BaseIconLayer {
private let centerDot = CALayer()
private let outerCircle = CALayer()
override var selectedColor: UIColor {
didSet {
guard isSelected, isEnabled else { return }
outerCircle.borderColor = selectedColor.cgColor
centerDot.backgroundColor = selectedColor.cgColor
}
}
override var normalColor: UIColor {
didSet {
guard !isSelected, isEnabled else { return }
outerCircle.borderColor = normalColor.cgColor
}
}
override var disabledColor: UIColor {
didSet {
guard !isEnabled else { return }
outerCircle.borderColor = disabledColor.cgColor
if isSelected { centerDot.backgroundColor = disabledColor.cgColor }
}
}
override func prepare() {
super.prepare()
addSublayer(centerDot)
addSublayer(outerCircle)
}
override func prepareForFirstAnimation() {
outerCircle.borderColor = (isEnabled ? (isSelected ? selectedColor : normalColor) : disabledColor).cgColor
if !isSelected {
centerDot.backgroundColor = (isEnabled ? normalColor : disabledColor).cgColor
}
outerCircle.borderWidth = outerCircleBorderWidth
}
override func firstAnimation() {
outerCircle.transform = outerCircleScaleToShrink
let to = isSelected ? sideLength / 2.0 : outerCircleBorderWidth * percentageOfOuterCircleWidthToStart
outerCircle.animate(#keyPath(CALayer.borderWidth), to: to)
if !isSelected {
centerDot.transform = centerDotScaleForMeeting
}
}
override func prepareForSecondAnimation() {
centerDot.transform = isSelected ? centerDotScaleForMeeting : .identity
centerDot.backgroundColor = (isSelected ? (isEnabled ? selectedColor : disabledColor) : .clear).cgColor
outerCircle.borderWidth = isSelected ? outerCircleBorderWidth * percentageOfOuterCircleWidthToStart : outerCircleFullBorderWidth
}
override func secondAnimation() {
outerCircle.transform = .identity
outerCircle.animate(#keyPath(CALayer.borderWidth), to: outerCircleBorderWidth)
if isSelected {
centerDot.transform = .identity
}
}
override func layoutSublayers() {
super.layoutSublayers()
guard !isAnimating else { return }
centerDot.frame = CGRect(x: centerDotDiameter / 2.0, y: centerDotDiameter / 2.0, width: centerDotDiameter, height: centerDotDiameter)
outerCircle.frame.size = CGSize(width: sideLength, height: sideLength)
centerDot.cornerRadius = centerDot.bounds.width / 2
outerCircle.cornerRadius = sideLength / 2
outerCircle.borderWidth = outerCircleBorderWidth
}
}
private extension RadioBoxLayer {
var percentageOfOuterCircleSizeToShrinkTo: CGFloat { return 0.9 }
var percentageOfOuterCircleWidthToStart: CGFloat { return 1 }
var outerCircleScaleToShrink: CATransform3D {
let s = percentageOfOuterCircleSizeToShrinkTo
return CATransform3DMakeScale(s, s, 1)
}
var centerDotScaleForMeeting: CATransform3D {
let s = ((sideLength - 2 * percentageOfOuterCircleWidthToStart * outerCircleBorderWidth) * percentageOfOuterCircleSizeToShrinkTo) / centerDotDiameter
return CATransform3DMakeScale(s, s, 1)
}
var outerCircleFullBorderWidth: CGFloat {
return (self.sideLength / 2.0) * 1.1 //without multipling 1.1 a weird plus sign (+) appears sometimes.
}
var centerDotDiameter: CGFloat { return sideLength / 2.0 }
var outerCircleBorderWidth: CGFloat { return sideLength * 0.11 }
}
//
// RadioButtonGroup.swift
// Material
//
// Created by Orkhan Alikhanov on 12/24/17.
// Copyright © 2017 CosmicMind, Inc. All rights reserved.
//
/// Lays out provided radio buttons within itself.
///
/// Checking one radio button that belongs to a radio group unchecks any previously checked
/// radio button within the same group. Intially, all of the radio buttons are unchecked.
///
/// The buttons are laid out by `Grid` system, so that changing properites of grid instance
/// (e.g interimSpace) are reflected.
open class RadioButtonGroup: BaseButtonGroup<RadioButton> {
/// Initializes RadioButtonGroup with an array of radio buttons each having
/// title equal to corresponding string in the `titles` parameter.
///
/// - Parameter titles: An array of title strings.
public convenience init(titles: [String]) {
let buttons = titles.map { RadioButton(title: $0) }
self.init(buttons: buttons)
}
/// Returns selected radio button within the group.
/// If none is selected (e.g in initial state), nil is returned.
open var selectedButton: RadioButton? {
return buttons.first { $0.isSelected }
}
/// Returns index of selected radio button within the group.
/// If none is selected (e.g in initial state), -1 is returned.
open var selectedIndex: Int {
guard let b = selectedButton else { return -1 }
return buttons.index(of: b)!
}
open override func didTap(button: RadioButton, at index: Int) {
let isAnimating = buttons.reduce(false) { $0 || $1.isAnimating }
guard !isAnimating else { return }
buttons.forEach { $0.setSelected($0 == button, animated: true) }
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment