目前在自己的个人项目里,已经开始使用Swift去编写代码。这篇文章把项目中自己设计的一个ActivityIndicator View展示给大家。

在开始之前,我们先看看最终的效果,如下图:

我建议大家下载本文对应在Github分享的完整项目,以便跟着本篇文章来阅读代码。

需求分析

我们需要实现一个自定义的和 UIActivityIndicatorView 提供相似功能的一个Loading效果。我们将使用 Core Graphics 来绘制这样的效果,并让它动起来。

让我们先分析一下这个控件的组成,为我们实际编码提供具体的思路。

首先,这个loading效果图,是由8个圆弧组成的一个圆。

我们先要会画圆弧:

像这样画8个圆弧,围成一个圆:

然后通过重复改变每一个圆弧的颜色,让它动起来。

我们继承UIView, 重写drawRect方法绘制界面,第一步得到当前绘图的上下文:

1
let context = UIGraphicsGetCurrentContext()

绘制圆弧

这里我们使用 UIBezierPath 类去构建路径,然后通过绘制路径的方式绘制圆弧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 初始化一个 UIBezierPath 实例
let arcPath = UIBezierPath()
 
// 构建Arc路径
arcPath.addArcWithCenter(CGPointMake(CGFloat(self.frame.size.width/2), CGFloat(self.frame.size.height/2)), radius: CGFloat(Config.CC_ARC_DRAW_RADIUS), startAngle: CGFloat(DegreesToRadians(startAngle)), endAngle: CGFloat(DegreesToRadians(startAngle + Config.CC_ARC_DRAW_DEGREE)), clockwise: true)
 
// 把路径添加到当前绘图的上下文
CGContextAddPath(context, arcPath.CGPath)
 
// 设置线段宽度
CGContextSetLineWidth(context, CGFloat(Config.CC_ARC_DRAW_WIDTH))
 
// 设置线段颜色
CGContextSetStrokeColorWithColor(context, strokeColor)
 
// 绘制        
CGContextStrokePath(context)

通过如上的方式,我们就可以成功画出一个圆弧。其中:

1
func addArcWithCenter(center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, clockwise: Bool)

这个方法构建路径的解释是 center 为圆点坐标,radius 为半径,startAngle 为开始的弧度,endAngle 为结束的弧度,clockwise 表示的是顺时针还是逆时针。

绘制8个圆弧

当我们可以成功在绘图上下文绘制出圆弧时,我们应该开始着手绘制效果图中的8个圆弧,并让它在正确的位置,并带上不同颜色。

这里是效果图的一些参数设置,包括半径,宽度,颜色等信息:

1
2
3
4
5
6
7
8
struct Config {
    static let CC_ACTIVITY_INDICATOR_VIEW_WIDTH = 40
    static let CC_ARC_DRAW_PADDING = 3.0
    static let CC_ARC_DRAW_DEGREE = 39.0
    static let CC_ARC_DRAW_WIDTH = 6.0
    static let CC_ARC_DRAW_RADIUS = 10.0
    static let CC_ARC_DRAW_COLORS = [UIColor(red: 242/255.0, green: 242/255.0, blue: 242/255.0, alpha: 1.0).CGColor, UIColor(red: 230/255.0, green: 230/255.0, blue: 230/255.0, alpha: 1.0).CGColor, UIColor(red: 179/255.0, green: 179/255.0, blue: 179/255.0, alpha: 1.0).CGColor, UIColor(red: 128/255.0, green: 128/255.0, blue: 128/255.0, alpha: 1.0).CGColor, UIColor(red: 128/255.0, green: 128/255.0, blue: 128/255.0, alpha: 1.0).CGColor, UIColor(red: 128/255.0, green: 128/255.0, blue: 128/255.0, alpha: 1.0).CGColor, UIColor(red: 128/255.0, green: 128/255.0, blue: 128/255.0, alpha: 1.0).CGColor, UIColor(red: 128/255.0, green: 128/255.0, blue: 128/255.0, alpha: 1.0).CGColor]
    }

