注:视频中添加了动画效果
effect.gif之前已经进过的这里就不讲了
第一步 动画原理
原理:利用drawRect的不停重绘,制作动画效果
以下是方法解释,这里跟代码里相比,简化了一下,代码里更详细一些
//每一小块的角度(弧度值)
float perAngle = M_PI*2*self.currentProgress/self.subdivCount;
//当前开始角度
float currentStartAngle;
//当前结束角度
float currentOverAngle;
for (NSInteger i = 0; i<self.subdivCount; i++) {
//当前开始角度
currentStartAngle = self.startAngle+perAngle*i;
//当前结束角度
currentOverAngle = currentStartAngle + perAngle;
//添加路径
CGContextAddArc(ctx,
center.x,
center.y,
self.radius-self.strokeWidth/2.0,
currentStartAngle,
currentOverAngle,
self.isClockDirection);
//开始渲染绘制图形(画图)kCGPathFillStroke这个模式的意思是描边和填充部分都要绘制
/**
具体效果,自行尝试
kCGPathFill,//填充
kCGPathEOFill,//奇偶填充
kCGPathStroke,//描边
kCGPathFillStroke,//填充 描边
kCGPathEOFillStroke//奇偶填充 描边
*/
CGContextDrawPath(ctx, kCGPathFillStroke);
第二步 制作使用步骤
创建一个输入框和一个确定按钮在效果图上面
在输入框里输入进度值之后,按下确定按钮,即可
//在头上加个输入框测试动画功能。
UITextField *animationTestTF = [[UITextField alloc] initWithFrame:CGRectMake(0, 0, 100, 50)];
animationTestTF.keyboardType = UIKeyboardTypeDecimalPad;
[self.view addSubview:animationTestTF];
animationTestTF.backgroundColor = [UIColor groupTableViewBackgroundColor];
animationTestTF.center = CGPointMake(cpView.center.x-40, cpView.center.y-70-50);
self.animationTestTF = animationTestTF;
//确定输入进度值按钮
UIButton *sureBtn = [UIButton buttonWithType:UIButtonTypeCustom];
sureBtn.frame = CGRectMake(0, 0, 30, 30);
sureBtn.titleLabel.adjustsFontSizeToFitWidth = YES;
[sureBtn setTitle:@"sure" forState:UIControlStateNormal];
[sureBtn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
sureBtn.backgroundColor = [UIColor whiteColor];
[self.view addSubview:sureBtn];
sureBtn.center = CGPointMake(animationTestTF.frame.size.width+animationTestTF.frame.origin.x+40, animationTestTF.center.y);
[sureBtn addTarget:self action:@selector(sureThisProgress) forControlEvents:UIControlEventTouchUpInside];
//确定按键方法
- (void)sureThisProgress{
self.cpView.isNoAnimated = NO;
if (self.animationTestTF.text&&self.animationTestTF.text.length) {
//改变进度条的值
self.cpView.progress = self.animationTestTF.text.floatValue;
}
}
//点击屏幕,收回键盘
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[super touchesBegan:touches withEvent:event];
[self.view endEditing:YES];
}
第三步 CircleProgressView中动画效果相关属性与方法讲解
/**
不加动画, 默认为NO。
友情提示,如果你写的页面有类似滑动条的方法控制进度线性增减的画,就将此属性设置为YES,即不加动画。
*/
@property (nonatomic, assign) BOOL isNoAnimated;
/**
是否从上次数值开始动画。默认为YES,即每次都从上次动画结束开始动画。
*/
@property (nonatomic, assign) BOOL isStartFromLast;
/**
元素(小块)个数。默认为64,越高线条越平滑,性能越差。64就刚刚好,当然这个属性其实还是看项目的需求而设置的更高或更低。
*/
@property (nonatomic, assign) NSInteger subdivCount;
/**
动画时长。当animationSameTime为NO时,此属性为动画的最长时间,即progress=1时的动画时间。
*/
@property (nonatomic, assign) CGFloat animationDuration;
/**
动画是否同等时间。
为YES则不同进度动画时长都为animationDuration,
为NO则根据不同进度对应不同动画时长,进度最大时动画时长为animationDuration。
默认为YES
*/
@property (nonatomic, assign) BOOL animationSameTime;
//这部分是.m文件里的属性
/**
定时器,如果你是用UISlider控件来控制进度的话,那么只要在设置进度的时候,标记一下需要重绘,就会给人一种动画感。因为UISlider的值变化是连续变化。但如果你需要让进度直接从0.3->0.8就不行了,这个时候你就需要一个动画来给用户更好的体验。而CADisplayLink定时器这时就是你必不可少的动画利器在。他的优缺点此处不多言,自己上网查,一大堆。这里只说一句,CADisplayLink是基于屏幕刷新而制定的。
*/
@property (nonatomic, strong) CADisplayLink *playLink;
/**
当前进度,顾名思义,在动画过程中,每次屏幕刷新重绘时相对应的进度值
*/
@property (nonatomic, assign) CGFloat currentProgress;
/**
增加的进度度值,一句话,动画时每次屏幕刷新,当前进度所增加的值
*/
@property (nonatomic, assign) CGFloat increaseProgressValue;
/**
是否巅倒反转,进度值不单单会增长,也会减少。所以动画不单单需要前进增长,也要能后退缩短。
*/
@property (nonatomic, assign) BOOL isReverse;
这里是相关的初始设置,好吧,实然发现又没什么好讲的了,因为demo注释相当清楚。
//需要动画
_isNoAnimated = NO;
//新动画结束时,从上次动画结束的位置开始动画
_isStartFromLast = YES;
//动画时的被分割的块数
_subdivCount = 64;
//动画默认时长
_animationDuration = 2;
//不同动画的时长是否相同
_animationSameTime = YES;
这里只贴部分核心代码,demo里很详细,可以自己下着看
//每一小块的角度(弧度值)
float perAngle = M_PI*2*self.currentProgress/self.subdivCount;
//当前开始角度
float currentStartAngle;
//当前结束角度
float currentOverAngle;
for (NSInteger i = 0; i<self.subdivCount; i++) {
//当前开始角度
currentStartAngle = self.startAngle+perAngle*i;
//当前结束角度
currentOverAngle = currentStartAngle + perAngle;
//添加路径
CGContextAddArc(ctx,
center.x,
center.y,
self.radius-self.strokeWidth/2.0,
currentStartAngle,
currentOverAngle,
self.isClockDirection);
//开始渲染绘制图形(画图)kCGPathFillStroke这个模式的意思是描边和填充部分都要绘制
/**
具体效果,自行尝试
kCGPathFill,//填充
kCGPathEOFill,//奇偶填充
kCGPathStroke,//描边
kCGPathFillStroke,//填充 描边
kCGPathEOFillStroke//奇偶填充 描边
*/
CGContextDrawPath(ctx, kCGPathFillStroke);
//进度设置相关
//当前进度,如果要求从上次结束的动画开始的话,将当前进度设置为上次的进度值。否则设为0.0
self.currentProgress = self.isStartFromLast==YES?self.progress:0.0;
//对比传进来的进度值与当前进度,以确定动画是否为后退动画,不明白这句代码意思的,多看两遍,你就明白其中的逻辑了。
self.isReverse = progress<self.currentProgress?YES:NO;
//当前进度
_progress = progress;
if (self.isNoAnimated) {//不需要动画
//直接将当前进度值设置为本次进度值
self.currentProgress = self.progress;
//标记重绘,当屏幕刷新的时候会自动调取drawrect方法
/**
ios 屏幕刷新1秒60次。
*/
[self setNeedsDisplay];
}else {//需要动画
//这里要做的事就有点多了,慢慢看就懂了
CGFloat pinLv = 60;//屏幕每秒刷新的次数(有兴趣的可以了解一下)
//是否从上次结束开始动画
if (self.isStartFromLast) {//是(从上次结束开始动画)
if (self.animationSameTime) {
//这里注释说明下,要不然,逻辑就容易迷糊了
/**
首先,要记得在这里的都是需要动画的。其次如果前面的逻辑你理解清晰的话,那这里就容易理解多了。
self.progress - self.currentProgress,这个减式是为了求出,这一段动画的动画距离(弧度)。(有正负之分,关系到动画是前进还是后退)
pinLv*self.animationDuration这个是动画的时间,千万要理解,这个乘式的含义。接下来我们要用的是CADisplayLink定时器。一秒60次。如果直接用self.animationDuration作分母忘了乘以频率的话,呵呵……
*/
//每次动画增加的值
self.increaseProgressValue = (self.progress - self.currentProgress)/(pinLv*self.animationDuration);
}else {
//如果你能领会上面的代码这里你一定懂。如果想不通,可以看一下下面的CADisplayLink定时器相关方法
self.increaseProgressValue = self.isReverse == YES ? -1.0/(pinLv*_animationDuration):1.0/(pinLv*_animationDuration);
}
}else {//不是
//从0开始动画
if (self.animationSameTime) {
//理解了上面的代码你就懂
self.increaseProgressValue = self.progress/(pinLv*self.animationDuration);
}else {
//理上你懂
self.increaseProgressValue = 1.0/(pinLv*_animationDuration);
}
}
//重置定时器
if (self.playLink) {
[self.playLink invalidate];
self.playLink = nil;
}
//CADisplayLink定时器使用方法,不解释,自查,可以关注下preferredFramesPerSecond、frameInterval这两个属性(想优化代码性能的话)
CADisplayLink *playLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(countingAction)];
[playLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
self.playLink = playLink;
}
//定时器方法
- (void)countingAction{
//当前进度设置每次动画都要加一份增加值
self.currentProgress += self.increaseProgressValue;
[self setNeedsDisplay];
/**
判断动画是否该结束了
*/
if (self.isStartFromLast) {
if (self.isReverse) {//倒退动画
if (self.currentProgress <= self.progress) {//当前进度值小于等于目标进度值时结束
[self dealWithLast];
}
} else {
if (self.currentProgress >= self.progress) {//当前进度值大于等于目标进度值时结束
[self dealWithLast];
}
}
} else {
if (self.currentProgress >= self.progress) {//当前进度值大于等于目标进度值时结束
[self dealWithLast];
}
}
}
//最后一次动画内容
- (void)dealWithLast {
//调节一下进度值
self.currentProgress = self.progress;
//注销定时器
[self.playLink invalidate];
self.playLink = nil;
//标记刷新(动画增长)
[self setNeedsDisplay];
}
结语:
逻辑很清晰,但看起代码来,可能还是会头大,不过不用担心,因为我代码注释写的很清楚,还是中文的。