文章目錄
- 一、 寫在前面
- 二、 CABasicAnimation的使用
- 三、 實現簡單的進度條功能
- 3.1 實現思路
- 3.2 實現步驟
- 3.2.1 自定義一個UIView的子類
- 3.2.2 重寫成員屬性progress的setter
- 3.2.3 核心部分:重寫- (void)drawRect:(CGRect)rect
- 四、 滑動條UISlider的使用方法
- 五、圓形進度條、帶漸變(帶demo)
- 5.1 畫圓:CAShapeLayer與UIBezierPath配合使用
- 5.2 添加顏色漸變層CAGradientLayer
- 5.3 設置遮罩層
- 5.4 設置遮罩layer:_progressLayer 的動畫
- 六、 主要代碼
- 七、 自我總結
一、 寫在前面
根據需求,需要實現一個圓形的進度條,根據當前程序進行的進度來實現進度條的狀態。文章最後會提供demo。
二、 CABasicAnimation的使用
2.1 基本介紹
CABasicAnimation提供了最基礎的動畫屬性設置,是簡單的keyframe動畫性能。CABasicAnimation可以看做是一種CAKeyframeAnimation的簡單動畫,因爲它只有頭尾的關鍵幀(keyframe)。
我們可以創建一個CABasicAnimaiton的對象通過keyPath的方式。CABasicAnimation提供了fromValue、toValue、byValue的設置(插值)。它們三個屬性定義了一個動畫的軌跡,並且最少兩個值不能爲空。
當設置了CABasicAnimation的起點與終點值後,中間的值都是通過插值方式計算出來的,插值計算是通過timingFunction來指定,timingFunction默認爲空,使用liner(勻速運動)。例如,當我們設置了一個position的動畫,設置了開始值PointA與結束值PointB,它們的運動先計算PointA與PointB的中間運動值PointCenter,而PointCenter是由timingFunction來指定值的,並且動畫默認是直線勻速運動的。
2.2 實例化
使用方法animationWithKeyPath:
對 CABasicAnimation進行實例化,並指定Layer的屬性作爲關鍵路徑進行註冊。
CABasicAnimation *pathAnimation=[CABasicAnimation animationWithKeyPath:@"strokeEnd"];
2.3 設定動畫
屬性 | 說明 |
---|---|
duration | 動畫的時長 |
repeatCount | 重複的次數。不停重複設置爲 HUGE_VALF |
repeatDuration | 設置動畫的時間。在該時間內動畫一直執行,不計次數。 |
beginTime | 指定動畫開始的時間。從開始延遲幾秒的話,設置爲【CACurrentMediaTime() + 秒數】 的方式 |
timingFunction | 設置動畫的速度變化 |
autoreverses | 動畫結束時是否執行逆動畫 |
fromValue | 所改變屬性的起始值 |
toValue | 所改變屬性的結束時的值 |
byValue | 所改變屬性相同起始值的改變量 |
例如:
pathAnimation.duration = 1;
pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
// 開始位置
pathAnimation.fromValue = [NSNumber numberWithFloat:start];
// 過程中的位置,即到什麼位置結束
pathAnimation.toValue = [NSNumber numberWithFloat:pro];
// 插入值
//pathAnimation.byValue = [NSNumber numberWithFloat:0.5];
2.3.1 使用方法functionWithName
kCAMediaTimingFunctionLinear
在整個動畫時間內動畫都是以一個相同的速度來改變。也就是勻速運動。
kCAMediaTimingFunctionEaseIn
動畫開始時會較慢,之後動畫會加速。
kCAMediaTimingFunctionEaseOut
動畫在開始時會較快,之後動畫速度減慢。
kCAMediaTimingFunctionEaseInEaseOut
動畫在開始和結束時速度較慢,中間時間段內速度較快。
2.4 防止動畫結束後回到初始狀態
只需設置removedOnCompletion
、fillMode
兩個屬性就可以了。
transformAnima.removedOnCompletion = NO;
transformAnima.fillMode = kCAFillModeForwards;
2.4.1 fillMode屬性的理解
該屬性定義了你的動畫在開始和結束時的動作。默認值是 kCAFillModeRemoved
。
取值的解釋:
kCAFillModeRemoved
動畫將在設置的 beginTime 開始執行(如沒有設置beginTime屬性,則動畫立即執行),動畫執行完成後將會layer的改變恢復原狀。
kCAFillModeForwards
動畫即使之後layer的狀態將保持在動畫的最後一幀,而removedOnCompletion的默認屬性值是 YES,所以爲了使動畫結束之後layer保持結束狀態,應將removedOnCompletion設置爲NO。
kCAFillModeBackwards
設置爲該值,將會立即執行動畫的第一幀,不論是否設置了 beginTime屬性。觀察發現,設置該值,剛開始視圖不見,還不知道應用在哪裏。
kCAFillModeBoth
該值是 kCAFillModeForwards 和 kCAFillModeBackwards的組合狀態
解釋:爲什麼動畫結束後返回原狀態?
首先我們需要搞明白一點的是,layer動畫運行的過程是怎樣的?其實在我們給一個視圖添加layer動畫時,真正移動並不是我們的視圖本身,而是 presentation layer 的一個緩存。動畫開始時 presentation layer開始移動,原始layer隱藏,動畫結束時,presentation layer從屏幕上移除,原始layer顯示。這就解釋了爲什麼我們的視圖在動畫結束後又回到了原來的狀態,因爲它根本就沒動過。
這個同樣也可以解釋爲什麼在動畫移動過程中,我們爲何不能對其進行任何操作。
所以在我們完成layer動畫之後,最好將我們的layer屬性設置爲我們最終狀態的屬性,然後將presentation layer 移除掉。
2.5 其他的一些設置屬性
repeatCount
設置動畫的執行次數
autoreverses
默認值爲 NO,將其設置爲 YES
speed
改變動畫的速度 可以直接設置動畫上的speed屬性,這樣只有這個動畫速度。
animation.speed = 2;
或者在layer上設置speed屬性,這樣在該視圖上的所有動畫都提速,該視圖上的所有子視圖上的動畫也會提速。
speed兩點需注意的:
(1) 如果設置動畫時間爲4s,speed設置爲2,則動畫只需2s即可執行完。
(2)如果同時設置了動畫的speed和layer 的speed,則實際的speed爲兩者相乘。
2.6 使用總結
- 在動畫執行完成之後,最好還是將動畫移除掉。也就是儘量不要設置removedOnCompletion屬性爲NO
- fillMode儘量取默認值就好了,不要去設置它的值。只有在極個別的情況下我們會修改它的值,以後會說到,這裏先佔個坑。
- 解決有時視圖會閃動一下的問題,我們可以將layer的屬性值設置爲我們的動畫最後要達到的值,然後再給我們的視圖添加layer動畫。
三、 實現簡單的進度條功能
3.1 實現思路
1、要實現繪圖,通常需要自定義一個UIView的子類,重寫父類的- (void)drawRect:(CGRect)rect方法,在該方法中實現繪圖操作
2、效果圖所示的效果其實是繪製一個圓弧,動態的改變終點的位置,最終達到一個封閉的圓。
3.2 實現步驟
3.2.1 自定義一個UIView的子類
//提供一個成員屬性,接收進度值
@property (nonatomic, assign) CGFloat progress;
3.2.2 重寫成員屬性progress的setter
//每次改變成員屬性progress的值,就會調用它的setter
- (void)setProgress:(CGFloat)progress
{
//調用其他的自定函數操作,比如進度條的動畫繪製
// 僅用於效果
_progress = progress;
//當下載進度改變時,手動調用重繪方法
[self setNeedsDisplay];
}
3.2.3 核心部分:重寫- (void)drawRect:(CGRect)rect
- (void)drawRect:(CGRect)rect
{
//設置圓弧的半徑
CGFloat radius = rect.size.width * 0.5;
//設置圓弧的圓心
CGPoint center = CGPointMake(radius, radius);
//設置圓弧的開始的角度(弧度制)
CGFloat startAngle = - M_PI_2;
//設置圓弧的終止角度
CGFloat endAngle = - M_PI_2 + 2 * M_PI * self.progress;
//使用UIBezierPath類繪製圓弧
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:center radius:radius - 5 startAngle:startAngle endAngle:endAngle clockwise:YES];
//將繪製的圓弧渲染到圖層上(即顯示出來)
[path stroke];
}
四、 滑動條UISlider的使用方法
4.1 創建滑動條
// 滑動條slider
UISlider *slider = [[UISlider alloc] initWithFrame:CGRectMake((SCREENWIDTH - 150) / 2, 200, 150, 20)];
[self.view addSubview:slider];
4.2 設定滑動條屬性
// 滑動條slider
UISlider *slider = [[UISlider alloc] initWithFrame:CGRectMake((SCREENWIDTH - 150) / 2, 200, 150, 20)];
slider.minimumValue = 9;// 設置最小值
slider.maximumValue = 11;// 設置最大值
slider.value = (slider.minimumValue + slider.maximumValue) / 2;// 設置初始值
slider.continuous = YES;// 設置可連續變化
// slider.minimumTrackTintColor = [UIColor greenColor]; //滑輪左邊顏色,如果設置了左邊的圖片就不會顯示
// slider.maximumTrackTintColor = [UIColor redColor]; //滑輪右邊顏色,如果設置了右邊的圖片就不會顯示
// slider.thumbTintColor = [UIColor redColor];//設置了滑輪的顏色,如果設置了滑輪的樣式圖片就不會顯示
[slider addTarget:self action:@selector(sliderValueChanged:) forControlEvents:UIControlEventValueChanged];// 針對值變化添加響應方法
[self.view addSubview:slider];
如上所示,在代碼中,我們設置了最大值、最小值、當前值。也可以改變滑動條左邊、右邊一集滑塊本身的顏色,不過我們這裏採用默認的設置,更改方法代碼中也寫了。
slider.continuous = YES
; 設爲YES後,我們才能在拖動滑塊的過程中持續獲取其值變更事件,如果是NO,則只有在滑動停止時纔會獲取變更事件。
4.3 拖動滑動條時的響應方法
// slider變動時改變label值
- (void)sliderValueChanged:(id)sender {
UISlider *slider = (UISlider *)sender;
// label需要自己創建
self.valueLabel.text = [NSString stringWithFormat:@"%.1f", slider.value];
}
五、圓形進度條、帶漸變(帶demo)
5.1 畫圓:CAShapeLayer與UIBezierPath配合使用
CAShapeLayer 是 CALayer 的子類,她比 CALayer 更靈活,可以畫出各種圖形,最簡單就是和UIBezierPath配合使用。
// 計算圓心位置
CGPoint arcCenter = CGPointMake(frame.size.width/2, frame.size.width/2);
// 計算半徑時候需要結合上線寬纔行,這個問題困擾了一會
CGFloat radius = (frame.size.width - PROGRESS_LINE_WIDTH) / 2;
// 圓形路徑
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:arcCenter
radius:radius
startAngle:M_PI_2
endAngle:M_PI*2+M_PI_2
clockwise:YES];
//CAShapeLayer
CAShapeLayer *shapLayer = [CAShapeLayer layer];
shapLayer.path = path.CGPath;
shapLayer.fillColor = [UIColor clearColor].CGColor;//圖形填充色
UIColor *grayColor = [UIColor colorWithRed:155/255.0 green:155/255.0 blue:155/255.0 alpha:0.8];
shapLayer.strokeColor = grayColor.CGColor;//邊線顏色
shapLayer.lineWidth = PROGRESS_LINE_WIDTH;
[self.layer addSublayer:shapLayer];
// 想看漸變效果的話可以把上面的這句話給註釋掉
5.2 添加顏色漸變層CAGradientLayer
CAGradientLayer是CALayer的子類,用來做漸變色的,用法請參考:
https://www.cnblogs.com/YouXianMing/p/3793913.html?utm_source=tuicool
這篇帖子中介紹的很詳細,就不做介紹了。
// 若漸變圖層兩個 漸變:RYUIColorWithRGB(140, 94, 0) >> RYUIColorWithRGB(229, 168, 46) >> RYUIColorWithRGB(140, 94, 0)
CALayer * grain = [CALayer layer];
[self.layer addSublayer:grain];
//採用一個漸變底層,若用兩個,注意底層的大小問題,一定要平分
CAGradientLayer * gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = CGRectMake(0, 0, frame.size.width, frame.size.height);
// 顏色分配
[gradientLayer setColors:[NSArray arrayWithObjects:
(id)[UIColorWithRGBStart CGColor],
(id)[UIColorWithRGBEnd CGColor], nil]];
[gradientLayer setLocations:@[@0.3,@1]];// 顏色分割線
[gradientLayer setStartPoint:CGPointMake(0, 0)];// 起始點
[gradientLayer setEndPoint:CGPointMake(1, 1)];// 結束點
[grain addSublayer:gradientLayer];
5.3 設置遮罩層
layer的遮罩層介紹: 設置遮罩層:CALayer的mask屬性,設置遮罩層後,layer.mask = maskLayer;maskLayer透明的地方layer不顯示,maskLayer不透明的地方layer顯示。
5.3.1 未設置遮罩層時
UIView *bgView = [[UIView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)];
[self.view addSubview:bgView];
bgView.layer.borderColor = [UIColor blackColor].CGColor;
bgView.layer.borderWidth = 1;
//底層被遮罩的layer
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
[bgView.layer addSublayer:gradientLayer];
gradientLayer.frame = CGRectMake(0, 0, 100, 100);
gradientLayer.backgroundColor = [UIColor redColor].CGColor;
效果圖如下:
5.3.2 設置遮罩層後
//遮罩層
CALayer *maskLayer = [CALayer layer];
maskLayer.frame = CGRectMake(0, 10, 100, 10);
gradientLayer.mask = maskLayer;
效果圖:(由於未設置遮罩層顏色,故底層紅色layer不顯示)
遮罩層設置顏色後:
遮罩層有顏色的地方顯示下面的layer,透明的地方反而不顯示
maskLayer.backgroundColor = [UIColor blackColor].CGColor;
5.3.3 下面進入正題,設置底層漸變layer,即:grain的遮蓋層(主要)
//進度layer 即:遮蓋layer
_progressLayer = [CAShapeLayer layer];
[self.layer addSublayer:_progressLayer];
_progressLayer.path = path.CGPath;
_progressLayer.strokeColor = [UIColor blueColor].CGColor;
_progressLayer.fillColor = [[UIColor clearColor] CGColor];
_progressLayer.lineWidth = PROGRESS_LINE_WIDTH;
_progressLayer.strokeEnd = 0.f;
_progressLayer.strokeStart = 0.0f;
_firstTime = true;
grain.mask = _progressLayer;//設置遮蓋層
5.4 設置遮罩layer:_progressLayer 的動畫
結合前面的第二、三、四看。
- (void)setProgress:(float)progress {
// [self startAninationWithStart:self.start withPro:progress];
[self endAninationWithValue:progress];
}
// 此方法,實現的是規定開始與結束位置,實現一次性的繪製
- (void)startAninationWithStart:(CGFloat)start withPro:(CGFloat)pro
{
CABasicAnimation *pathAnimation=[CABasicAnimation animationWithKeyPath:@"strokeEnd"];
pathAnimation.duration = 1;
pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
// 開始位置
pathAnimation.fromValue = [NSNumber numberWithFloat:start];
// 過程中的位置,即到什麼位置結束
pathAnimation.toValue = [NSNumber numberWithFloat:pro];
// 插入值
//pathAnimation.byValue = [NSNumber numberWithFloat:0.5];
pathAnimation.autoreverses=NO;
pathAnimation.fillMode = kCAFillModeForwards;
pathAnimation.removedOnCompletion = NO;
pathAnimation.repeatCount = 1;
[_progressLayer addAnimation:pathAnimation forKey:@"strokeEndAnimation"];
}
// 此方法實現繪製過程中,實時定製繪製的終點
-(void)endAninationWithValue:(CGFloat)end
{
CABasicAnimation *pathAnimation=[CABasicAnimation animationWithKeyPath:@"strokeEnd"];
pathAnimation.duration = 1;
pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
if (_firstTime){
pathAnimation.fromValue = [NSNumber numberWithFloat:0];
} else {
pathAnimation.fromValue = [NSNumber numberWithFloat:_lastProgress];
}
// 插入值
// pathAnimation.byValue = [NSNumber numberWithFloat:end];
// 終點值
pathAnimation.toValue = [NSNumber numberWithFloat:end];
pathAnimation.autoreverses=NO;
pathAnimation.fillMode = kCAFillModeForwards;
pathAnimation.removedOnCompletion = NO;
pathAnimation.repeatCount = 1;
_lastProgress = end;
_firstTime = false;
[_progressLayer addAnimation:pathAnimation forKey:@"strokeEndAnimation"];
}
六、 主要代碼
6.1 cycleViewProgress.h
//
// cycleViewProgress.h
// CycleProgressBar
//
// Created by 孫明喆 on 2020/1/17.
// Copyright © 2019 孫明喆. All rights reserved.
//
#import <UIKit/UIKit.h>
#define PROGRESS_LINE_WIDTH 4 //弧線的寬度
// 設置漸變色RGB從(46, 201, 144)漸變到(21, 203, 210),如果不採用漸變色,將兩個色彩設爲一致即可
#define UIColorWithRGBStart [UIColor colorWithRed:46/255.0 green:201/255.0 blue:144/255.0 alpha:1]
#define UIColorWithRGBEnd [UIColor colorWithRed:21/255.0 green:203/255.0 blue:210/255.0 alpha:1]
@interface cycleViewProgress : UIView
@property(nonatomic,assign)float progress;
@end
6.2 cycleViewProgress.m
//
// cycleViewProgress.m
// CycleProgressBar
//
// Created by 孫明喆 on 2020/1/17.
// Copyright © 2019 孫明喆. All rights reserved.
//
#import "cycleViewProgress.h"
#define RYUIColorWithRGB(r,g,b) [UIColor colorWithRed:r/255.0 green:g/255.0 blue:b/255.0 alpha:1]
@interface cycleViewProgress()
@property(nonatomic,strong)CAShapeLayer * progressLayer;
@property(nonatomic,assign)float lastProgress;
@property(nonatomic,assign)BOOL firstTime;
@end
@implementation cycleViewProgress
-(instancetype)initWithFrame:(CGRect)frame
{
if (self=[super initWithFrame:frame])
{
CGPoint arcCenter = CGPointMake(frame.size.width/2, frame.size.width/2);
// 計算半徑時候需要結合上線寬纔行,這個問題困擾了一會
CGFloat radius = (frame.size.width - PROGRESS_LINE_WIDTH) / 2;
// 圓形路徑
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:arcCenter
radius:radius
startAngle:M_PI_2
endAngle:M_PI*2+M_PI_2
clockwise:YES];
//CAShapeLayer
CAShapeLayer *shapLayer = [CAShapeLayer layer];
shapLayer.path = path.CGPath;
shapLayer.fillColor = [UIColor clearColor].CGColor;//圖形填充色
UIColor *grayColor = [UIColor colorWithRed:155/255.0 green:155/255.0 blue:155/255.0 alpha:0.8];
shapLayer.strokeColor = grayColor.CGColor;//邊線顏色
shapLayer.lineWidth = PROGRESS_LINE_WIDTH;
[self.layer addSublayer:shapLayer];
//漸變圖層 漸變:RYUIColorWithRGB(140, 94, 0) >> RYUIColorWithRGB(229, 168, 46) >> RYUIColorWithRGB(140, 94, 0)
CALayer * grain = [CALayer layer];
[self.layer addSublayer:grain];
//採用一個漸變底層
CAGradientLayer * gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = CGRectMake(0, 0, frame.size.width, frame.size.height);
// 顏色分配
// [gradientLayer setColors:[NSArray arrayWithObjects:
// (id)[RYUIColorWithRGB(46, 201, 144) CGColor],
// (id)[RYUIColorWithRGB(21, 203, 210) CGColor], nil]];
// 顏色分配
[gradientLayer setColors:[NSArray arrayWithObjects:
(id)[UIColorWithRGBStart CGColor],
(id)[UIColorWithRGBEnd CGColor], nil]];
[gradientLayer setLocations:@[@0.3,@1]];// 顏色分割線
[gradientLayer setStartPoint:CGPointMake(0, 0)];// 起始點
[gradientLayer setEndPoint:CGPointMake(1, 1)];// 結束點
[grain addSublayer:gradientLayer];
//進度layer
_progressLayer = [CAShapeLayer layer];
[self.layer addSublayer:_progressLayer];
_progressLayer.path = path.CGPath;
_progressLayer.strokeColor = [UIColor blueColor].CGColor;
_progressLayer.fillColor = [[UIColor clearColor] CGColor];
_progressLayer.lineWidth = PROGRESS_LINE_WIDTH;
_progressLayer.strokeEnd = 0.f;
_progressLayer.strokeStart = 0.0f;
_firstTime = true;
grain.mask = _progressLayer;//設置遮蓋層
}
return self;
}
- (void)setProgress:(float)progress {
[self endAninationWithValue:progress];
}
// 此方法實現繪製過程中,實時定製繪製的終點
-(void)endAninationWithValue:(CGFloat)end
{
CABasicAnimation *pathAnimation=[CABasicAnimation animationWithKeyPath:@"strokeEnd"];
pathAnimation.duration = 1;
pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
if (_firstTime){
pathAnimation.fromValue = [NSNumber numberWithFloat:0];
} else {
pathAnimation.fromValue = [NSNumber numberWithFloat:_lastProgress];
}
// 插入值
// pathAnimation.byValue = [NSNumber numberWithFloat:end];
// 終點值
pathAnimation.toValue = [NSNumber numberWithFloat:end];
pathAnimation.autoreverses=NO;
pathAnimation.fillMode = kCAFillModeForwards;
pathAnimation.removedOnCompletion = NO;
pathAnimation.repeatCount = 1;
_lastProgress = end;
_firstTime = false;
[_progressLayer addAnimation:pathAnimation forKey:@"strokeEndAnimation"];
}
@end
6.3 demo
七、 自我總結
基本上總體上來學習了一下繪製的過程,對View的一些屬性和用法有更深入的瞭解了,這篇文章本來打算在1月結束前完成的,但是由於太多事情給耽擱了,過完年後也沒怎麼停下來,年前將文章的大體框架整理了出來,今天才完成這篇文章,上篇到目前爲止,也是框架整理好了。
總要走出自己的舒適圈,我不認爲自己有拖延症,但是自制力確實大不如前,2020年,是時候從頭再改變自己一次了,fighting!