iOS:将圆形切片动画为更宽的切片

ish*_*hak 6 core-animation ios

Core-Animation按照此图像中的描述处理角度:(来自http://btk.tillnagel.com/tutorials/rotation-translation-matrix.html的图片)

编辑:添加动画gif来更好地解释我需要的东西:

在此输入图像描述在此输入图像描述

我需要为切片设置动画以使其变宽,从300:315度开始,到300:060结束.

要创建每个切片,我正在使用此功能:

extension CGFloat {
    func toRadians() -> CGFloat {
        return self * CGFloat(Double.pi) / 180.0
    }
}
func createSlice(angle1:CGFloat, angle2:CGFloat) -> UIBezierPath! {
    let path: UIBezierPath = UIBezierPath()
    let width: CGFloat = self.frame.size.width/2
    let height: CGFloat = self.frame.size.height/2
    let centerToOrigin: CGFloat = sqrt((height)*(height)+(width)*(width));
    let ctr: CGPoint = CGPoint(x: width, y: height)
    path.move(to: ctr)
    path.addArc( withCenter: ctr,
                 radius: centerToOrigin,
                 startAngle: CGFloat(angle1).toRadians(),
                 endAngle: CGFloat(angle2).toRadians(),
                 clockwise: true
    )
    path.close()
    return path
}
Run Code Online (Sandbox Code Playgroud)

我现在可以创建两个切片和一个较小的子层,但我找不到如何从这一点开始:

func doStuff() {
    path1 = self.createSlice(angle1: 300,angle2: 315)
    path2 = self.createSlice(angle1: 300,angle2: 60)

    let shapeLayer = CAShapeLayer()
    shapeLayer.path = path1.cgPath
    shapeLayer.fillColor = UIColor.cyan.cgColor
    self.layer.addSublayer(shapeLayer)
Run Code Online (Sandbox Code Playgroud)

我非常感谢这里的任何帮助!

Dav*_*ist 10

只有一种颜色

如果你想为纯色填充饼段的角度设置动画,就像你问题中的那个一样,那么你可以通过strokeEnd设置a 的动画来实现CAShapeLayer.

这里的"诀窍"是制作一条非常宽泛的路线.更具体地说,您可以在预期半径的一半处创建一个仅为圆弧(下方动画中的虚线)的路径,然后将其全半径作为其线宽.当你动画抚摸那条线时,它看起来像下面的橙色部分:

动画形状图层的笔触结束

根据您的使用情况,您可以:

  • 创建从一个角度到另一个角度的路径,并将笔划的动画结束从0到1
  • 创建一个完整圆的路径,将笔划开始和笔划结束设置为圆的某个部分,并为从中的起始部分到结束部分的行程结束设置动画.

如果您的绘图只是这样的单一颜色,那么这将是您问题的最小解决方案.

但是,如果您的绘图更复杂(例如,还要调整饼图段),那么此解决方案根本不起作用,您将不得不做一些更复杂的事情.


自定义绘图/自定义动画

如果您对饼图段的绘制更复杂,那么您很快就会发现自己必须创建具有自定义可设置动画属性的图层子类.这样做会增加一些代码 - 其中一些可能看起来有点不寻常1 - 但并不像听起来那么可怕.

  1. 这可能是Objective-C中更方便的事情之一.

动态属性

首先,使用您将需要的属性创建一个图层子类.在Objective-C的说法中,这些属性应该是@dynamic,即不合成.这与dynamicSwift不同.相反,我们必须使用@NSManaged.

class PieSegmentLayer : CALayer {
    @NSManaged var startAngle, endAngle, strokeWidth: CGFloat
    @NSManaged var fillColor, strokeColor: UIColor?

    // More to come here ...
}
Run Code Online (Sandbox Code Playgroud)

这允许Core Animation动态处理这些属性,允许它跟踪更改并将它们集成到动画系统中.

注意:一个好的经验法则是这些属性应该与图层的绘图/视觉呈现相关.如果它们不是那么它很可能不属于该层.相反,它们可以添加到视图中,而视图又使用图层进行绘制.

复制图层

在自定义动画期间,Core Animation将要为不同的帧创建和呈现不同的图层配置.与Apple的大多数其他框架不同,这种情况发生在复制构造函数中init(layer:).对于要复制的上述五个属性,我们需要覆盖init(layer:)和复制它们的值.

在Swift中,我们还必须覆盖普通init()init?(coder).

override init(layer: Any) {
    super.init(layer: layer)
    guard let other = layer as? PieSegmentLayer else { return }

    fillColor   = other.fillColor
    strokeColor = other.strokeColor
    startAngle  = other.startAngle
    endAngle    = other.endAngle
    strokeWidth = other.strokeWidth
}

override init() {
    super.init()
}

required init?(coder aDecoder: NSCoder) {
    return nil
}
Run Code Online (Sandbox Code Playgroud)

应对变化

Core Animation在很多方面都是为了提高性能而构建的.实现这一目标的方法之一是避免不必要的工作.默认情况下,属性更改时,图层不会重绘自身.但是这些属性用于绘制,我们希望图层在任何更改时重绘.为此,我们需要覆盖needsDisplay(forKey:)并返回true密钥是否是这些属性之一.

override class func needsDisplay(forKey key: String) -> Bool {
    switch key {
    case #keyPath(startAngle), #keyPath(endAngle),
         #keyPath(strokeWidth),
         #keyPath(fillColor), #keyPath(strokeColor):
        return true

    default:
        return super.needsDisplay(forKey: key)
    }
}
Run Code Online (Sandbox Code Playgroud)

另外,如果我们想要这些属性的图层默认隐式动画,我们需要重写action(forKey:)以返回部分配置的动画对象.如果我们只想要一些属性(例如角度)来隐式动画,那么我们只需要为这些属性返回一个动画.除非我们需要非常自定义的东西,否则只需将fromValue设置返回到当前演示文稿值的基本动画:

override func action(forKey key: String) -> CAAction? {
    switch key {
    case #keyPath(startAngle), #keyPath(endAngle):
        let anim = CABasicAnimation(keyPath: key)
        anim.fromValue = presentation()?.value(forKeyPath: key)
        return anim

    default:
        return super.action(forKey: key)
    }
}
Run Code Online (Sandbox Code Playgroud)

画画

自定义动画的最后一部分是自定义绘图.这是通过覆盖draw(in:)并使用提供的上下文来绘制图层来完成的:

override func draw(in ctx: CGContext) {
    let center = CGPoint(x: bounds.midX, y: bounds.midY)
    // subtract half the stroke width to avoid clipping the stroke
    let radius = min(center.x, center.y) - strokeWidth / 2

    // The two angle properties are in degrees but CG wants them in radians.
    let start = startAngle * .pi / 180
    let end   = endAngle   * .pi / 180

    ctx.beginPath()
    ctx.move(to: center)

    ctx.addLine(to: CGPoint(x: center.x + radius * cos(start),
                            y: center.y + radius * sin(start)))

    ctx.addArc(center: center, radius: radius,
               startAngle: start, endAngle: end,
               clockwise: start > end)

    ctx.closePath()

    // Configure the graphics context
    if let fillCGColor = fillColor?.cgColor {
        ctx.setFillColor(fillCGColor)
    }
    if let strokeCGColor = strokeColor?.cgColor {
        ctx.setStrokeColor(strokeCGColor)
    }
    ctx.setLineWidth(strokeWidth)

    ctx.setLineCap(.round)
    ctx.setLineJoin(.round)

    // Draw
    ctx.drawPath(using: .fillStroke)
}
Run Code Online (Sandbox Code Playgroud)

在这里,我填充并描绘了一个从图层中心延伸到最近边缘的饼图段.您应该将其替换为自定义绘图.

一个自定义动画

有了所有代码,我们现在有了一个自定义图层子类,其属性可以隐式(仅通过更改它们)和显式(通过为其键添加CAAnimation)进行动画处理.结果看起来像这样:

自定义图层子类动画

最后的话

这些动画的帧速率可能并不明显,但在这两种解决方案中利用核心动画(以不同方式)的一个强大好处是它将单个状态的绘制与动画的时间分离.

这意味着该层不知道也不必知道持续时间,延迟,时序曲线等.这些都可以在外部进行配置和控制.