【问题标题】:How to correctly calculate speed in gradient fill animation and rotation如何正确计算渐变填充动画和旋转中的速度
【发布时间】:2020-06-09 08:27:24
【问题描述】:

我有一个带有图层的视图:虚线半圆、围绕半圆移动的圆以及虚线半圆上的渐变填充蒙版。我无法正确计算两个动画中的动画持续时间。目前,我的圆圈移动速度比半圆形填充中的渐变快。

这是我的代码:

class DashedCircleView: UIView {

    var circleLayer = CAShapeLayer()
    var circleGradientLayer = CAGradientLayer()
    var movingCircleLayer = CAShapeLayer()

    override init(frame: CGRect = .zero) {
        super.init(frame: frame)
        createDashedSemiCircle()
        fillDashedSemiCircleWithGradient()
        createMovingCircle()
        animateGradient()
        rotateMovingCircle()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        createDashedSemiCircle()
        fillDashedSemiCircleWithGradient()
        createMovingCircle()
        animateGradient()
        rotateMovingCircle()
    }

private extension DashedCircleView {

    func createDashedSemiCircle() {
        backgroundColor = .clear
        circleLayer.path = UIBezierPath(ovalIn: bounds).cgPath
        circleLayer.lineWidth = 2.0
        circleLayer.masksToBounds = false
        circleLayer.strokeColor =  UIColor.darkGray.cgColor //border of circle
        circleLayer.fillColor = UIColor.clear.cgColor //inside the circle
        circleLayer.lineJoin = kCALineJoinRound
        circleLayer.lineDashPattern = [10, 10]
        layer.addSublayer(circleLayer)

        circleLayer.path = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2, y: frame.size.height / 2),
        radius: min(frame.size.height, frame.size.width) / 2,
        startAngle: 0,
        endAngle: .pi,
        clockwise: false).cgPath
    }

    func createMovingCircle() {

        let rect = CGRect(x: 0,
                          y: 0,
                          width: 20,
                          height: 20)
        let path = UIBezierPath(ovalIn: rect)
        movingCircleLayer.fillColor = UIColor.blue.cgColor
        movingCircleLayer.path = path.cgPath
        movingCircleLayer.bounds = rect
        layer.addSublayer(movingCircleLayer)
    }

    func rotateMovingCircle() {

        var affineTransform = CGAffineTransform(rotationAngle: 0.0)
        affineTransform = affineTransform.rotated(by: CGFloat(Double.pi))

        let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2, y: frame.size.height / 2),
        radius: min(frame.size.height, frame.size.width) / 2,
        startAngle: .pi,
        endAngle: 3 * .pi / 2,
        clockwise: true).cgPath

        let orbitRotateAnimation = CAKeyframeAnimation(keyPath: "position")
        orbitRotateAnimation.path = circlePath
        orbitRotateAnimation.duration = 4
        orbitRotateAnimation.isAdditive = true
        orbitRotateAnimation.repeatCount = 0
        orbitRotateAnimation.calculationMode = kCAAnimationLinear
        orbitRotateAnimation.rotationMode = kCAAnimationRotateAuto
        orbitRotateAnimation.fillMode = kCAFillModeForwards
        orbitRotateAnimation.isRemovedOnCompletion = false

        movingCircleLayer.add(orbitRotateAnimation, forKey: nil)
    }

    func fillDashedSemiCircleWithGradient() {
        layer.addSublayer(circleGradientLayer)
        circleGradientLayer.mask = circleLayer
        circleGradientLayer.frame = CGRect.init(origin: self.bounds.origin, size: self.frame.size)
        circleGradientLayer.borderColor = UIColor.clear.cgColor
        circleGradientLayer.borderWidth = 1
        circleGradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
        circleGradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5) // horizontal gradient
        circleGradientLayer.locations = [0.0, 0.0] //max 0.88
        circleGradientLayer.bounds = bounds
        circleGradientLayer.colors = [
            #colorLiteral(red: 0.9372549057, green: 0.3490196168, blue: 0.1921568662, alpha: 1).cgColor,
            UIColor.init(red: 211/255, green: 211/255, blue: 211/255, alpha: 1.0).cgColor
        ]
    }

    func animateGradient() {

        _ = 7
        let endValue: Double = 23
        let curValue: Double = 12
        let toValue = (curValue * 0.88) / endValue

        let colorsAnimation = CABasicAnimation(keyPath: #keyPath(CAGradientLayer.locations))
        colorsAnimation.fillMode = kCAFillModeForwards
        colorsAnimation.fromValue = [0.0, 0.0]
        colorsAnimation.toValue = [toValue, toValue]
        colorsAnimation.duration = 4.2
        colorsAnimation.repeatCount = 0
        colorsAnimation.autoreverses = false
        colorsAnimation.isRemovedOnCompletion = false
        circleGradientLayer.add(colorsAnimation, forKey: nil)
    }
}

