所聞所獲1:動畫效果的進度球(水面上升、水面湧動)

動畫效果的進度球的效果如下圖,水面會有上升動畫、湧動動畫效果:


 

(1)、進度球的大致實現原理是這樣的:新建一個view並把它切割成圓形,在這個view 裏面畫兩條sin曲線,並且將曲線下放部分填滿色,然後新開兩條線程分別做曲線上升的效果和曲線平移變動弧度的效果,這樣就完成了帶動畫效果的進度球。項目代碼可在下方鏈接下載:

http://download.csdn.net/download/shayneyeorg/8865895

 

(2)、具體代碼抽取如下:

首先在viewController中初始化流量球並傳遞給它上升的高度和是否動畫:

複製代碼
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor darkGrayColor];
    
    //定義圓形所對應的矩形的寬度
    CGFloat width = 200;
    //定義這個矩形的y點的值
    CGFloat originY = 140;
    
    //將進度圖對象初始化在這個矩形上
    WaveGraph *wave = [[WaveGraph alloc]initWithFrame:CGRectMake((self.view.bounds.size.width-width)/2, originY, width, width)];
    
    //開始動作,第一個參數就是水面高度和圓形高度的百分比對應的小數數值
    [wave setProgress:0.7 animated:YES];
    
    [self.view addSubview:wave];
}
複製代碼

 

(3)、接下來看一看WaveGraph類的內容,它首先定義了以下的屬性和方法:

複製代碼
@interface WaveGraph : UIView
@property (nonatomic, assign) CGFloat progress;//一個介於0到1的數字,表示流量剩餘情況,決定水面的高度百分比

- (void)setProgress:(CGFloat)progress animated:(BOOL)animated; 
@end

@interface WaveGraph() {
    float drawProgress; //畫的高度,這個數值一直在增加,從0增加到進度球水量應有的高度
    BOOL isProgressing; //水面是否處在上升的過程
    float waveArc; //水面曲線的弧度
    float waveHorizMove; //水面曲線的水平偏移距離參數
    BOOL waveArcChange; //水面曲線弧度是否變動的標誌
}

@property (nonatomic, strong) NSThread *progressThread; //水面上升的線程
@property (nonatomic, strong) NSThread *waveThread;  //水面湧動的線程
@end
複製代碼

 

(4)、在viewController中調用到了WaveGraph類的initWithFrame:方法:

複製代碼
#pragma mark - 初始化方法

-(instancetype)initWithFrame:(CGRect)frame{
    self = [super initWithFrame:frame];
    self.backgroundColor = [UIColor whiteColor];

    if (self) {
        waveArc = 1.5; //曲線的弧度
        waveHorizMove = 2; //曲線的水平偏移距離參數
    }
    return self;
}
複製代碼

 

(5)、在-initWithFrame:方法後會自動執行drawRect:方法,在動畫過程中,畫面每一次微小的變動,都是調用drawRect:方法重畫,無數次重畫最終呈現出動畫的效果。drawRect:方法的代碼如下:

複製代碼
#pragma mark - drawRect