我们可以在drawRect方法,循坏绘制8个圆弧,此时完整的代码看上去像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
override func drawRect(rect: CGRect) {
 
    let context = UIGraphicsGetCurrentContext()
 
    var startAngle = Config.CC_ARC_DRAW_PADDING
 
    for index in 1...8 {
        let arcPath = UIBezierPath()
        arcPath.addArcWithCenter(CGPointMake(CGFloat(self.frame.size.width/2), CGFloat(self.frame.size.height/2)), radius: CGFloat(Config.CC_ARC_DRAW_RADIUS), startAngle: CGFloat(DegreesToRadians(startAngle)), endAngle: CGFloat(DegreesToRadians(startAngle + Config.CC_ARC_DRAW_DEGREE)), clockwise: true)
        CGContextAddPath(context, arcPath.CGPath)
        startAngle += Config.CC_ARC_DRAW_DEGREE + (Config.CC_ARC_DRAW_PADDING * 2)
 
        CGContextSetLineWidth(context, CGFloat(Config.CC_ARC_DRAW_WIDTH))
        let colorIndex = abs(index - self.animateIndex)
        let strokeColor = Config.CC_ARC_DRAW_COLORS[colorIndex]
        CGContextSetStrokeColorWithColor(context, strokeColor)
        CGContextStrokePath(context)
    }
}

使用for循环绘制8次,产生8个圆弧,并且设置不同的颜色。这里的self.animateIndex用来跟踪整个动画的头一个颜色最浅圆弧的位置。通过它和当前index的绝对值,获得当前圆弧应该显示的颜色。

动起来

在设计一个ActivityIndicator View的时候,我们应该像UIKit提供的 UIActivityIndicatorView 一样,至少需要实现这三组API:

1
2
3
func startAnimating()
func stopAnimating()
func isAnimating() -> Bool

这里我们使用一个timer去改变self.animateIndex的值,不断重画当前视图,来产生动画效果,代码看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 使用该值驱动改变圆弧颜色,产生动画效果
private var animateIndex: Int = 1
 
// 动画的Timer
private var animatedTimer: NSTimer?
 
// timer响应的事件,在这里setNeedsDisplay让UIKit重画当前视图,然后不断改变animateIndex值。
@objc private func animate () {
    if !self.hidden {
        self.setNeedsDisplay()
        self.animateIndex++
        if self.animateIndex > 8 {
            self.animateIndex = 1
        }
    }
}
 
