本文由@Chun发表于Chun Tips :http://chun.tips/blog/2014/12/11/shi-yong-swift-gou-jian-zi-ding-yi-de-activityindicator-view/
目前在自己的个人项目里,已经开始使用Swift去编写代码。这篇文章把项目中自己设计的一个ActivityIndicator View展示给大家。
在开始之前,我们先看看最终的效果,如下图:
我建议大家下载本文对应在Github分享的完整项目:https://github.com/yechunjun/CCActivityIndicatorView,以便跟着本篇文章来阅读代码。
需求分析
我们需要实现一个自定义的和 UIActivityIndicatorView 提供相似功能的一个Loading效果。我们将使用 Core Graphics 来绘制这样的效果,并使用 Core Animation让它动起来。
让我们先分析一下这个控件的组成,为我们实际编码提供具体的思路。
首先,这个loading效果图,是由8个圆弧组成的一个圆。
我们先要会画圆弧:
像这样画8个不同颜色的圆弧,围成一个圆,形成动画的一帧:
实现该动画至少需要8种不同情况的圆,然后通过Core Animation的CAKeyframeAnimation关键帧动画让它动起来。
绘制圆弧
这里我们使用 UIBezierPath 类去构建路径,然后通过绘制路径的方式绘制圆弧。
let context = UIGraphicsGetCurrentContext() // 初始化一个 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)
通过如上的方式,我们就可以成功画出一个圆弧。其中:
func addArcWithCenter(center: CGPoint,radius: CGFloat,startAngle: CGFloat,endAngle: CGFloat,clockwise: Bool)
这个方法构建路径的解释是 center 为圆点坐标,radius 为半径,startAngle 为开始的弧度,endAngle 为结束的弧度,clockwise 表示的是顺时针还是逆时针。
绘制一帧Contents:8个圆弧
当我们可以成功在绘图上下文绘制出圆弧时,我们应该开始着手绘制效果图中的8个圆弧,并让它在正确的位置,带上不同颜色。
static let CC_ACTIVITY_INDICATOR_VIEW_NUMBEROFFRAMES: Int = 8 static let CC_ACTIVITY_INDICATOR_VIEW_WIDTH: CGFloat = 40 static let CC_ARC_DRAW_PADDING: CGFloat = 3.0 static let CC_ARC_DRAW_DEGREE: CGFloat = 39.0 static let CC_ARC_DRAW_WIDTH: CGFloat = 6.0 static let CC_ARC_DRAW_RADIUS: CGFloat = 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,UIColor(red: 179/255.0,green: 179/255.0,blue: 179/255.0,UIColor(red: 128/255.0,green: 128/255.0,blue: 128/255.0,alpha: 1.0).CGColor]
我们可以在循坏中绘制8个圆弧,生成一张image,做为动画的一帧,此时完整的代码看上去像这样:
static func CCActivityIndicatorViewFrameImage(frame: Int,_ scale: CGFloat) -> UIImage { // 创建一个基于位图的上下文(context) UIGraphicsBeginImageContextWithOptions(CGSizeMake(CC_ACTIVITY_INDICATOR_VIEW_WIDTH,CC_ACTIVITY_INDICATOR_VIEW_WIDTH),false,scale) let context = UIGraphicsGetCurrentContext() var startDegree = Config.CC_ARC_DRAW_PADDING for index in 1...8 { let arcPath = UIBezierPath() let center = CGPointMake(Config.CC_ACTIVITY_INDICATOR_VIEW_WIDTH / 2,Config.CC_ACTIVITY_INDICATOR_VIEW_WIDTH / 2) let startAngle = CGFloat(DegreesToRadians(Double(startDegree))) let endAngle = CGFloat(DegreesToRadians(Double(startDegree + Config.CC_ARC_DRAW_DEGREE))) arcPath.addArcWithCenter(center,radius: Config.CC_ARC_DRAW_RADIUS,startAngle: startAngle,endAngle: endAngle,clockwise: true) CGContextAddPath(context,arcPath.CGPath) startDegree += Config.CC_ARC_DRAW_DEGREE + (Config.CC_ARC_DRAW_PADDING * 2) CGContextSetLineWidth(context,Config.CC_ARC_DRAW_WIDTH) let colorIndex = abs(index - frame) let strokeColor = Config.CC_ARC_DRAW_COLORS[colorIndex] CGContextSetStrokeColorWithColor(context,strokeColor) CGContextStrokePath(context) } let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return image }
使用for循环绘制8次,产生8个圆弧,并且设置不同的颜色。这里的frame用来确定当前image所处的帧数。通过它和当前index的绝对值,我们可以获得当前圆弧应该显示的颜色。
动起来
在设计一个ActivityIndicator View的时候,我们应该像UIKit提供的 UIActivityIndicatorView 一样,至少需要实现这三组API:
func startAnimating() func stopAnimating() func isAnimating() -> Bool
感谢 @传人Joe的评价,这里把原先通过timer执行的动画换成了CoreAnimation来实现。
func startAnimating () { func animate () { self.animating = true let animationDuration: CFTimeInterval = 0.8 // 构造一组动画所需的images var animationImages = [CGImageRef]() for frame in 1...Config.CC_ACTIVITY_INDICATOR_VIEW_NUMBEROFFRAMES { animationImages.append(Config.CCActivityIndicatorViewFrameImage(frame,UIScreen.mainScreen().nativeScale).CGImage) } // 使用Core Animation关键帧CAKeyframeAnimation构建动画 let animation = CAKeyframeAnimation(keyPath: "contents") animation.calculationMode = kCAAnimationDiscrete animation.duration = animationDuration animation.repeatCount = HUGE animation.values = animationImages animation.removedOnCompletion = false animation.fillMode = kCAFillModeBoth self.layer.addAnimation(animation,forKey: "contents") } if !self.animating { animate() } } func stopAnimating () { self.animating = false self.layer.removeAnimationForKey("contents") self.layer.contents = Config.CCActivityIndicatorViewFrameImage(0,UIScreen.mainScreen().nativeScale).CGImage }
这里需要注意的是:考虑不同iPhone分辨率的情况,我们在绘制image作为视图Layer的contents的时候,使用UIScreen.mainScreen().nativeScale的大小构造image context上下文环境
总结
到这个时候,我们应该就能看到和效果图一样的动画效果。但是写一个可供使用的自定义控件时,应该考虑更多的细节工作。比如初始化,视图移除,intrinsicContentSize,是否需要支持 @IBInspectable 和 @IBDesignable 等等,来让使用我们控件的开发者更加友好。更加详细的代码和Demo可以去这里查看:https://github.com/yechunjun/CCActivityIndicatorView
推荐几篇阅读量最高的文章