This repository has been archived by the owner. It is now read-only.
Permalink
Cannot retrieve contributors at this time
executable file
137 lines (115 sloc)
4.58 KB
| // FilledPageControl | |
| // | |
| // Copyright (c) 2016 Kyle Zaragoza <popwarsweet@gmail.com> | |
| // MIT License | |
| import UIKit | |
| @IBDesignable open class FilledPageControl: UIView { | |
| // MARK: - PageControl | |
| @IBInspectable open var pageCount: Int = 0 { | |
| didSet { | |
| updateNumberOfPages(pageCount) | |
| } | |
| } | |
| @IBInspectable open var progress: CGFloat = 0 { | |
| didSet { | |
| updateActivePageIndicatorMasks(progress) | |
| } | |
| } | |
| open var currentPage: Int { | |
| return Int(round(progress)) | |
| } | |
| // MARK: - Appearance | |
| override open var tintColor: UIColor! { | |
| didSet { | |
| inactiveLayers.forEach() { $0.backgroundColor = tintColor.cgColor } | |
| } | |
| } | |
| @IBInspectable open var inactiveRingWidth: CGFloat = 1 { | |
| didSet { | |
| updateActivePageIndicatorMasks(progress) | |
| } | |
| } | |
| @IBInspectable open var indicatorPadding: CGFloat = 10 { | |
| didSet { | |
| layoutPageIndicators(inactiveLayers) | |
| } | |
| } | |
| @IBInspectable open var indicatorRadius: CGFloat = 5 { | |
| didSet { | |
| layoutPageIndicators(inactiveLayers) | |
| } | |
| } | |
| fileprivate var indicatorDiameter: CGFloat { | |
| return indicatorRadius * 2 | |
| } | |
| fileprivate var inactiveLayers = [CALayer]() | |
| // MARK: - State Update | |
| fileprivate func updateNumberOfPages(_ count: Int) { | |
| // no need to update | |
| guard count != inactiveLayers.count else { return } | |
| // reset current layout | |
| inactiveLayers.forEach() { $0.removeFromSuperlayer() } | |
| inactiveLayers = [CALayer]() | |
| // add layers for new page count | |
| inactiveLayers = stride(from: 0, to:count, by:1).map() { _ in | |
| let layer = CALayer() | |
| layer.backgroundColor = self.tintColor.cgColor | |
| self.layer.addSublayer(layer) | |
| return layer | |
| } | |
| layoutPageIndicators(inactiveLayers) | |
| updateActivePageIndicatorMasks(progress) | |
| self.invalidateIntrinsicContentSize() | |
| } | |
| // MARK: - Layout | |
| fileprivate func updateActivePageIndicatorMasks(_ progress: CGFloat) { | |
| // ignore if progress is outside of page indicators' bounds | |
| guard progress >= 0 && progress <= CGFloat(pageCount - 1) else { return } | |
| // mask rect w/ default stroke width | |
| let insetRect = CGRect(width: indicatorDiameter, height: indicatorDiameter).insetBy(dx: inactiveRingWidth, dy: inactiveRingWidth) | |
| let leftPageFloat = trunc(progress) | |
| let leftPageInt = Int(progress) | |
| // inset right moving page indicator | |
| let spaceToMove = insetRect.width / 2 | |
| let percentPastLeftIndicator = progress - leftPageFloat | |
| let additionalSpaceToInsetRight = spaceToMove * percentPastLeftIndicator | |
| let closestRightInsetRect = insetRect.insetBy(dx: additionalSpaceToInsetRight, dy: additionalSpaceToInsetRight) | |
| // inset left moving page indicator | |
| let additionalSpaceToInsetLeft = (1 - percentPastLeftIndicator) * spaceToMove | |
| let closestLeftInsetRect = insetRect.insetBy(dx: additionalSpaceToInsetLeft, dy: additionalSpaceToInsetLeft) | |
| // adjust masks | |
| for (idx, layer) in inactiveLayers.enumerated() { | |
| let maskLayer = CAShapeLayer() | |
| maskLayer.fillRule = kCAFillRuleEvenOdd | |
| let boundsPath = UIBezierPath(rect: layer.bounds) | |
| let circlePath: UIBezierPath | |
| if leftPageInt == idx { | |
| circlePath = UIBezierPath(ovalIn: closestLeftInsetRect) | |
| } else if leftPageInt + 1 == idx { | |
| circlePath = UIBezierPath(ovalIn: closestRightInsetRect) | |
| } else { | |
| circlePath = UIBezierPath(ovalIn: insetRect) | |
| } | |
| boundsPath.append(circlePath) | |
| maskLayer.path = boundsPath.cgPath | |
| layer.mask = maskLayer | |
| } | |
| } | |
| fileprivate func layoutPageIndicators(_ layers: [CALayer]) { | |
| let layerDiameter = indicatorRadius * 2 | |
| var layerFrame = CGRect(width: layerDiameter, height: layerDiameter) | |
| layers.forEach() { layer in | |
| layer.cornerRadius = self.indicatorRadius | |
| layer.frame = layerFrame | |
| layerFrame.origin.x += layerDiameter + indicatorPadding | |
| } | |
| } | |
| override open var intrinsicContentSize: CGSize { | |
| return sizeThatFits(.zero) | |
| } | |
| override open func sizeThatFits(_ size: CGSize) -> CGSize { | |
| let layerDiameter = indicatorRadius * 2 | |
| return CGSize(width: CGFloat(inactiveLayers.count) * layerDiameter + CGFloat(inactiveLayers.count - 1) * indicatorPadding, | |
| height: layerDiameter) | |
| } | |
| } |