如何在iOS中设置自定义属性的动画

如何在iOS中设置自定义属性的动画,ios,swift,uiviewanimation,Ios,Swift,Uiviewanimation,我有一个自定义UIView,它使用核心图形调用绘制其内容。所有这些都工作正常,但现在我想设置一个影响显示的值变化的动画。我有一个自定义属性可以在自定义UView中实现这一点: var _anime: CGFloat = 0 var anime: CGFloat { set { _anime = newValue for(gauge) in gauges { gauge.animate(newValue) }

我有一个自定义UIView,它使用核心图形调用绘制其内容。所有这些都工作正常,但现在我想设置一个影响显示的值变化的动画。我有一个自定义属性可以在自定义UView中实现这一点:

var _anime: CGFloat = 0
var anime: CGFloat {
    set {
        _anime = newValue
        for(gauge) in gauges {
            gauge.animate(newValue)
        }
        setNeedsDisplay()
    }
    get {
        return _anime
    }
}
我已经从ViewController启动了一个动画:

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    self.emaxView.anime = 0.5
    UIView.animate(withDuration: 4) {
        DDLogDebug("in animations")
        self.emaxView.anime = 1.0
    }
}
这不起作用-动画值会从0.5更改为1.0,但会立即更改。有两次调用动画设定器,一次值为0.5,然后立即调用1.0。如果我将正在设置动画的属性更改为标准UIView属性,例如alpha,它将正常工作

我来自安卓系统的背景,所以整个iOS动画框架在我看来似乎有点像黑魔法。除了预定义的UIView属性外,还有其他方法可以设置属性的动画吗

下面是动画视图应该是什么样子的-它大约每1/2秒获得一个新值,我希望指针在这段时间内从上一个值平滑地移动到下一个值。更新它的代码是:

open func animate(_ progress: CGFloat) {
    //DDLogDebug("in animate: progress \(progress)")
    if(dataValid) {
        currentValue = targetValue * progress + initialValue * (1 - progress)
    }
}
更新后调用
draw()
将使用新指针位置重新绘制,在
initialValue
targetValue


如果在
gauge.animate(newValue)
函数中修改任何自动布局约束,则需要调用
layoufifreeded()
而不是
setNeedsDisplay()


简短回答:使用
CADisplayLink
每n帧呼叫一次。示例代码:

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    let displayLink = CADisplayLink(target: self, selector: #selector(animationDidUpdate))
    displayLink.preferredFramesPerSecond = 50
    displayLink.add(to: .main, forMode: .defaultRunLoopMode)
    updateValues()
}

var animationComplete = false
var lastUpdateTime = CACurrentMediaTime()
func updateValues() {
    self.emaxView.animate(0);
    lastUpdateTime = CACurrentMediaTime()
    animationComplete = false
}

func animationDidUpdate(displayLink: CADisplayLink) {

    if(!animationComplete) {
        let now = CACurrentMediaTime()
        let interval = (CACurrentMediaTime() - lastUpdateTime)/animationDuration
        self.emaxView.animate(min(CGFloat(interval), 1))
        animationComplete = interval >= 1.0
    }
}
}


代码可以被细化和概括,但它正在做我需要的工作。

如果完全用CoreGraphics绘制,如果你想做一点数学,有一种非常简单的方法来制作动画。幸运的是,这里有一个刻度,告诉你旋转的弧度数,所以数学是最小的,不涉及三角。这样做的好处是,您不必重新绘制整个背景,甚至不必重新绘制指针。这可能有点棘手,以获得正确的角度和东西,我可以帮助,如果以下不工作

通常在Draw(矩形)中绘制视图的背景。你应该把指针放进CALayer里。您几乎可以将指针的绘图代码(包括中间的深灰色圆圈)移动到一个单独的方法中,该方法返回UIImage。图层将根据视图的框架(在布局子视图中)调整大小,并且必须将定位点设置为(0.5,0.5),这实际上是默认值,因此您可以忽略该线。然后,动画方法只是根据需要更改层的变换以旋转。我会这样做的。我要更改方法和变量名,因为anime和animate太模糊了

因为图层特性以0.25的持续时间隐式设置动画,所以您甚至不需要调用动画方法就可以摆脱此问题。我已经有一段时间没有和CoreAnimation合作了,所以很明显要测试一下