extension Double {
    func rounded(toPlaces places:Int) -> Double {
        let divisor = pow(10.0, Double(places))
        return (self * divisor).rounded() / divisor
    }
}

一开始梯度不动,然后急剧加速,中途减速。

【问题讨论】:

  • 不是渐变“加速和减速” ...问题是第一个“10-pt dash”几乎是垂直的,最后一个“10- pt dash”几乎是水平的。如果您注释掉这一行://circleGradientLayer.mask = circleLayer,您会清楚地看到它。我认为,要获得您想要的效果,您需要以与蓝色圆圈相同的速度旋转渐变层。在旋转下面的渐变层时让蒙版“保持不动”有点棘手......您可能需要使用几个相互叠加的视图。
  • 您还在寻找解决方案吗?
  • @DonMag no,如何关闭话题?
  • 作为一般规则...如果您已经解决了您的问题,其他用户可以将您的解决方案发布为您自己问题的答案。如果您意识到这是由于错误,请继续删除它。如果您“放弃并继续前进”,则可以保持不变……将来有人可能会遇到它并有解决方案(这将使其他用户受益)。

标签: ios swift core-graphics cashapelayer cabasicanimation


【解决方案1】:

两个问题……

首先,这并非不可能,但很难同步两种不同类型的动画。

其次,您看到的渐变加速/减速不是由于动画速度的实际变化,而是由于您尝试填充的虚线曲线不是线性的。

一种可能的解决方案是覆盖几个子视图...

  • 顶部带有蓝色“点”层的视图
  • 下面有渐变层的视图
  • 同步旋转图层

但是,我们不能直接屏蔽渐变层(用虚线半圆),因为如果我们这样做,那么虚线圆将随着渐变旋转。相反,我们可以用“虚线圆视图”来掩盖“渐变视图”。

结局是这样的:

这是动画效果 - 顶部变成 45 度,中间变成 90 度,底部旋转到 180 度(实际持续时间设置为 2 秒...查看这些 gif 图像会出现不同)。

使用虚线半圆作为子视图:

使用 tased-semi-circle 遮罩渐变视图:

这是使用的代码:

class DashCircleViewController: UIViewController, UITextFieldDelegate {

    let dCircle1: MyDashedCircleView = {
        let v = MyDashedCircleView()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.clipsToBounds = false
        return v
    }()

    let dCircle2: MyDashedCircleView = {
        let v = MyDashedCircleView()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.clipsToBounds = false
        return v
    }()

    let dCircle3: MyDashedCircleView = {
        let v = MyDashedCircleView()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.clipsToBounds = false
        return v
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        let g = view.safeAreaLayoutGuide

        view.addSubview(dCircle1)
        NSLayoutConstraint.activate([
            dCircle1.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            dCircle1.topAnchor.constraint(equalTo: g.topAnchor, constant: 50.0),
            dCircle1.widthAnchor.constraint(equalToConstant: 200),
            dCircle1.heightAnchor.constraint(equalTo: dCircle1.widthAnchor),
        ])

        view.addSubview(dCircle2)
        NSLayoutConstraint.activate([
            dCircle2.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            dCircle2.topAnchor.constraint(equalTo: dCircle1.bottomAnchor, constant: 60.0),
            dCircle2.widthAnchor.constraint(equalTo: dCircle1.widthAnchor),
            dCircle2.heightAnchor.constraint(equalTo: dCircle2.widthAnchor),
        ])

        view.addSubview(dCircle3)
        NSLayoutConstraint.activate([
            dCircle3.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            dCircle3.topAnchor.constraint(equalTo: dCircle2.bottomAnchor, constant: 60.0),
            dCircle3.widthAnchor.constraint(equalTo: dCircle1.widthAnchor),
            dCircle3.heightAnchor.constraint(equalTo: dCircle2.widthAnchor),
        ])

        let tap = UITapGestureRecognizer(target: self, action: #selector(self.didTap(_:)))
        view.addGestureRecognizer(tap)

    }

    var isReady: Bool = false

    @objc func didTap(_ sender: UIGestureRecognizer) -> Void {
        // when view is tapped, toggle dCircle views between
        // initial state and animated
        isReady.toggle()
        if isReady {
            // animate circle1 only 45 degrees (25% of 180 degrees)
            dCircle1.doAnim(duration: 2.0, rotationDistance: 0.25)
            // animate circle2 only 90 degrees (50% of 180 degrees)
            dCircle2.doAnim(duration: 2.0, rotationDistance: 0.50)
            // animate circle3 180 degrees (100% of 180 degrees)
            dCircle3.doAnim(duration: 2.0, rotationDistance: 1.00)
        } else {
            dCircle1.resetAnim()
            dCircle2.resetAnim()
            dCircle3.resetAnim()
        }
    }
}

class MyDashedCircleView: UIView {