-(void)drawRect:(CGRect)rect{
    //上下文
    CGContextRef context = UIGraphicsGetCurrentContext();

    //path句柄
    CGMutablePathRef path1 = CGPathCreateMutable();
    CGMutablePathRef path2 = CGPathCreateMutable();

    //線寬
    CGContextSetLineWidth(context, 1);

    //drwaProgress是畫的高度百分比,這個變量會一直變動,直到增加到等於最終的高度百分比progress
    float waveHeight = rect.size.height*(1-drawProgress);

    //所以y就是上方空白部分的高度的百分比乘於頁面高度,即是畫線起點的y值
    float y = waveHeight;

    //設置起點
    CGPathMoveToPoint(path1, NULL, 0, y);
    CGPathMoveToPoint(path2, NULL, 0, y);

    if (y == 0 || y == rect.size.height) {
        //如果畫線起點的y值爲0或和頁面高度一樣,那麼就直接從左到右畫一條直線
        CGPathAddLineToPoint(path1, nil, rect.size.width, y);
        CGPathAddLineToPoint(path2, nil, rect.size.width, y);

    } else {
        //如果y值在頁面之內
        for (float x=0; x<=rect.size.width; x++){
            //從左往右一個點一個點地畫,畫x條直線
            //使用sin函數畫水面曲線
            y = waveArc * sin(x/rect.size.width*M_PI - 4*waveHorizMove/M_PI) * 3 + waveHeight;
            CGPathAddLineToPoint(path1, nil, x, y);
            y = waveArc * sin(x/rect.size.width*M_PI + 4*waveHorizMove/M_PI) * 5 + waveHeight;

            CGPathAddLineToPoint(path2, nil, x, y);
        }
    }

    //再畫3條線,從曲線右端到底部繞一圈回到曲線左端
    CGPathAddLineToPoint(path1, nil, rect.size.width, rect.size.height);
    CGPathAddLineToPoint(path1, nil, 0, rect.size.height);
    CGPathAddLineToPoint(path1, nil, 0, waveHeight);

    //路徑填色
    CGContextSetFillColorWithColor(context, [[UIColor grayColor] CGColor]);

    //將路徑添加到上下文中
    CGContextAddPath(context, path1);

    //區域填色
    CGContextFillPath(context);

    //畫出來
    CGContextDrawPath(context, kCGPathStroke);

    //和path1一樣處理
    CGPathAddLineToPoint(path2, nil, rect.size.width, rect.size.height);
    CGPathAddLineToPoint(path2, nil, 0, rect.size.height);
    CGPathAddLineToPoint(path2, nil, 0, waveHeight);

    CGContextSetFillColorWithColor(context, [[UIColor blueColor] CGColor]);
    CGContextAddPath(context, path2);
    CGContextFillPath(context);
    CGContextDrawPath(context, kCGPathStroke);

    CGPathRelease(path1);
    CGPathRelease(path2);
}
複製代碼

 

(6)、在viewController中還調用到了WaveGraph類的setProgress: animated:方法,這個方法就是生成動畫效果的方法了:

複製代碼
#pragma mark - 動畫的主方法

//設置流量球高度並做動畫,傳進來的參數是水面最終的高度
- (void)setProgress:(CGFloat)progress animated:(BOOL)animated {
    //將矩形切成一個圓形。setShape:方法詳見後方,而circleShape:方法是寫在UIBezierPath分類裏的自定義方法,由於離題較遠,這裏不敷述,詳見代碼
    [self setShape:[UIBezierPath circleShape:self.frame].CGPath];

    //處理progress小於0和大於1的情況
    CGFloat pinnedProgress = MIN(MAX(progress, 0.f), 1.f);

    int i = (int)(pinnedProgress*100);//將progress乘於100得到一個自然數i,後面會在0到i的遞增的代碼段裏實現水面逐步上升的效果
    _progress = pinnedProgress;

if (animated) {
        //如果需要動畫效果
        [self cancelProgressThread];
        isProgressing = YES;//水面是否處在上升的過程
        drawProgress = 0;//從0開始上升
        [self setNeedsDisplay];//調用drawRect:

        //另開一條線程,讓水面上升
        self.progressThread = [[NSThread alloc] initWithTarget:self selector:@selector(animateProgress) object:nil];
        [self.progressThread start];

    } else {
        //不需要上升動作,水面高度就直接用最後高度
        isProgressing = NO;
        [self onMainChangeProgress:[NSNumber numberWithInt:i]];
    }
}
複製代碼

 

(7)、在(6)中的setProgress: animated:方法裏,調用到了setShape:方法來切割出一個圓形,setShape:方法的內容如下:

複製代碼
#pragma mark - 設置圓形的方法

- (void)setShape:(CGPathRef)shape {
    //傳進來的參數是一個圓形了
    if (shape == nil) {
        self.layer.mask = nil;
    }

    //設置遮罩層爲圓形
    CAShapeLayer *maskLayer = [CAShapeLayer layer];
    maskLayer.path = shape;
    self.layer.mask = maskLayer;
}
複製代碼

 

