swift 中的 UIView 水平条动画

UIView horizontal bar animation in swift

我正在制作这个动画,其中每秒都会收到一个数字,并且进度条必须根据双精度值填充或下降。

我已经创建了视图并将所有视图添加到 UIStackView 中。还为所有视图制作了 outlet collection。 (按标签对它们进行排序并使它们呈圆形)。

我可以循环查看视图并更改它们的背景颜色,但我想看看是否有更好的方法来做到这一点。有什么建议吗?

谢谢

我坚信通过解决有机问题来学习,并慢慢建立我对某个主题的全球知识。所以恐怕我没有什么好的教程给你。

不过,这是一个可以让您快速入门的示例。

// For participating in Simulator's "slow animations"
@_silgen_name("UIAnimationDragCoefficient") func UIAnimationDragCoefficient() -> Float

import UIKit

@IBDesignable
class VerticalProgessView: UIControl {

    @IBInspectable
    var numberOfSegments: UInt = 0

    @IBInspectable
    var verticalSegmentGap: CGFloat = 4.0

    @IBInspectable
    var outerColor: UIColor = UIColor(red: 33, green: 133, blue: 109)

    @IBInspectable
    var unfilledColor: UIColor = UIColor(red: 61, green: 202, blue: 169)

    @IBInspectable
    var filledColor: UIColor = UIColor.white

    private var _progress: Float = 0.25
    @IBInspectable
    open var progress: Float {
        get {
            return _progress
        }
        set {
            self.setProgress(newValue, animated: false)
        }
    }

    let progressLayer = CALayer()

    let maskLayer = CAShapeLayer()

    var skipLayoutSubviews = false

    open func setProgress(_ progress: Float, animated: Bool) {

        if progress < 0 {
            _progress = 0
        } else if progress > 1.0 {
            _progress = 1
        } else {
            // Clamp the percentage to discreet values
            let discreetPercentageDistance: Float = 1.0 / 28.0
            let nearestProgress = discreetPercentageDistance * round(progress/discreetPercentageDistance)

            _progress = nearestProgress
        }

        CATransaction.begin()
        CATransaction.setCompletionBlock { [weak self] in
            self?.skipLayoutSubviews = false
        }
        if !animated {
            CATransaction.setDisableActions(true)
        } else {
            CATransaction.setAnimationDuration(0.25 * Double(UIAnimationDragCoefficient()))
        }

        let properties = progressLayerProperties()
        progressLayer.bounds = properties.bounds
        progressLayer.position = properties.position

        skipLayoutSubviews = true
        CATransaction.commit() // This triggers layoutSubviews
    }

    override func prepareForInterfaceBuilder() {
        awakeFromNib()
    }

    override func awakeFromNib() {
        super.awakeFromNib()

        self.backgroundColor = UIColor.clear

        self.layer.backgroundColor = unfilledColor.cgColor

        // Initialize and add the progressLayer
        let properties = progressLayerProperties()
        progressLayer.bounds = properties.bounds
        progressLayer.position = properties.position
        progressLayer.backgroundColor = filledColor.cgColor
        self.layer.addSublayer(progressLayer)

        // Initialize and add the maskLayer (it has the holes)
        maskLayer.frame = self.layer.bounds
        maskLayer.fillColor = outerColor.cgColor
        maskLayer.fillRule = kCAFillRuleEvenOdd
        maskLayer.path = maskPath(for: maskLayer.bounds)
        self.layer.addSublayer(maskLayer)

        // Layer hierarchy

        // self.maskLayer
        // self.progressLayer
        // self.layer
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        if skipLayoutSubviews {
            // Crude but effective, not fool proof though
            skipLayoutSubviews = false
            return
        }

        let timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
        // Doesn't work for 180° rotations
        let duration = UIApplication.shared.statusBarOrientationAnimationDuration * Double(UIAnimationDragCoefficient())

        CATransaction.begin()
        CATransaction.setAnimationTimingFunction(timingFunction)
        CATransaction.setAnimationDuration(duration)

        let properties = progressLayerProperties()
        progressLayer.bounds = properties.bounds
        progressLayer.position = properties.position

        let size = self.bounds.size

        let anchorPoint = CGPoint(x: 0.5, y: 1.0)
        maskLayer.anchorPoint = anchorPoint
        maskLayer.bounds = CGRect(origin: CGPoint.zero, size: size)
        maskLayer.position = CGPoint(x: size.width * anchorPoint.x, y: size.height * anchorPoint.y)

        // Animate the segments

        let pathChangeAnimation = CAKeyframeAnimation(keyPath: "path")
        let finalPath = maskPath(for: maskLayer.bounds)

        pathChangeAnimation.values = [maskLayer.path!, finalPath]
        pathChangeAnimation.keyTimes = [0, 1]
        pathChangeAnimation.timingFunction = timingFunction
        pathChangeAnimation.duration = duration
        pathChangeAnimation.isRemovedOnCompletion = true
        maskLayer.add(pathChangeAnimation, forKey: "pathAnimation")

        CATransaction.setCompletionBlock {
            // CAAnimation's don't actually change the value
            self.maskLayer.path = finalPath
        }

        CATransaction.commit()
    }

