当前位置:网站首页>5-旋转的小菊-旋转画布和定时器
5-旋转的小菊-旋转画布和定时器
2022-06-23 07:56:00 【颐和园】
这个例子完全模仿了苹果的 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)
}
}
- 计算较短边,因为我们想绘制一个正方形的外壳。
- 计算外壳的 frame。如果初始化时传入的 rect 是一个长方形,我们需要将正方形外壳绘制在视图中心。
- 调用 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()
- 保存绘图状态。
- 我们需要围绕 hud 中心绘制一圈叶片(当然目前先绘制一片)。为了方便计算叶片的绘制位置,我们需要将 context 的坐标原点暂时移动到 hud 的中心。tranlateBy 方法用于移动坐标原点到指定位置。
- 计算叶片的 frame,注意此时坐标的计算方式已经变了,当前坐标的原点是视图中心。同时,叶片的坐标和宽高采用的都是相对数值,即都是按比例缩放的。
- 在制定位置绘制填充矩形充当菊花叶片。

绘制更多叶片
我们总共需要绘制 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()
}
- 求取叶片颜色中的灰度、饱和度、亮度和透明度,因为我们准备对亮度进行运算。
- 角度计算,因为 12 个叶片的旋转角度是不同的,两两之间相差 30 度,转换为弧度。加上负号,是因为我们的绘制顺序是逆时针进行的。
- 改变叶片的颜色,将亮度值不断降低(逆时针)。
- 改变坐标原点。
- 逆时针旋转画布。每次循环都会在上一次的基础上多旋转 30 度。
- frame 保持不变,因为实际上画布在旋转了。如果画布不旋转,那我们就必须旋转图形了,但那个的计算要复杂许多。因此,我们采用旋转画布的方式。
- 绘制矩形。

旋转的花瓣
要让小菊花旋转,我们又要用定时器了。在 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
- tick 假设我们旋转 1 周需要 12 次动画,那么 tick 就记录了当前进行到第几次。
- timer 定时器。
- isAnimating 记录动画的启动/停止状态。
- lastTime 是 CADisplayLink 定时器,它和一般的 Timer 不同,它初始化时不能设定触发间隔(它没有系统时钟信号),因此我们需要一个变量来保存上次触发函数的时间。
- 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/12 圈(30 度)需要多少时间。
- 计算从上一次回调到现在过去了多少时间。
- 只有超过了 1/12 圈(30 度)需要的时间,我们才需要重新绘制。很显然,屏幕刷新一次的时间太短了,我们并不需要那么频繁地重绘图形。
- tick+1,将当前转到角度记录下来。
- 如果 tick 达到了满圈(360度),将角度归零,防止 tick 无限制累加下去,导致整数溢出。
- 重绘。这将导致 draw(_ 方法被调用。
- 记录最新的 lastTime。
最终效果如图:

边栏推荐
- 生产环境服务器环境搭建+项目发布流程
- PHP serialization and deserialization CTF
- Location of firewalld configuration file
- 11 字符串函数
- GTEST death test
- How to mine keywords and improve user experience before website construction?
- aquatone工具 中的2个bug修复
- 走好数据中台最后一公里,为什么说数据服务API是数据中台的标配?
- How to start Jupiter notebook in CONDA virtual environment
- jmeter压测结果分析
猜你喜欢

VTK. Le bouton gauche de la souris JS glisse pour changer le niveau et la largeur de la fenêtre

AVL树的实现

Vulnhub | DC: 3 |【实战】

开源技术交流丨批流一体数据同步引擎ChunJun数据还原-DDL功能模块解析

数据资产为王,解析企业数字化转型与数据资产管理的关系

Check the file through the port

ThreadPoolExecutor线程池实现原理与源码解析

Image segmentation - improved network structure

Display proportion of sail soft accumulation diagram

QT irregular shape antialiasing
随机推荐
@Controller和@RestController的区别?
Hackers use new PowerShell backdoors in log4j attacks
How to start Jupiter notebook in CONDA virtual environment
Implementation principle and source code analysis of ThreadPoolExecutor thread pool
Location of firewalld configuration file
顺序表课设
Deep learning ----- different methods to implement lenet-5 model
力扣(LeetCode)173. 二叉搜索树迭代器(2022.06.22)
Deep learning ----- convolution (conv2d) bottom layer
开源软件、自由软件、Copyleft、CC都是啥,傻傻分不清楚?
数据资产为王,解析企业数字化转型与数据资产管理的关系
MySQL brochure notes 5 InnoDB record storage structure
Create an orderly sequence table and perform the following operations: 1 Insert element x into the table and keep it in order; 2. find the element with the value of X, and delete it if found; 3. outpu
vtk.js鼠標左鍵滑動改變窗比特和窗寬
Vulnhub | DC: 4 |【实战】
INT 104_LEC 06
Check the file through the port
生产环境服务器环境搭建+项目发布流程
抓包发现tcp会话中老是出现重复的ack和大量的tcp重传——SACK(Selective Acknowledgment, 选择性确认)技术
socket编程(多线程)