// 开始动画
func startAnimating () {
 
    if self.hidden {
        self.hidden = false
    }
 
    if let timer = self.animatedTimer {
        timer.fire()
    else {
        self.animatedTimer = NSTimer(timeInterval: 0.1, target: self, selector: "animate", userInfo: nil, repeats: true)
        NSRunLoop.currentRunLoop().addTimer(self.animatedTimer!, forMode: NSRunLoopCommonModes)
    }
}

这里使用

1
init(timeInterval ti: NSTimeInterval, target aTarget: AnyObject, selector aSelector: Selector, userInfo: AnyObject?, repeats yesOrNo: Bool) -> NSTimer

而不是使用

1
class func scheduledTimerWithTimeInterval(ti: NSTimeInterval, target aTarget: AnyObject, selector aSelector: Selector, userInfo: AnyObject?, repeats yesOrNo: Bool) -> NSTimer

构建timer的原因是:当我们在使用自己的ActivityIndicator View的时候,我们可能把它放到UIScrollView上面。这个时候使用scheduledTimerWithTimeInterval创建的timer是加入到当前Run Loop中的,而UIScrollView在接收到用户交互事件时,主线程Run Loop会设置为UITrackingRunLoopMode。这个时候会导致timer失效。更详细的解答,我在走进Run Loop的世界 (一):什么是Run Loop?一文中有说明。

总结

到这个时候,我们应该就能看到和效果图一样的动画效果。但是写一个可供使用的自定义控件时,应该考虑更多的细节工作。比如初始化,视图移除,intrinsicContentSize,是否需要支持 @IBInspectable 和 @IBDesignable 等等,来让使用我们控件的开发者更加友好。更加详细的代码和Demo可以去这里查看:https://github.com/yechunjun/CCActivityIndicatorView

使用 Swift 构建自定义的ActivityIndicator View的更多相关文章

  1. 自定义控制器的View(loadView)及其注意点

    *:first-child { margin-top: 0 !important; } body > *:last-child { margin-bottom: 0 !important; } ...

  2. Android开发——构建自定义组件

    Android中,你的应用程序程序与View类组件有着一种固定的联系,例如按钮(Button). 文本框(TextView), 可编辑文本框(EditText), 列表框(ListView), 复选框 ...

  3. 十六、C# 常用集合类及构建自定义集合(使用迭代器)

    常用集合类及构建自定义集合 1.更多集合接口:IList<T>.IDictionary<TKey,TValue>.IComparable<T>.ICollectio ...

  4. 贝塞尔曲线:原理、自定义贝塞尔曲线View、使用!!!

    一.原理 转自:http://www.2cto.com/kf/201401/275838.html Android动画学习Demo(3) 沿着贝塞尔曲线移动的Property Animation Pr ...

  5. 构建自定义docker镜像,上传至docker hub

    docker 优势 (外部参考) Docker 让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后 发布到任何流行的Linux机器上,便可以实现虚拟化.Docker改变了虚拟化的方 式,使 ...

  6. docker构建自定义镜像

    docker构建自定义镜像 要构建一个镜像,第一步准备所需要的文件,第二步编写Dockerfile文件,比如我现在构建一个java web镜像 第一步:准备java web工程的war包文件(这里假设 ...

  7. android显示通知栏Notification以及自定义Notification的View

    遇到的最大的问题是监听不到用户清除通知栏的广播.所以是不能监听到的. 自定义通知栏的View,然后service运行时更改notification的信息. /** * Show a notificat ...

  8. Linux下基于官方源代码RPM包构建自定义MySQL RPM包

    rpmbuild时不要以root用户执行! 方法一: 1.首先安装rpmbuild #yum install rpm-build gcc gcc-c++ cmake bison ncurses-dev ...

  9. java并发编程(7)构建自定义同步工具及条件队列

    构建自定义同步工具 一.通过轮询与休眠的方式实现简单的有界缓存 public void put(V v) throws InterruptedException { while (true) { // ...

随机推荐

  1. 19-10-24-J-快乐?

    向未来的大家发送祝福(不接受的请自动忽略): 祝大家程序员节快乐! 好了. ZJ一下 额. 考场上差点死了. 码1h后,T1还没过大样例. 我×××. 后来发现是自己××了. T2T3丢暴力. 比咕的 ...

  2. mysql innodb 的 逻辑存储结构

    如上图: innodb 的 逻辑存储单元分成 表空间,段,区,页 4个等级 默认情况下,一个数据库 所有变共享一个 默认的表空间(tablespan).可以指定每个表一个表空间. 一个表空间管理着 多 ...

  3. Flask中cookie和session设置与csrf原理攻防

    Flask之操作cookie app.py from flask import Flask, request, Response app = Flask(__name__) @app.route('/ ...

  4. scrapy中的Request和Response对象

    前言: 如果框架中的组件比做成是人的各个器官的话,那个Request和Response就是血液,Item就是代谢产物 Request对象: 是用来描述一个HTTP请求,其构造参数有 url 请求的UR ...

  5. Java爬虫的实现

    距离上一次写爬虫还是几年前了,那时候一直使用的是httpclient. 由于最近的项目又需要使用到爬虫,因此又重新查询了一些爬虫相关的框架,其中最合适的是WebMagic 官方文档:https://g ...

  6. 2019.10.29 csp-s模拟测试93 反思总结

    T1: 求出前缀和,三维偏序O(nlog2n)CDQ 二维其实就可以 #include<iostream> #include<cstdio> #include<cstri ...

  7. DES、RSA、MD5、SHA、随机生成加密与解密

    一.数据加密/编码算法列表   常见用于保证安全的加密或编码算法如下:   1.常用密钥算法   密钥算法用来对敏感数据.摘要.签名等信息进行加密,常用的密钥算法包括:   DES(Data Encr ...

  8. idea2018.1.5永久破解过程

    可以根据官网推荐注册idea:http://idea.lanyus.com/ 步骤如下:1 下载破解(crack) jar 包 链接:https://pan.baidu.com/s/1-COPHVJi ...

  9. 使用Python Requests上传表单数据和文件

    在Python环境下写一个HTTP客户端,发送POST请求,同时上传表单数据和文件,我们可以使用Requests模块来实现.代码如下: data = { 'name': 'nginx' } files ...

  10. REM布局计算,移动端,pc端有兼容性)

    rem布局计算(移动端,pc端有兼容性) <!DOCTYPE html> <html> <head lang="en"> <script& ...