这里的优点是,您只需将刻度盘的转速设置为所需的转速,它就会旋转到该转速。没有人会读你的代码,并像WTF是_anime!:)我已经包括了init方法来提醒您更改层的内容比例(或者它以低质量呈现),很明显,您的init中可能还有其他内容

class SpeedDial: UIView {

    var pointer: CALayer!
    var pointerView: UIView!

    var rpm: CGFloat = 0 {
        didSet {
            pointer.setAffineTransform(rpm == 0 ? .identity : CGAffineTransform(rotationAngle: rpm/25 * .pi))
        }
    }

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

        pointer = CALayer()
        pointer.contentsScale = UIScreen.main.scale

        pointerView = UIView()
        addSubview(pointerView)
        pointerView.layer.addSublayer(pointer)
    }

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

        pointer = CALayer()
        pointer.contentsScale = UIScreen.main.scale

        pointerView = UIView()
        addSubview(pointerView)
        pointerView.layer.addSublayer(pointer)
    }

    override func draw(_ rect: CGRect) {
        guard let context = UIGraphicsGetCurrentContext() else { return }

        context.saveGState()

        //draw background with values
        //but not the pointer or centre circle

        context.restoreGState()
    }


    override func layoutSubviews() {
        super.layoutSubviews()

        pointerView.frame = bounds

        pointer.frame = bounds
        pointer.anchorPoint = CGPoint(x: 0.5, y: 0.5)

        pointer.contents = drawPointer(in: bounds)?.cgImage
    }


    func drawPointer(in rect: CGRect) -> UIImage? {
        UIGraphicsBeginImageContextWithOptions(rect.size, false, 0)
        guard let context = UIGraphicsGetCurrentContext() else { return nil }

        context.saveGState()

        // draw the pointer Image. Make sure to draw it pointing at zero. ie at 8 o'clock
        // I'm not sure what your drawing code looks like, but if the pointer is pointing
        // vertically(at 12 o'clock), you can get it pointing at zero by rotating the actual draw context like so:
        // perform this context rotation before actually drawing the pointer
        context.translateBy(x: rect.width/2, y: rect.height/2)
        context.rotate(by: -17.5/25 * .pi) // the angle judging by the dial - remember .pi is 180 degrees
        context.translateBy(x: -rect.width/2, y: -rect.height/2)



        context.restoreGState()
        let pointerImage = UIGraphicsGetImageFromCurrentImageContext()

        UIGraphicsEndImageContext()
        return pointerImage

    }

}
指针的标识转换使其指向0 RPM,因此每次将RPM提高到所需的值时,它都会旋转到该值


编辑:测试它,它工作。除了我犯了几个错误-您不需要更改层的位置,我相应地更新了代码。此外,更改图层的变换会触发直接父视图中的LayoutSubview。我忘了这件事。最简单的方法是将指针层放入UIView,UIView是SpeedDial的子视图。我已经更新了代码。祝你好运这可能有些过分,但它比设置视图、背景和所有对象的整个渲染动画更具可重用性。

您试图通过此属性更改实现什么?如果该属性更改了在draw(in:rect)中执行的绘图的某些方面,您将无法将其设置为那样的动画。@twiz_uu似乎是这样。还有其他方法的建议吗?看看如何使用
CAAnimation
修改自定义draw rect图形!我不确定
UIView.animate
way是否适用于自定义绘制rect图形@克莱德,如果我知道你想制作什么动画,我真的会的。您是否有前后图片或一般描述?很可能最终,您将不得不将动画方面放在CALayer中,并为层设置动画。另外,
func animate
的代码是helpful@twiz_我更新了更详细的问题布局没有被修改
setNeedsDisplay()
触发对
draw()
的调用,这是更新显示所需的全部内容。感谢您的详细回答。是的,这一切都是通过核心图形完成的,CGContext被缩放和转换,这样我就可以在一个以针为中心的100x100框中绘制。如何设置数值的动画?这需要重新绘制每个动画周期。此外,在同一个UIView中有多个类似的仪表,所有仪表都会同时更新,因此将它们一起设置动画是有意义的。添加多个子视图会变得混乱-现在有一个类来绘制仪表,它非常轻量级,每个仪表有一个参数化实例。我还有其他需要动画的元素,这是标准UIView或图层属性无法完成的-例如,改变长度和颜色的条形图。不用担心。改变长度和颜色的条最好作为层设置动画。更改RPM文本-我只需将UILabel子视图添加到快速拨号盘,并相应地定位,然后更改RPM变量的didSet子句中的值