iOS開發——UI環(圓)形進度條按進度進行繪製

一、 寫在前面

根據需求,需要實現一個圓形的進度條,根據當前程序進行的進度來實現進度條的狀態。文章最後會提供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 防止動畫結束後回到初始狀態

只需設置removedOnCompletionfillMode兩個屬性就可以了。

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

MyGithub

七、 自我總結

基本上總體上來學習了一下繪製的過程,對View的一些屬性和用法有更深入的瞭解了,這篇文章本來打算在1月結束前完成的,但是由於太多事情給耽擱了,過完年後也沒怎麼停下來,年前將文章的大體框架整理了出來,今天才完成這篇文章,上篇到目前爲止,也是框架整理好了。
總要走出自己的舒適圈,我不認爲自己有拖延症,但是自制力確實大不如前,2020年,是時候從頭再改變自己一次了,fighting!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章