    // Provides a path that will mask out all the holes to show self.layer and the progressLayer behind
    private func maskPath(for rect: CGRect) -> CGPath {

        let horizontalSegmentGap: CGFloat = 5.0

        let segmentWidth = rect.width - horizontalSegmentGap * 2
        let segmentHeight = rect.height/CGFloat(numberOfSegments) - verticalSegmentGap + verticalSegmentGap/CGFloat(numberOfSegments)

        let segmentSize = CGSize(width: segmentWidth, height: segmentHeight)
        let segmentRect = CGRect(x: horizontalSegmentGap, y: 0, width: segmentSize.width, height: segmentSize.height)

        let path = CGMutablePath()
        for i in 0..<numberOfSegments {

            // Literally, just move it down by the y value here
            // this simplifies the math of calculating the starting points and what not
            let transform = CGAffineTransform.identity.translatedBy(x: 0, y: (segmentSize.height + verticalSegmentGap) * CGFloat(i))

            let segmentPath = UIBezierPath(roundedRect: segmentRect, cornerRadius: segmentSize.height / 2)
            segmentPath.apply(transform)

            path.addPath(segmentPath.cgPath)
        }

        // Without the outerPath, we'll end up with a bunch of squircles instead of a bunch of holes
        let outerPath = CGPath(rect: rect, transform: nil)

        path.addPath(outerPath)

        return path
    }

    /// Provides the current and correct bounds and position for the progressLayer
    private func progressLayerProperties() -> (bounds: CGRect, position: CGPoint) {
        let frame = self.bounds
        let height = frame.height * CGFloat(progress)
        let y = frame.height * CGFloat(1 - progress)
        let width = frame.width

        let anchorPoint = maskLayer.anchorPoint

        let bounds = CGRect(x: 0, y: 0, width: width, height: height)
        let position = CGPoint(x: 0 + width * anchorPoint.x, y: y + height * anchorPoint.x)

        return (bounds: bounds, position: position)
    }

    // TODO: Implement functions to further mimic UIProgressView
}

extension UIColor {
    convenience init(red: Int, green: Int, blue: Int) {
        self.init(red: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue) / 255.0, alpha: 1)
    }
}

在故事板中使用

享受魔法

所以你的做法很好。这里有两种不同的方式。第一个是 Core Graphics。您可能想要更新方法,甚至在子图层中制作颜色渐变。

第一种方式

import UIKit

class Indicator: UIView {

    var padding : CGFloat = 5.0
    var minimumSpace : CGFloat = 4.0
    var indicators : CGFloat = 40
    var indicatorColor : UIColor = UIColor.lightGray
    var filledIndicatorColor = UIColor.blue

    var currentProgress = 0.0
    var radiusFactor : CGFloat = 0.25
    var fillReversed = false

    override init(frame: CGRect) {
        super.init(frame: frame)
        setUp(animated: false)

    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setUp(animated: false)
        backgroundColor = UIColor.green
    }

    func updateProgress(progress:Double, animated:Bool) {
        currentProgress = progress
        setUp(animated: animated)
    }

