Commit 6f08ea16 by Orkhan Alikhanov

Added RadioButton and CheckButton

parent 88afd83a
...@@ -174,6 +174,9 @@ ...@@ -174,6 +174,9 @@
96E3C39A1D3A1CC20086A024 /* ErrorTextField.swift in Headers */ = {isa = PBXBuildFile; fileRef = 961F18E71CD93E3E008927C5 /* ErrorTextField.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 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, ); }; }; 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, ); }; }; 96F1A5531F24F17A001D8CAF /* TabsController.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96E09DC71F2287E50000B121 /* TabsController.swift */; settings = {ATTRIBUTES = (Public, ); }; };
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 */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
...@@ -285,6 +288,9 @@ ...@@ -285,6 +288,9 @@
96E09DC71F2287E50000B121 /* TabsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabsController.swift; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 96F1DC871D654FDF0025F925 /* Material+CALayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Material+CALayer.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 */ /* End PBXFileReference section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
...@@ -606,6 +612,9 @@ ...@@ -606,6 +612,9 @@
96BCB7601CB40DC500C806FE /* FlatButton.swift */, 96BCB7601CB40DC500C806FE /* FlatButton.swift */,
96BCB7931CB40DC500C806FE /* RaisedButton.swift */, 96BCB7931CB40DC500C806FE /* RaisedButton.swift */,
9658F2161CD6FA4700B902C1 /* IconButton.swift */, 9658F2161CD6FA4700B902C1 /* IconButton.swift */,
9DF352411FED20C900B2A11B /* BaseIconLayerButton.swift */,
9DF352431FED20ED00B2A11B /* RadioButton.swift */,
9DF352451FED210000B2A11B /* CheckButton.swift */,
); );
name = Button; name = Button;
sourceTree = "<group>"; sourceTree = "<group>";
...@@ -916,6 +925,7 @@ ...@@ -916,6 +925,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
961E6BE21DDA2AF3004E6C93 /* Screen.swift in Sources */, 961E6BE21DDA2AF3004E6C93 /* Screen.swift in Sources */,
9DF352441FED20ED00B2A11B /* RadioButton.swift in Sources */,
965E81261DD4D7C800D61E4B /* Material+NSMutableAttributedString.swift in Sources */, 965E81261DD4D7C800D61E4B /* Material+NSMutableAttributedString.swift in Sources */,
965E80FF1DD4D5C800D61E4B /* BottomNavigationController.swift in Sources */, 965E80FF1DD4D5C800D61E4B /* BottomNavigationController.swift in Sources */,
965E81031DD4D5C800D61E4B /* CollectionView.swift in Sources */, 965E81031DD4D5C800D61E4B /* CollectionView.swift in Sources */,
...@@ -923,6 +933,7 @@ ...@@ -923,6 +933,7 @@
965E81071DD4D5C800D61E4B /* CollectionViewLayout.swift in Sources */, 965E81071DD4D5C800D61E4B /* CollectionViewLayout.swift in Sources */,
965E81081DD4D5C800D61E4B /* CollectionReusableView.swift in Sources */, 965E81081DD4D5C800D61E4B /* CollectionReusableView.swift in Sources */,
965E81091DD4D5C800D61E4B /* DataSourceItem.swift in Sources */, 965E81091DD4D5C800D61E4B /* DataSourceItem.swift in Sources */,
9DF352461FED210000B2A11B /* CheckButton.swift in Sources */,
965E810A1DD4D5C800D61E4B /* Font.swift in Sources */, 965E810A1DD4D5C800D61E4B /* Font.swift in Sources */,
965E810B1DD4D5C800D61E4B /* RobotoFont.swift in Sources */, 965E810B1DD4D5C800D61E4B /* RobotoFont.swift in Sources */,
965E810C1DD4D5C800D61E4B /* DynamicFontType.swift in Sources */, 965E810C1DD4D5C800D61E4B /* DynamicFontType.swift in Sources */,
...@@ -955,6 +966,7 @@ ...@@ -955,6 +966,7 @@
965E80ED1DD4C55200D61E4B /* Material+UIWindow.swift in Sources */, 965E80ED1DD4C55200D61E4B /* Material+UIWindow.swift in Sources */,
961527B91F3A509900E8B2AC /* ChipBarController.swift in Sources */, 961527B91F3A509900E8B2AC /* ChipBarController.swift in Sources */,
965E80E41DD4C53300D61E4B /* PulseView.swift in Sources */, 965E80E41DD4C53300D61E4B /* PulseView.swift in Sources */,
9DF352421FED20C900B2A11B /* BaseIconLayerButton.swift in Sources */,
966C17731F0439F600D3E83C /* Material+MotionAnimation.swift in Sources */, 966C17731F0439F600D3E83C /* Material+MotionAnimation.swift in Sources */,
965E80E51DD4C53300D61E4B /* PulseAnimation.swift in Sources */, 965E80E51DD4C53300D61E4B /* PulseAnimation.swift in Sources */,
965E80FE1DD4D59500D61E4B /* ToolbarController.swift in Sources */, 965E80FE1DD4D59500D61E4B /* ToolbarController.swift in Sources */,
......
//
// 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 = { return type(of: self).iconLayer }()
open override var isSelected: Bool {
didSet {
iconLayer.setSelected(isSelected, animated: false)
}
}
open var normalIconColor: UIColor {
get {
return iconLayer.normalColor
}
set {
iconLayer.normalColor = newValue
}
}
open var selectedIconColor: UIColor {
get {
return iconLayer.selectedColor
}
set {
iconLayer.selectedColor = newValue
}
}
open var isAnimating: Bool { return iconLayer.isAnimating }
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)
// we push the title to the right to make room for iconLayer
// `contentEdgeInsets` is used to let default implementation of
// `intrinsicContentSize` consider our titleSpacing
let titleSpacing = (margin + iconSize + margin) / 2
titleEdgeInsets = UIEdgeInsets(top: 0, left: titleSpacing, bottom: 0, right: -titleSpacing)
contentEdgeInsets = UIEdgeInsets(top: margin, left: titleSpacing, bottom: margin, right: titleSpacing)
}
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
iconLayer.frame.size = CGSize(width: iconSize, height: iconSize)
iconLayer.frame.origin.y = bounds.height / 2 - iconSize / 2
iconLayer.frame.origin.x = margin
// 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 s = margin + iconSize + margin // considering margin as well
visualLayer.bounds.size = CGSize(width: s, height: s)
visualLayer.frame.center = iconLayer.frame.center
visualLayer.cornerRadius = s / 2
}
private let margin: CGFloat = 5
private let iconSize: CGFloat = 16
}
// MARK: - BaseIconLayer
internal class BaseIconLayer: CALayer {
var selectedColor = Color.blue.base
var normalColor = Color.lightGray
func prepareForFirstAnimation() {}
func firstAnimation() {}
func prepareForSecondAnimation() {}
func secondAnimation() {}
var isAnimating = false
var isSelected = false
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: partialDuration, timingFunction: .easeInOut, animations: {
self.isAnimating = true
self.firstAnimation()
}, completion: {
Motion.disable {
self.prepareForSecondAnimation()
}
Motion.delay(self.partialDuration * self.delayFactor) {
Motion.animate(duration: self.partialDuration, timingFunction: .easeInOut, animations: {
self.secondAnimation()
}, completion: { self.isAnimating = false })
}
})
}
var sideLength: CGFloat { return frame.height }
let totalDuration = 0.5
private let delayFactor = 0.33
private var partialDuration: TimeInterval { return 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(forKey: 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() }
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
let borderLayer = CALayer()
let checkMarkLeftLayer = CAShapeLayer()
let checkMarkRightLayer = CAShapeLayer()
let checkMarkLayer = CALayer()
override var selectedColor: UIColor {
didSet {
guard isSelected else { return }
borderLayer.borderColor = selectedColor.cgColor
}
}
override var normalColor: UIColor {
didSet {
guard !isSelected else { return }
borderLayer.borderColor = normalColor.cgColor
}
}
open override func prepare() {
super.prepare()
addSublayer(borderLayer)
addSublayer(checkMarkLayer)
checkMarkLayer.addSublayer(checkMarkLeftLayer)
checkMarkLayer.addSublayer(checkMarkRightLayer)
checkMarkLeftLayer.lineCap = kCALineCapSquare
checkMarkRightLayer.lineCap = kCALineCapSquare
}
override func prepareForFirstAnimation() {
borderLayer.borderColor = (isSelected ? selectedColor : normalColor).cgColor
if isSelected {
borderLayer.borderWidth = borderLayerNormalBorderWidth
} else {
borderLayer.backgroundColor = normalColor.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 ? selectedColor : .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: totalDuration * 0.1)
checkMarkLeftLayer.strokeColor = checkmarkColor.cgColor
checkMarkRightLayer.strokeColor = checkmarkColor.cgColor
checkMarkLeftLayer.path = checkMarkPathLeft.cgPath
checkMarkRightLayer.path = checkMarkPathRigth.cgPath
checkMarkLeftLayer.lineWidth = lineWidth
checkMarkRightLayer.lineWidth = lineWidth
checkMarkLeftLayer.strokeEnd = 0
checkMarkRightLayer.strokeEnd = 0
} 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 }
borderLayer.frame.size = CGSize(width: sideLength, height: sideLength)
checkMarkLayer.frame.size = borderLayer.frame.size
checkMarkLeftLayer.frame.size = borderLayer.frame.size
checkMarkRightLayer.frame.size = borderLayer.frame.size
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 }
}
//
// 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 else { return }
outerCircle.borderColor = selectedColor.cgColor
centerDot.backgroundColor = selectedColor.cgColor
}
}
override var normalColor: UIColor {
didSet {
if !isSelected {
outerCircle.borderColor = normalColor.cgColor
}
}
}
override func prepare() {
super.prepare()
addSublayer(centerDot)
addSublayer(outerCircle)
}
override func prepareForFirstAnimation() {
outerCircle.borderColor = (isSelected ? selectedColor : normalColor).cgColor
if !isSelected {
centerDot.backgroundColor = normalColor.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 ? selectedColor : .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 }
}
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