5-旋转的小菊-旋转画布和定时器

Posted 颐和园

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了5-旋转的小菊-旋转画布和定时器相关的知识,希望对你有一定的参考价值。

这个例子完全模仿了苹果的 UIActivityIndicatorView 控件,它显示了一个旋转的小菊花。这个控件使用起来是非常简单的,可以完全不需要你编写一行代码(使用 IB),也不需要任何图片!如果让你自己用 Core Graphics 实现这个控件,你会怎么做呢?

绘制外壳

class MockIndicator: UIView {
    var leafColor: UIColor = .white
    var hudColor: UIColor = UIColor(red: 170/255, green: 169/255, blue: 169/255, alpha: 0.5)
    
    override func draw(_ rect: CGRect) {
        // 1
        let shorterSide = min(rect.width, rect.height)
        // 2
        var frame = CGRect(x: (rect.width-shorterSide)/2, y:(rect.height-shorterSide)/2, width: shorterSide, height: shorterSide)
        // 3
        RectanglePainter.drawFillColor(frame, fillColor: hudColor, cornerRadius: 40/240*shorterSide)
    }
}
  1. 计算较短边,因为我们想绘制一个正方形的外壳。
  2. 计算外壳的 frame。如果初始化时传入的 rect 是一个长方形,我们需要将正方形外壳绘制在视图中心。
  3. 调用 RectanglePainter 绘制一个浅灰色的正方形外壳。

绘制叶片

        let context = UIGraphicsGetCurrentContext()
        // 1
        context?.saveGState()
        // 2
        context?.translateBy(x:rect.midX, y:rect.midY)
        // 3
        frame = CGRect(x: 0, y: -(0.25*shorterSide), width: 0.025*shorterSide, height: 0.1333*shorterSide)
        // 4
        RectanglePainter.drawFillColor(frame, fillColor: leafColor, cornerRadius: 0)
        // 5
        context?.restoreGState()
  1. 保存绘图状态。
  2. 我们需要围绕 hud 中心绘制一圈叶片(当然目前先绘制一片)。为了方便计算叶片的绘制位置,我们需要将 context 的坐标原点暂时移动到 hud 的中心。tranlateBy 方法用于移动坐标原点到指定位置。
  3. 计算叶片的 frame,注意此时坐标的计算方式已经变了,当前坐标的原点是视图中心。同时,叶片的坐标和宽高采用的都是相对数值,即都是按比例缩放的。
  4. 在制定位置绘制填充矩形充当菊花叶片。

绘制更多叶片

我们总共需要绘制 12 片叶片,叶片之间相差 30 度:

        // 1
        var h: CGFloat = 0
        var s: CGFloat = 0
        var b: CGFloat = 0
        var a: CGFloat = 0
        leafColor.getHue(&h, saturation: &s, brightness: &b, alpha: &a)
        
        for i in 0...11 {
            let context = UIGraphicsGetCurrentContext()
            // 2
            let angle = -CGFloat.pi/6*CGFloat(i)
            let brightness:CGFloat =  1-0.1*CGFloat(i%12)
            // 3
            let color = UIColor(hue: h, saturation: s, brightness: brightness, alpha: a)
            
            context?.saveGState()
            // 4
            context?.translateBy(x:rect.midX, y:rect.midY)
            // 5
            context?.rotate(by:angle)
            // 6
            frame = CGRect(x: 0, y: -(0.25*shorterSide), width: 0.025*shorterSide, height: 0.1333*shorterSide)
            // 7
            RectanglePainter.drawFillColor(frame, fillColor: color, cornerRadius: 0)
            context?.restoreGState()
        }
  1. 求取叶片颜色中的灰度、饱和度、亮度和透明度,因为我们准备对亮度进行运算。
  2. 角度计算,因为 12 个叶片的旋转角度是不同的,两两之间相差 30 度,转换为弧度。加上负号,是因为我们的绘制顺序是逆时针进行的。
  3. 改变叶片的颜色,将亮度值不断降低(逆时针)。
  4. 改变坐标原点。
  5. 逆时针旋转画布。每次循环都会在上一次的基础上多旋转 30 度。
  6. frame 保持不变,因为实际上画布在旋转了。如果画布不旋转,那我们就必须旋转图形了,但那个的计算要复杂许多。因此,我们采用旋转画布的方式。
  7. 绘制矩形。

旋转的花瓣

要让小菊花旋转,我们又要用定时器了。在 Indicator.h 中增加如下属性声明:

    private var tick:Int = 0				// 1
    private var timer: CADisplayLink?	// 2
    var isAnimating = false				// 3
    private var lastTime:CFTimeInterval = 0.0	// 4
    var secondPerLoop: Double = 1 // 5
  1. tick 假设我们旋转 1 周需要 12 次动画,那么 tick 就记录了当前进行到第几次。
  2. timer 定时器。
  3. isAnimating 记录动画的启动/停止状态。
  4. lastTime 是 CADisplayLink 定时器,它和一般的 Timer 不同,它初始化时不能设定触发间隔(它没有系统时钟信号),因此我们需要一个变量来保存上次触发函数的时间。
  5. secondPerLoop 小菊花每转一圈的时间,这里我们设置为 1,即每秒转一圈。

开启/关闭定时器,这和上一节没有太多区别了:

    func startAnimation() {
        if isAnimating {
            timer?.invalidate()
        }
        timer = CADisplayLink.init(target: self, selector: #selector(animate))
        timer?.add(to: RunLoop.current, forMode: .default)
        isAnimating = true
        lastTime = CACurrentMediaTime()
    }
    
    func stopAnimation() {
        if isAnimating {
            timer?.invalidate()
            isAnimating = false
            timer = nil
        }
    }

唯一需要注意的就是时钟启动时需要在 lastTime 中记录当前时间。

最重要的还是定时器回调函数:

    @objc private func animate() {
        guard let timer = timer else {
            return
        }
        // 1
        let period = secondPerLoop/12
        let currentTime = timer.timestamp
        // 2
        let elapsed = currentTime - lastTime
        // 3
        if elapsed > period {
            // 4
            tick += 1
            // 5
            if tick >= 12 {
                tick = 0
            }
            // 6
            setNeedsDisplay()
            // 7
            lastTime = currentTime
        }
    }

  1. 计算每转 1/12 圈(30 度)需要多少时间。
  2. 计算从上一次回调到现在过去了多少时间。
  3. 只有超过了 1/12 圈(30 度)需要的时间,我们才需要重新绘制。很显然,屏幕刷新一次的时间太短了,我们并不需要那么频繁地重绘图形。
  4. tick+1,将当前转到角度记录下来。
  5. 如果 tick 达到了满圈(360度),将角度归零,防止 tick 无限制累加下去,导致整数溢出。
  6. 重绘。这将导致 draw(_😃 方法被调用。
  7. 记录最新的 lastTime。

最终效果如图:

以上是关于5-旋转的小菊-旋转画布和定时器的主要内容,如果未能解决你的问题,请参考以下文章

画布旋转功能创建空白图像

html5图像旋转缩放并上传

JS中canvas画布绘制中如何实现缩放,位移,旋转

使用 ActionBar 旋转 Android 的双片段

颤动 - 如何使用画布围绕中心旋转图像

HTML5之canvas 7画布旋转