基础知识
本文所用到的是CAKeyframeAnimation即关键帧动画,CAKeyframeAnimation和CABasicAnimation同为CAPropertyAnimation的子类,都可以通过keyPath来为view的某一属性实现常用动画。
不同的是,CABasicAnimation只能指定属性的起始值(fromValue)、最终值(toValue)和步进值(byValue);而CAKeyframeAnimation却可以指定属性的每一个关键值(values或path)、关键值对应的时间节点(keyTimes)以及在这些关键值之间所用的时间函数(timingFunctions)。
相比之下,CAKeyframeAnimation要更加的灵活多变,而使用CAKeyframeAnimation的关键就在于寻找合适的节点和时间。
引言
最近在看关于绘图和动画方面的东西,看到一些不错的效果也总是想如何才能实现,而本人平时用的手机是nokia,感觉wp系统的加载动画有点意思,所以也尝试做了一个实现。
其效果是一串小点陆续从左侧进入,在中间集合后再陆续从右侧滑出,实现效果如下。
思路与算法
从图中可以看出,动画的主体是五个圆点,而动画效果就是通过圆点位置的移动来实现的,基于这一点我也总结了几种方案。
- 最开始想到的是写出每个点的x坐标关于时间的函数,开始匀减速,中间匀速,最后是匀加速,需要注意的是每个点开始加载的间隔时间。
- 通过观察发现,在滑入和滑出的过程中几个点之间的距离大概呈等差数列状,但是比较困难的是第一点向右滑出后后续点得滑出如何处理。第二个方案不太靠谱。
- 同样经过观察发现,从左侧滑入和从右侧滑出时圆点之间的距离基本是一致的,而在中间集合时各点之间的距离也是一致的,只是在相聚和相离的过程中点之间距离有所不同。于是可以得出,点之间的距离可以分为最大和最小两种,而介于两者中间的距离则交给动画来实现。
最终采用了第三种方案,通过点间最大距离可以得到在屏幕上的关键分割点,而点在中间集合效果可以通过在中点左右增加关键点来实现。用到的参数如下:
- 加载视图宽度:width
- 点间最大距离:maxDistance
- 点数:ptCount
于是最初关键分割点数=width/maxDistance+1;
中点索引=width/2/maxDistance+1;
由于各个点加载时间有所延迟,在第一个点到达中间和右侧时都需要等待其它点,即在中点左右与终点位置都要增加(ptCount-1)个关键分割点,所以关键分割点数+=2*(ptCount -1 );
为了避免中心分割点位置超出其它分割点,可以让点间最小距离minDistance=maxDistance/ptCount;
某分割点是否中心分割点可以通过该点索引来计算:
int offset=pos-center;//pos起始值为1
若offset>=0 && offset 小于ptCount则为中心分割点。
各个分割点的x坐标则是以上各参数的函数计算值。
代码实现
动画中的各个小圆点只是用来显示图形而并不涉及操作,所以在此用自定义CALayer来创建。
@interface IPZPointLayer : CAShapeLayer
@end
@implementation IPZPointLayer
-(instancetype)init{
self=[super init];
if (self) {
self.bounds = CGRectMake(0, 0, 3, 3);
self.path = [UIBezierPath bezierPathWithOvalInRect:self.bounds].CGPath;
self.fillColor = [UIColor blackColor].CGColor;
}
return self;
}
@end
在view中定义动画,可以根据不同的加载点数和最大点间隔定义不同动画:
-(void) initAnimationByPtCount:(int)ptCount andMaxDistance:(int)maxDistance{
double width=self.bounds.size.width;
double height=self.bounds.size.height;
int posCount=2*(ptCount -1 ) +ceil(width/maxDistance)+1;
int center=floor(width/2/maxDistance)+1;
int minDistance=maxDistance/ptCount;
NSMutableArray *positions;
for (int i=0; i<ptCount; i++) {
positions=[NSMutableArray arrayWithCapacity:posCount];
for (int j=1; j<=posCount; j++) {
int pos=j-i;
if (pos<=1) {//第一分割点
[positions addObject:@(-1)];
continue;
}
int offset=pos-center;
if (offset<0) {//中点以前
pos =(pos-1) *maxDistance;
[positions addObject:@(pos)];
}else if (offset>=0 && offset <ptCount) {//中心分割点
if (offset<floor(ptCount/2)+1) {//中点前
pos=(center-1) *maxDistance +minDistance*( offset-floor(ptCount/2)-1);
[positions addObject:@(pos)];
}else{//中点后
pos=(center-1) *maxDistance +minDistance*( offset-floor(ptCount/2)-1);
[positions addObject:@(pos)];
}
}else{//中心分割点以后
pos=pos-ptCount;
pos *= maxDistance;
[positions addObject:@(pos)];
}
}
IPZPointLayer *firPT=[[IPZPointLayer alloc]init];
firPT.position=CGPointMake(-1, height/2);
[self.layer addSublayer:firPT];
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.keyPath = @"position.x";
animation.values = positions; //各级值
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
animation.duration = 3;
animation.repeatCount= HUGE_VALF;
[firPT addAnimation:animation forKey:@"load"];
}
}
使用动画,指定五个加载点以及最大距离为50;
[self initAnimationByPtCount:5 andMaxDuration:50];