    private func setUp(animated:Bool){

        // internal space
        let neededPadding = (indicators - 1) * minimumSpace
        //calculate height and width minus padding and space since vertical
        let height = (bounds.height - neededPadding - (padding * 2.0)) / indicators
        let width = bounds.width - padding * 2.0

        if animated == true{
            let trans = CATransition()
            trans.type = kCATransitionFade
            trans.duration = 0.5
            self.layer.add(trans, forKey: nil)
        }
        layer.sublayers?.removeAll()
        for i in 0...Int(indicators - 1.0){
            let indicatorLayer = CALayer()
            indicatorLayer.frame = CGRect(x: padding, y: CGFloat(i) * height + padding + (minimumSpace * CGFloat(i)), width: width, height: height)
            //haha i don't understand my logic below but it works hahaha
            // i know it has to go backwards
            if fillReversed{
                if CGFloat(1 - currentProgress) * self.bounds.height < indicatorLayer.frame.origin.y{
                    indicatorLayer.backgroundColor = filledIndicatorColor.cgColor
                }else{
                    indicatorLayer.backgroundColor = indicatorColor.cgColor
                }
            }else{
                if CGFloat(currentProgress) * self.bounds.height > indicatorLayer.frame.origin.y{
                     indicatorLayer.backgroundColor = indicatorColor.cgColor
                }else{
                    indicatorLayer.backgroundColor = filledIndicatorColor.cgColor

                }
            }

            indicatorLayer.cornerRadius = indicatorLayer.frame.height * radiusFactor
            indicatorLayer.masksToBounds = true
            self.layer.addSublayer(indicatorLayer)
        }
    }

    //handle rotation
    override func layoutSubviews() {
        super.layoutSubviews()
        setUp(animated: false)
    }
}

第二种方式是使用CAShapeLayer,好处是进度会得到一个自然的动画。

    import UIKit

class Indicator: UIView {

    var padding : CGFloat = 5.0
    var minimumSpace : CGFloat = 4.0
    var indicators : CGFloat = 40
    var indicatorColor : UIColor = UIColor.lightGray
    var filledIndicatorColor = UIColor.blue

    var currentProgress = 0.0
    var radiusFactor : CGFloat = 0.25
    private var progressLayer : CALayer?
    private var shapeHoles : CAShapeLayer?

    override init(frame: CGRect) {
        super.init(frame: frame)
        transparentDotsAndProgress()

    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        transparentDotsAndProgress()
    }

    func updateProgress(progress:Double) {
        if progress <= 1 && progress >= 0{
            currentProgress = progress
            transparentDotsAndProgress()
        }
    }

    //handle rotation
    override func layoutSubviews() {
        super.layoutSubviews()
        transparentDotsAndProgress()
    }

    func transparentDotsAndProgress(){
        self.layer.masksToBounds = true

        let neededPadding = (indicators - 1) * minimumSpace
        //calculate height and width minus padding and space since vertical
        let height = (bounds.height - neededPadding - (padding * 2.0)) / indicators
        let width = bounds.width - padding * 2.0

        let path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: self.bounds.width, height: self.bounds.height), cornerRadius: 0)
        path.usesEvenOddFillRule = true
        for i in 0...Int(indicators - 1.0){
            let circlePath = UIBezierPath(roundedRect: CGRect(x: padding, y: CGFloat(i) * height + padding + (minimumSpace * CGFloat(i)), width: width, height: height), cornerRadius: height * radiusFactor)
            path.append(circlePath)
        }


        if progressLayer == nil{
            progressLayer = CALayer()
            progressLayer?.backgroundColor = filledIndicatorColor.cgColor
            self.layer.addSublayer(progressLayer!)

        }

        progressLayer?.frame =  CGRect(x: 0, y: -self.bounds.height - padding + CGFloat(currentProgress) * self.bounds.height, width: bounds.width, height: bounds.height)


        self.shapeHoles?.removeFromSuperlayer()
        shapeHoles = CAShapeLayer()
        shapeHoles?.path = path.cgPath
        shapeHoles?.fillRule = kCAFillRuleEvenOdd
        shapeHoles?.fillColor = UIColor.white.cgColor
        shapeHoles?.strokeColor = UIColor.clear.cgColor
        self.layer.backgroundColor = indicatorColor.cgColor
        self.layer.addSublayer(shapeHoles!)


    }

}

这两种方法都应该有效,但 CAShapeLayer 的优点是您可以获得自然的动画。