    // holds the blue dot layer
    var movingCircleView = UIView()

    // the blue "dot"
    var movingCircleLayer = CAShapeLayer()

    // will use gradientLayer and will be masked by circleMaskView
    var gradientView = UIView()

    // gradient layer
    var gradientLayer = CAGradientLayer()

    // uses circleLayer and will use be used to mask gradientView
    var circleMaskView = UIView()

    // the dashed semi-circle layer
    var circleLayer = CAShapeLayer()

    override init(frame: CGRect = .zero) {
        super.init(frame: frame)
        commonInit()
    }

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

    func commonInit() -> Void {

        gradientView.backgroundColor = .clear
        gradientView.translatesAutoresizingMaskIntoConstraints = false
        circleMaskView.backgroundColor = .clear
        circleMaskView.translatesAutoresizingMaskIntoConstraints = false
        movingCircleView.backgroundColor = .clear
        movingCircleView.translatesAutoresizingMaskIntoConstraints = false

        // add gradientView as subview
        addSubview(gradientView)

        // add moving circle view
        addSubview(movingCircleView)

        // constrain subviews to self
        NSLayoutConstraint.activate([
            gradientView.topAnchor.constraint(equalTo: topAnchor),
            gradientView.leadingAnchor.constraint(equalTo: leadingAnchor),
            gradientView.trailingAnchor.constraint(equalTo: trailingAnchor),
            gradientView.bottomAnchor.constraint(equalTo: bottomAnchor),

            movingCircleView.topAnchor.constraint(equalTo: topAnchor),
            movingCircleView.leadingAnchor.constraint(equalTo: leadingAnchor),
            movingCircleView.trailingAnchor.constraint(equalTo: trailingAnchor),
            movingCircleView.bottomAnchor.constraint(equalTo: bottomAnchor),
        ])

    }
    override func layoutSubviews() {
        super.layoutSubviews()

        // create the gradient layer
        createGradientLayer()
        // add it to gradient view
        gradientView.layer.addSublayer(gradientLayer)

        // create semi-circle layer
        createDashedSemiCircleLayer()
        // add it to circle mask view
        circleMaskView.layer.addSublayer(circleLayer)

        // during dev only
        // set to true to show dashed-circle as a subview instead of masking the gradient view
        if false {
            insertSubview(circleMaskView, belowSubview: movingCircleView)
            // constrain circleMaskView to self
            NSLayoutConstraint.activate([
                circleMaskView.topAnchor.constraint(equalTo: topAnchor),
                circleMaskView.leadingAnchor.constraint(equalTo: leadingAnchor),
                circleMaskView.trailingAnchor.constraint(equalTo: trailingAnchor),
                circleMaskView.bottomAnchor.constraint(equalTo: bottomAnchor),
            ])
        } else {
            // apply the mask
            gradientView.mask = circleMaskView
        }

        // create blue dot layer
        createMovingCircleLayer()
        // add it to movingCircleView
        movingCircleView.layer.addSublayer(movingCircleLayer)

    }
    func resetAnim() -> Void {
        // reset to starting conditions (without built-in animations)
        movingCircleLayer.removeAllAnimations()
        gradientLayer.removeAllAnimations()
    }
    func doAnim(duration _duration: CFTimeInterval, rotationDistance: CGFloat) -> Void {
        // perform the animation
        animateBoth(_duration, rotationDistance: rotationDistance)
    }
}

private extension MyDashedCircleView {