(8)、在(6)中的setProgress: animated:方法裏,在新開線程裏調用了實現水面動畫上升的animateProgress方法:

複製代碼
#pragma mark - 水面上升的線程方法

//水面上升的動畫線程
- (void)animateProgress {
    //乘上一個100得到一個數字,用來計數
    int num = (int)(self.progress*100);
    int i = 0;

    //從i到num,水面逐步上升
    while (isProgressing) {
        if ([[NSThread currentThread] isCancelled]) {
            [NSThread exit];
        }
        if (i == num) {
            //高度到了就把上升標識去掉
            isProgressing = NO;
            [NSThread sleepForTimeInterval:0.03];
            [self performSelectorOnMainThread:@selector(onMainChangeProgress:) withObject:[NSNumber numberWithInteger:i] waitUntilDone:YES];
            break;

        } else {
            i ++;
        }

        [NSThread sleepForTimeInterval:0.03];
        [self performSelectorOnMainThread:@selector(onMainChangeProgress:) withObject:[NSNumber numberWithInt:i] waitUntilDone:NO];
    }
}

- (void)cancelProgressThread {
    if (self.progressThread && self.progressThread.isExecuting) {
        [self.progressThread cancel];
        self.progressThread = nil;
    }
}
複製代碼

 

(9)、可以看到,每一計算出水面的高度之後,都會調用onMainChangeProgress:方法重新畫圖,這個方法是最主要的畫圖方法。

其實在(6)中的setProgress: animated:方法裏,不需動畫的代碼段裏,就已經直接調用了onMainChangeProgress:方法了,忽略了上升過程的代碼。

onMainChangeProgress:方法的代碼如下:

複製代碼
//畫的方法
- (void)onMainChangeProgress:(NSNumber *)number {
    @synchronized(self) {
        int i = [number intValue];
        //改一下高度百分比,然後讓曲線重新畫
        drawProgress = (float)i/100;
        [self setNeedsDisplay];

        //當波浪動作線程是空的或者波浪線程並非正在執行,就新開線程做湧動
        if (!self.waveThread || !self.waveThread.isExecuting) {
            //水面湧動的線程
            self.waveThread = [[NSThread alloc] initWithTarget:self selector:@selector(animateWave) object:nil];
            [self.waveThread start];
        }

        if (!isProgressing) {
            //這個方法決定了在水面達到最終高度之後還要湧動多久
            [self performSelector:@selector(cancelWaveThread) withObject:nil afterDelay:300.f];
        }
    }
}
複製代碼

 

(10)、水面湧動線程有關的方法如下:

複製代碼
#pragma mark - 水面湧動的線程方法

//波浪湧動
- (void)animateWave {
    while (TRUE) {
        if ([[NSThread currentThread] isCancelled]) {
            [NSThread exit];
        }

        //要回到主線程修改ui
        [self performSelectorOnMainThread:@selector(onMainWave) withObject:nil waitUntilDone:NO];

        //這個暫停時間決定了水面湧動的速度
        [NSThread sleepForTimeInterval:0.03];
    }
}

//每改動一次,就重新畫一次,每次畫完會暫停0.03秒(在animateWave方法裏)
- (void)onMainWave {
    if (waveArcChange) {
        waveArc += 0.01;
    }else{

        waveArc -= 0.01;
    }

    if (waveArc<=1) {
        waveArcChange = YES;
    }

    if (waveArc>=1.5) {
        waveArcChange = NO;
    }

    waveHorizMove+=0.1;//讓曲線水平移動,造成水湧動的效果

    [self setNeedsDisplay];
}

- (void)cancelWaveThread {
    if (self.waveThread && self.waveThread.isExecuting) {
        [self.waveThread cancel];
        self.waveThread = nil;
    }
}
複製代碼

 

(11)、至此就完成了帶動畫的進度球的功能。

 

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