难度:⭐️
最终效果:
在平时堆UI的时候,避免不了要碰到UED要给我们出一点点难题,比如这次又叫我抄一下淘宝的下拉刷新了(手动斜眼)。
说干就干,首先新建一个View封装所有的代码:
import UIKit
class AnimatedArrow: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
}
init
中调用我们添加子视图的代码。这时候来分析一下我们需要添加点什么:
- 一个画圆圈的layer
- 一个包含箭头的layer
其中箭头这个图层还要分为左中右三根直线。
我们先来画这个圆圈, 我们需要一个CAShapeLayer,把这个layer的frame设置一个正方形,然后利用贝塞尔曲线在这个正方形的Rect中画一个圆,把这个layer的路径设置为圆的路径。
然后设置一下layer基本的填充色、描边色、线帽(线的起点和终点两端的形状)。
为了做圆形的填充动画(其实就是描边),我们需要把描边的起点和终点设置一下。起点为0,终点由代码控制,设置为self.progress
.
稍微注意一下UIKit Graphics 方法和 Core Graphics方法的区别,Core Graphics是从macOS继承下来的,UIGraphics是iOS独有的一套。所以贝塞尔曲线类UIBezierPath是在UIKit里面的。
var circleLayer:CAShapeLayer?
func loadCircleLayer() {
let layer = CAShapeLayer()
//incase self.bounds.size is not a square
let radius = min(self.bounds.width, self.bounds.height)
let frame = CGRect(x: 0, y: 0, width: radius, height: radius)
layer.frame = frame
layer.strokeColor = UIColor.black.cgColor
layer.fillColor = UIColor.clear.cgColor
let path = UIBezierPath(ovalIn: frame)
layer.path = path.cgPath
layer.lineWidth = 1
layer.lineCap = kCALineCapRound
layer.strokeStart = 0
layer.strokeEnd = self.progress
self.layer.addSublayer(layer)
self.circleLayer = layer
}
再来看一下self.progress
,复写了一下它的set方法,在set的时候赋值并且改变circleLayer
的描边终点。至于最大值是0.95,那是为了留一个缺口,好看出旋转动画。
private var _progress:CGFloat = 0
var progress:CGFloat {
get {
return _progress
}
set {
_progress = newValue
self.circleLayer?.strokeEnd = min(0.95, newValue)
}
}
现在我们来画中间的这个箭头,首先判断一下比例:
可以看出来中间这根线大概是边长的二分之一,然后水平垂直都居中。我们来创建一下它的路径:
为什么有个 0.5 ? 因为线宽1,为了居中,就要左偏移0.5
func middlePath() -> CGPath {
let width = self.bounds.size.width / 2;
let path = UIBezierPath()
path.move(to: CGPoint(x: width - 0.5, y: width / 2))
path.addLine(to: CGPoint(x: width - 0.5, y: 3 * width / 2))
return path.cgPath
}
左边和右边这两条线起点是中间线的下端点,终点是左右一半的水平终点和垂直居中处,那就好办了,画一下他们俩的路径:
func leftPath() -> CGPath {
let width = self.bounds.size.width / 2;
let path = UIBezierPath()
path.move(to: CGPoint(x: width / 2, y: width))
path.addLine(to: CGPoint(x: width - 0.5, y: 3 * width / 2))
return path.cgPath
}
func rightPath() -> CGPath {
let width = self.bounds.size.width / 2;
let path = UIBezierPath()
path.move(to: CGPoint(x: 3 * width / 2, y: width))
path.addLine(to: CGPoint(x: width - 0.5, y: 3 * width / 2))
return path.cgPath
}
现在,根据这三条路径创建图层:
func templateLayer(path:CGPath) -> CAShapeLayer {
let layer = CAShapeLayer()
layer.frame = self.bounds
layer.strokeColor = UIColor.black.cgColor
layer.path = path
layer.lineWidth = 1
layer.lineCap = kCALineCapRound
layer.fillColor = UIColor.clear.cgColor
return layer
}
var containerLayer:CALayer?
func loadArrowLayer() {
self.containerLayer = CALayer()
self.containerLayer?.frame = self.bounds
self.containerLayer?.addSublayer(templateLayer(path: middlePath()))
self.containerLayer?.addSublayer(templateLayer(path: leftPath()))
self.containerLayer?.addSublayer(templateLayer(path: rightPath()))
self.layer.addSublayer(self.containerLayer!)
}
然后把它们组合起来:
func commonInit() {
loadCircleLayer()
loadArrowLayer()
}
创建动画:
圆形的填充progress是根据操作进行的,这个我们可以通过监听UIScrollView的contentOffset实现。还有一个就是箭头的隐藏和转圈,几行代码就可以了:
func startAnimation() {
self.containerLayer?.isHidden = true
let animation = CABasicAnimation(keyPath: "transform.rotation.z")
animation.fromValue = 0
animation.toValue = M_PI * 2
animation.duration = 1
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animation.repeatCount = Float.infinity
self.circleLayer?.add(animation, forKey: "infinity_rotate")
}
func stopAnimation() {
self.containerLayer?.isHidden = false
self.circleLayer?.removeAnimation(forKey: "infinity_rotate")
}
剩下要做的事情就是把这些操作组合一下,比如只在 progress 为0.95的时候才能触发 startAnimation()
这样可以避免视图显示不全。
最后,我把这个View添加到视图中,通过 set progress 来进行填充,然后调用动画就可以了。
- EOF -