    func createDashedSemiCircleLayer() {
        circleLayer.lineWidth = 2.0
        circleLayer.strokeColor =  UIColor.darkGray.cgColor //border of circle
        circleLayer.fillColor = UIColor.clear.cgColor //inside the circle
        circleLayer.lineJoin = CAShapeLayerLineJoin.round
        circleLayer.lineDashPattern = [10, 10]

        // inset by 2-pts so we don't clip the edges of the dashed-circle-line
        let f = bounds.insetBy(dx: 2, dy: 2)

        circleLayer.path = UIBezierPath(arcCenter: CGPoint(x: f.midX, y: f.midY),
                                        radius: min(f.maxX, f.maxY) / 2,
                                        startAngle: 0,
                                        endAngle: .pi,
                                        clockwise: false).cgPath
    }

    func createMovingCircleLayer() {

        // inset by 1-pt to sit centered on the dashed semi-circle
        movingCircleLayer.frame = bounds.insetBy(dx: 1.0, dy: 1.0)
        let rect = CGRect(x: -10.0,
                          y: bounds.midY - 10.0,
                          width: 20,
                          height: 20)
        let path = UIBezierPath(ovalIn: rect)
        movingCircleLayer.fillColor = UIColor.blue.cgColor
        movingCircleLayer.path = path.cgPath

    }

    func createGradientLayer() -> Void {
        gradientLayer.frame = bounds
        gradientLayer.borderColor = UIColor.clear.cgColor
        gradientLayer.borderWidth = 1
        // vertical gradient - top will be gray, bottom will be orange
        gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
        gradientLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
        // produces half-orange half-gray with no "fade gradient"
        gradientLayer.locations = [0.0, 0.5, 0.5, 1.0]
        gradientLayer.colors = [
            UIColor.init(red: 211/255, green: 211/255, blue: 211/255, alpha: 1.0).cgColor,
            UIColor.init(red: 211/255, green: 211/255, blue: 211/255, alpha: 1.0).cgColor,
            UIColor.init(red: 239/255, green: 89/255, blue: 49/255, alpha: 1.0).cgColor,
            UIColor.init(red: 239/255, green: 89/255, blue: 49/255, alpha: 1.0).cgColor,
        ]
    }

    func animateBoth(_ duration: CFTimeInterval, rotationDistance: CGFloat) {

        CATransaction.begin()
        CATransaction.setAnimationDuration(duration)

        //-- animate blue dot layer rotation

        let circleOrbitRotateAnimation = CAKeyframeAnimation(keyPath: "transform.rotation")
        circleOrbitRotateAnimation.values = [0, .pi * rotationDistance]
        circleOrbitRotateAnimation.isAdditive = true
        circleOrbitRotateAnimation.repeatCount = 0
        circleOrbitRotateAnimation.calculationMode = .linear // kCAAnimationLinear
        circleOrbitRotateAnimation.rotationMode = .rotateAuto // kCAAnimationRotateAuto
        circleOrbitRotateAnimation.fillMode = .forwards // kCAFillModeForwards
        circleOrbitRotateAnimation.isRemovedOnCompletion = false

        movingCircleLayer.add(circleOrbitRotateAnimation, forKey: nil)

        //-- animate gradient layer rotation

        let gradientRotateAnimation = CAKeyframeAnimation(keyPath: "transform.rotation")
        gradientRotateAnimation.values = [0, .pi * rotationDistance]
        gradientRotateAnimation.isAdditive = true
        gradientRotateAnimation.repeatCount = 0
        gradientRotateAnimation.calculationMode = .linear // kCAAnimationLinear
        gradientRotateAnimation.rotationMode = .rotateAuto // kCAAnimationRotateAuto
        gradientRotateAnimation.fillMode = .forwards // kCAFillModeForwards
        gradientRotateAnimation.isRemovedOnCompletion = false

        gradientLayer.add(gradientRotateAnimation, forKey: nil)

        //--

        CATransaction.commit()
    }

}

【讨论】:

  • 谢谢!我会尽快深入研究您的代码。我找到了另一个解决方案! 0 级的 1 个静态虚线圆圈,然后在 1 和 2 级 - 不是虚线圆圈,虚线圆圈作为蒙版,蓝色球体在 3 级。然后我用“笔画开始”为蓝色和非虚线圆圈设置动画,时间为 2.0 秒
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-05-21
相关资源
最近更新 更多