如圖,這個(gè)動(dòng)畫(huà)的是如何做的呢?
分析:
- 1.環(huán)形進(jìn)度指示器,根據(jù)下載進(jìn)度來(lái)更新它
- 2.擴(kuò)展環(huán),向內(nèi)向外擴(kuò)展這個(gè)環(huán),中間擴(kuò)展的時(shí)候,去掉這個(gè)遮蓋
一.環(huán)形進(jìn)度指示器
1.自定義View繼承UIView,命名為CircularLoaderView.swift,此View將用來(lái)保存動(dòng)畫(huà)的代碼
2.創(chuàng)建CAShapeLayer
let circlePathLayer = CAShapeLayer()
let circleRadius: CGFloat = 20.0
3.初始化CAShapeLayer
// 兩個(gè)初始化方法都調(diào)用configure方法
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder : aDecoder)
configure()
}
// 初始化代碼來(lái)配置這個(gè)shape layer:
func configure(){
circlePathLayer.frame = bounds
circlePathLayer.lineWidth = 2.0
circlePathLayer.fillColor = UIColor.clear.cgColor
circlePathLayer.strokeColor = UIColor.red.cgColor
layer.addSublayer(circlePathLayer)
backgroundColor = .white
progress = 0.0
}
4.設(shè)置環(huán)形進(jìn)度條的矩形frame
// 小矩形的frame
func circleFrame() -> CGRect {
var circleFrame = CGRect(x: 0, y: 0, width: 2*circleRadius, height: 2*circleRadius)
let circlePathBounds = circlePathLayer.bounds
circleFrame.origin.x = circlePathBounds.midX - circleFrame.midX
circleFrame.origin.y = circlePathBounds.midY - circleFrame.midY
return circleFrame
}
可以參考下圖,理解這個(gè)circleFrame
5.每次自定義的這個(gè)view的size改變時(shí)谨湘,你都需要重新計(jì)算circleFrame,所以要將它放在一個(gè)獨(dú)立的方法,方便調(diào)用
// 通過(guò)一個(gè)矩形(正方形)繪制橢圓(圓形)路徑
func circlePath() -> UIBezierPath {
return UIBezierPath.init(ovalIn: circleFrame())
}
6.由于layers
沒(méi)有autoresizingMask
這個(gè)屬性趟佃,你需要在layoutSubviews
方法中更新circlePathLayer
的rame
來(lái)恰當(dāng)?shù)仨憫?yīng)view
的size
變化
override func layoutSubviews() {
super.layoutSubviews()
circlePathLayer.frame = bounds
circlePathLayer.path = circlePath().cgPath
}
7.給CircularLoaderView.swift
文件添加一個(gè)CGFloat
類型屬性,自定義的setter
和getter
方法,setter
方法驗(yàn)證輸入值要在0到1之間鸽素,然后賦值給layer
的strokeEnd
屬性泄隔。
var progress : CGFloat{
get{
return circlePathLayer.strokeEnd
}
set{
if (newValue > 1) {
circlePathLayer.strokeEnd = 1
}else if(newValue < 0){
circlePathLayer.strokeEnd = 0
}else{
circlePathLayer.strokeEnd = newValue
}
}
}
8.利用Kingfisher
,在image下載回調(diào)方法中更新progress.
此處是自定義ImageView,在storyboard中拖個(gè)ImageView,設(shè)置為自定義的ImageView類型,在這個(gè)ImageView初始化的時(shí)候就會(huì)調(diào)用下面的代碼
class CustomImageView: UIImageView {
// 創(chuàng)建一個(gè)實(shí)例對(duì)象
let progressIndicatorView = CircularLoaderView(frame: CGRect.zero)
// 從xib中加載會(huì)走這個(gè)方法
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
addSubview(progressIndicatorView)
progressIndicatorView.frame = bounds
let url = URL.init(string: "https://koenig-media.raywenderlich.com/uploads/2015/02/mac-glasses.jpeg")
self.kf.setImage(with: url, placeholder: nil, options: nil, progressBlock: { [weak self] (reseivdSize, expectedSize) in
self?.progressIndicatorView.progress = CGFloat(reseivdSize) / CGFloat(expectedSize)
}) { [weak self] (image, error, _, _) in
self?.progressIndicatorView.reveal()
}
}
}
二.擴(kuò)展這個(gè)環(huán)
仔細(xì)看,此處是兩個(gè)動(dòng)畫(huà)一起執(zhí)行,1是向外擴(kuò)展2.是向內(nèi)擴(kuò)展.但可以用一個(gè)Bezier path完成此動(dòng)畫(huà),需要用到組動(dòng)畫(huà).
- 1.增加圓的半徑(path屬性)來(lái)向外擴(kuò)展
- 2.同時(shí)增加line的寬度(lineWidth屬性)來(lái)使環(huán)更加厚和向內(nèi)擴(kuò)展
func reveal() {
// 背景透明图张,那么藏著后面的imageView將顯示出來(lái)
backgroundColor = .clear
progress = 1.0
// 移除隱式動(dòng)畫(huà),否則干擾reveal animation
circlePathLayer.removeAnimation(forKey: "strokenEnd")
// 從它的superLayer 移除circlePathLayer ,然后賦值給super的layer mask
circlePathLayer.removeFromSuperlayer()
// 通過(guò)這個(gè)這個(gè)circlePathLayer 的mask hole動(dòng)畫(huà) ,image 逐漸可見(jiàn)
superview?.layer.mask = circlePathLayer
// 1 求出最終形狀
let center = CGPoint(x:bounds.midX,y: bounds.midY)
let finalRadius = sqrt((center.x*center.x) + (center.y*center.y))
let radiusInset = finalRadius - circleRadius
let outerRect = circleFrame().insetBy(dx: -radiusInset, dy: -radiusInset)
// CAShapeLayer mask最終形狀
let toPath = UIBezierPath.init(ovalIn: outerRect).cgPath
// 2 初始值
let fromPath = circlePathLayer.path
let fromLineWidth = circlePathLayer.lineWidth
// 3 最終值
CATransaction.begin()
// 防止動(dòng)畫(huà)完成跳回原始值
CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions)
circlePathLayer.lineWidth = 2 * finalRadius
circlePathLayer.path = toPath
CATransaction.commit()
// 4 路徑動(dòng)畫(huà),lineWidth動(dòng)畫(huà)
let lineWidthAnimation = CABasicAnimation(keyPath: "lineWidth")
lineWidthAnimation.fromValue = fromLineWidth
lineWidthAnimation.toValue = 2*finalRadius
let pathAnimation = CABasicAnimation(keyPath: "path")
pathAnimation.fromValue = fromPath
pathAnimation.toValue = toPath
// 5 組動(dòng)畫(huà)
let groupAnimation = CAAnimationGroup()
groupAnimation.duration = 1
groupAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
groupAnimation.animations = [pathAnimation ,lineWidthAnimation]
groupAnimation.delegate = self
circlePathLayer.add(groupAnimation, forKey: "strokeWidth")
}
三.監(jiān)聽(tīng)動(dòng)畫(huà)的結(jié)束
extension CircularLoaderView :CAAnimationDelegate {
// 移除mask
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
superview?.layer.mask = nil
}
}
示例下載地址github
原文地址 Rounak Jain
參考地址