iOS高级动画:圆形树展开&收起动画

前段时间帮某某做了一个动画效果,今天分享给大家。关于动画的基础知识,这里不会细说,如果您还没有核心动画的基础知识,请先阅读相关文章,了解核心动画如何使用,然后再继续阅读本篇文章。

本篇文章,涉及到以下知识点:

  • 如何添加缩放动画

  • 如何添加平移动画

  • 如何添加旋转动画

  • 如何添加关键帧动画

  • 如何使用组合动画

  • 如何实现渐变图层

  • 如何实现圆形渐变进度条

温馨提示:新手不适合阅读本篇文章哦,不过可以初步阅读了解一下!

如果没有了解过动画的基础知识,可先看看笔者之前的一些文章:

本篇文章不深入讲基础知识,只讲如何实现及实现的要点,并放出关键代码。对于伸手党,请不要私聊我要完整的源代码。如果您正好在项目中有这样的需求,可以尝试根据本篇文章讲解动手做一个!

最终效果图

无缩放动画的效果图:

tree4ani.gif

从动画效果可以看出来,有平移、旋转、关键帧动画,同时还有渐变进度条充满的动画。另外还要注意移动的距离。请忽略样式丑陋的问题~

有缩放动画的效果图:

tree6ani.gif

从动画效果可以看出来,个人变成了6个,且是平分的,比上面的效果图多了缩放的动画!这个缩放动画,生成GIF图的效果真丑,跟手机运行起来看到的差别比较大!

设计思路

这里是封装里了通用的组件,如果是在项目中使用,可以轻松调用且可以复用。从动画效果可以看到,这个整体是由以下几个部分组成的:

1)中间带进度图的圆形控件(这里叫叶子吧)

2)从中心圆出来到四周的N个控件,其中每个都是拥有同样的特性的

所以,我们设计成三个类,分别是:

  • HYBCurveItemView:代表散开的每个子项控件

  • HYBCurveMainView:代表中间的圆形控件

  • HYBCurveMenuView:由前两个控件组合而形成的整体控件

设计HYBCurveItemView

假设叫叶子。那么每个叶子就有相同的特性,需要知道自己的归属:

  • 自身的大小

  • 展示的元素

  • 可移动到最远的哪个位置

  • 可移动到最近的哪个位置

  • 起始位置

  • 最终展开后停留的位置

可设置以下几个位置属性:

1
2
3
4
@property (nonatomic, assign) CGPoint startPoint;
@property (nonatomic, assign) CGPoint endPoint;
@property (nonatomic, assign) CGPoint nearPoint;
@property (nonatomic, assign) CGPoint farPoint;

这个类只需要有相关属性即可!

设计HYBCurveMainView

假设中大圆。大圆主要需要属性以下特性:

  • 自身大小

  • 展示的元素

  • 带有渐变圆形进度条

可执行的操作:

  • 更新进度

  • 展开、收起叶子

对于这个大圆类,细读如何添加圆形进度条。

设计渐变进度圆环

首先,添加白色固定圆形

底部是一个白色的圆环,当白色圆环填满时,表示0%。那么白色圆环可以用什么来实现呢?其实CAShapeLayer就是非常好的,它可以添加圆形路径来实现的,记得设置填充色为透明哦,不然连中间的内容也看不见了。代码如下:

1
2
3
4
5
6
7
8
9
self.outLayer = [CAShapeLayer layer];
CGRect rect = {kLineWidth / 2, kLineWidth / 2, frame.size.width - kLineWidth, frame.size.height - kLineWidth};
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:rect];
self.outLayer.strokeColor = [UIColor whiteColor].CGColor;
self.outLayer.lineWidth = kLineWidth;
self.outLayer.fillColor =  [UIColor clearColor].CGColor;
self.outLayer.lineCap = kCALineCapRound;
self.outLayer.path = path.CGPath;
[self.layer addSublayer:self.outLayer];

因为要设置为白色圆环,所以画笔颜色设置为白色,线宽就设置为圆环的大小。这样就可以初步形成了带有白色圆环的底色了。此时就是0%。

其次,设置可调进度的进度圆环图层

下面所创建的图层,会用于设置渐变颜色图层的mask,这样才能显示中间的内容,而不是渐变的图层。它的大小与白底圆环一样大小:

1
2
3
4
5
6
7
self.progressLayer = [CAShapeLayer layer];
self.progressLayer.frame = self.bounds;
self.progressLayer.fillColor = [UIColor clearColor].CGColor;
self.progressLayer.strokeColor = [UIColor whiteColor].CGColor;
self.progressLayer.lineWidth = kLineWidth;
self.progressLayer.lineCap = kCALineCapRound;
self.progressLayer.path = path.CGPath;

然后,增加渐变图层

要实现渐变图层,可以通过CAGradientLayer来创建,这里的颜色是随意指定的,所以效果不太协调,大家可自由调整。这里呢使用了两个渐变图层,然后放到一个大的渐变图层中,两个小的图层各占一半。 当添加了mask后,就只有进度这一部分渐变可显示了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
CAGradientLayer *gradientLayer1 = [CAGradientLayer layer];
gradientLayer1.frame = CGRectMake(0, 0, width / 2, height);
CGColorRef red = [UIColor redColor].CGColor;
CGColorRef purple = [UIColor purpleColor].CGColor;
CGColorRef yellow = [UIColor yellowColor].CGColor;
CGColorRef orange = [UIColor orangeColor].CGColor;
[gradientLayer1 setColors:@[(__bridge id)red, (__bridge id)purple, (__bridge id)yellow, (__bridge id)orange]];
[gradientLayer1 setLocations:@[@0.3, @0.6, @0.8, @1.0]];
[gradientLayer1 setStartPoint:CGPointMake(0.5, 1)];
[gradientLayer1 setEndPoint:CGPointMake(0.5, 0)];  
CAGradientLayer *gradientLayer2 =  [CAGradientLayer layer];
gradientLayer2.frame = CGRectMake(width / 2, 0, width / 2, height);
CGColorRef blue = [UIColor brownColor].CGColor;
CGColorRef purple1 = [UIColor purpleColor].CGColor;
CGColorRef r1 = [UIColor yellowColor].CGColor;
CGColorRef o1 = [UIColor orangeColor].CGColor;
[gradientLayer2 setColors:@[(__bridge id)blue, (__bridge id)purple1, (__bridge id)r1, (__bridge id)o1]];
[gradientLayer2 setLocations:@[@0.3, @0.6, @0.8, @1.0]];
[gradientLayer2 setStartPoint:CGPointMake(0.5, 0)];
[gradientLayer2 setEndPoint:CGPointMake(0.5, 1)]; 
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = self.bounds;
[gradientLayer addSublayer:gradientLayer1];
[gradientLayer1 addSublayer:gradientLayer2];
gradientLayer.mask = self.progressLayer;
[self.layer addSublayer:gradientLayer];

注意,一定要设置gradientLayer.mask = self.progressLayer;这样才能显示中间的内容,如果不设置mask,那么就只有渐变图层了。

最后,动画更新进度

在需要更新进度的时候,可以调用这个API来更新进度,带有动画效果。

1
2
3
4
5
6
7
8
- (void)updateProgressWithNumber:(NSUInteger)number {
     [CATransaction begin];
     [CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn]];
     [CATransaction setAnimationDuration:0.5];
     self.progressLayer.strokeEnd =  number / 100.0;
     self.percentLabel.text = [NSString stringWithFormat:@ "%@%%" , @(number)];
     [CATransaction commit];
}

设计HYBCurveMenuView

这个类就是圆形菜单类了,整合前两个。它主要具备以下特性:

  • 自身大小

  • 是否添加缩放动画

  • 更换叶子

可执行的操作:

  • 展开、收起

  • 点击大圆回调

  • 更换叶子

当更换所有的叶子时,需要调整所有叶子的位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
- (void)setMenuItems:(NSArray *)menuItems {
   if  (_menuItems != menuItems) {
     _menuItems = menuItems;
     
     for  (UIView *v  in  self.subviews) {
       if  (v.tag >= 1000) {
         [v removeFromSuperview];
       }
     }
     
     // add the menu buttons
     int count = (int)[menuItems count];
     CGFloat cnt = 1;
     for  (int i = 0; i < count; i ++) {
       HYBCurveItemView *item = [menuItems objectAtIndex:i];
       item.tag = 1000 + i;
       item.startPoint = self.startPoint;
       CGFloat pi =  M_PI / count;
       CGFloat endRadius = item.bounds.size.width / 2 + self.endDistance + _mainView.bounds.size.width / 2;
       CGFloat nearRadius = item.bounds.size.width / 2 + self.nearDistance + _mainView.bounds.size.width / 2;
       CGFloat farRadius = item.bounds.size.width / 2 + self.farDistance + _mainView.bounds.size.width / 2;
       item.endPoint = CGPointMake(self.startPoint.x + endRadius * sinf(pi * cnt),
                                   self.startPoint.y - endRadius * cosf(pi * cnt));
       item.nearPoint = CGPointMake(self.startPoint.x + nearRadius * sinf(pi * cnt),
                                    self.startPoint.y - nearRadius * cosf(pi * cnt));
       item.farPoint = CGPointMake(self.startPoint.x + farRadius * sinf(pi * cnt),
                                   self.startPoint.y - farRadius * cosf(pi * cnt));
       item.center = item.startPoint;
       [self addSubview:item];
       
       cnt += 2;
     }
     
     [self bringSubviewToFront:_mainView];
   }
}

其中,这几个属性带有默认值(分别表示起点、最近点、最远点、展开后最终停留点):

1
2
3
4
5
self.startPoint = self.center;
// 修改这时的参数来调整大圆与圆之间的距离
self.nearDistance = 30;
self.farDistance = 60;
self.endDistance = 30;

对于上面的点的计算,主要是一点点的数学知识,需要懂得象限与角度的关系。

展开或者收起

调用下面的方法来展开或者收起。这里会遍历所有的叶子,让每个叶子都添加对应的动画变换,就可以看到动画轨迹了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)expend:(BOOL)isExpend {
   _isExpend = isExpend;
   
   [self.menuItems enumerateObjectsUsingBlock:^(HYBCurveItemView *obj, NSUInteger idx, BOOL * _Nonnull stop) {
     if  (self.scale) {
       if  (isExpend) {
         obj.transform = CGAffineTransformIdentity;
       else  {
         obj.transform = CGAffineTransformMakeScale(0.001, 0.001);
       }
     }
     
     [self addRotateAndPostisionForItem:obj toShow:isExpend];
   }];
}

接下来是最关键的动画核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
- (void)addRotateAndPostisionForItem:(HYBCurveItemView *)item toShow:(BOOL)show {
   if  (show) {
     CABasicAnimation *scaleAnimation = nil;
     if  (self.scale) {
       scaleAnimation = [CABasicAnimation animationWithKeyPath:@ "transform.scale" ];
       scaleAnimation.fromValue = [NSNumber numberWithFloat:0.2];
       scaleAnimation.toValue = [NSNumber numberWithFloat:1.0];
       scaleAnimation.duration = 0.5f;
       scaleAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
     }
     
     CAKeyframeAnimation *rotateAnimation = [CAKeyframeAnimation animationWithKeyPath:@ "transform.rotation.z" ];
     rotateAnimation.values = @[@(M_PI), @(0.0)];
     rotateAnimation.duration = 0.5f;
     rotateAnimation.keyTimes = @[@(0.3), @(0.4)];
     
     CAKeyframeAnimation *positionAnimation = [CAKeyframeAnimation animationWithKeyPath:@ "position" ];
     positionAnimation.duration = 0.5f;
     CGMutablePathRef path = CGPathCreateMutable();
     CGPathMoveToPoint(path, NULL, item.startPoint.x, item.startPoint.y);
     CGPathAddLineToPoint(path, NULL, item.farPoint.x, item.farPoint.y);
     CGPathAddLineToPoint(path, NULL, item.nearPoint.x, item.nearPoint.y);
     CGPathAddLineToPoint(path, NULL, item.endPoint.x, item.endPoint.y);
     positionAnimation.path = path;
     CGPathRelease(path);
     
     CAAnimationGroup *animationgroup = [CAAnimationGroup animation];
     if  (self.scale) {
       animationgroup.animations = @[scaleAnimation, positionAnimation, rotateAnimation];
     else  {
       animationgroup.animations = @[positionAnimation, rotateAnimation];
     }
     animationgroup.duration = 0.5f;
     animationgroup.fillMode = kCAFillModeForwards;
     animationgroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
     [item.layer addAnimation:animationgroup forKey:@ "Expand" ];
     item.center = item.endPoint;
   else  {
     CABasicAnimation *scaleAnimation = nil;
     if  (self.scale) {
       scaleAnimation = [CABasicAnimation animationWithKeyPath:@ "transform.scale" ];
       scaleAnimation.fromValue = [NSNumber numberWithFloat:1.0];
       scaleAnimation.toValue = [NSNumber numberWithFloat:0.2];
       scaleAnimation.duration = 0.5f;
       scaleAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
     }
     
     CAKeyframeAnimation *rotateAnimation = [CAKeyframeAnimation animationWithKeyPath:@ "transform.rotation.z" ];
     rotateAnimation.values = @[@0, @(M_PI * 2), @(0)];
     rotateAnimation.duration = 0.5f;
     rotateAnimation.keyTimes = @[@0, @(0.4), @(0.5)];
     CAKeyframeAnimation *positionAnimation = [CAKeyframeAnimation animationWithKeyPath:@ "position" ];
     positionAnimation.duration = 0.5f;
     CGMutablePathRef path = CGPathCreateMutable();
     CGPathMoveToPoint(path, NULL, item.endPoint.x, item.endPoint.y);
     CGPathAddLineToPoint(path, NULL, item.farPoint.x, item.farPoint.y);
     CGPathAddLineToPoint(path, NULL, item.startPoint.x, item.startPoint.y);
     positionAnimation.path = path;
     CGPathRelease(path);
     
     CAAnimationGroup *animationgroup = [CAAnimationGroup animation];
     if  (self.scale) {
       animationgroup.animations = @[scaleAnimation, positionAnimation, rotateAnimation];
     else  {
       animationgroup.animations = @[positionAnimation, rotateAnimation];
     }
     
     animationgroup.duration = 0.5f;
     animationgroup.fillMode = kCAFillModeForwards;
     animationgroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
     [item.layer addAnimation:animationgroup forKey:@ "Close" ];
     item.center = item.startPoint;
   }
}

讲讲展开缩放动画

展开时缩放动画可以通过修改transform.scale来实现,这是x、y方向都缩放了。也就是在刚出来时,缩放不断变大到最终大小。

1
2
3
scaleAnimation = [CABasicAnimation animationWithKeyPath:@ "transform.scale" ];
scaleAnimation.fromValue = [NSNumber numberWithFloat:0.2];
scaleAnimation.toValue = [NSNumber numberWithFloat:1.0];

讲讲展开旋转动画

我们旋转的是z轴,而不是x、y轴,通过transform.rotation.z来实现。这里使用的是关键帧来实现,其中设置values及keyTimes的个数是对应的:

1
2
3
4
CAKeyframeAnimation *rotateAnimation = [CAKeyframeAnimation animationWithKeyPath:@ "transform.rotation.z" ];
rotateAnimation.values = @[@(M_PI), @(0.0)];
rotateAnimation.duration = 0.5f;
rotateAnimation.keyTimes = @[@(0.3), @(0.4)];

讲讲展开移动动画

通过position可以实现平移动画,这里也是使用关键帧来实现,因为需要到path。通过添加路径,来实现起点、最近、最远、最终停留的路径:

1
2
3
4
5
6
7
8
9
CAKeyframeAnimation *positionAnimation = [CAKeyframeAnimation animationWithKeyPath:@ "position" ];
positionAnimation.duration = 0.5f;
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, item.startPoint.x, item.startPoint.y);
CGPathAddLineToPoint(path, NULL, item.farPoint.x, item.farPoint.y);
CGPathAddLineToPoint(path, NULL, item.nearPoint.x, item.nearPoint.y);
CGPathAddLineToPoint(path, NULL, item.endPoint.x, item.endPoint.y);
positionAnimation.path = path;
CGPathRelease(path);

讲讲展开组合动画

上面创建了好几种动画,那么要实现组合,就需要通过CAAnimationGroup来实现了。然后添加到叶子中的动画,就是组合动画:

1
2
3
4
5
6
7
8
9
10
11
CAAnimationGroup *animationgroup = [CAAnimationGroup animation];
if  (self.scale) {
     animationgroup.animations = @[scaleAnimation, positionAnimation, rotateAnimation];
else  {
     animationgroup.animations = @[positionAnimation, rotateAnimation];
}
animationgroup.duration = 0.5f;
animationgroup.fillMode = kCAFillModeForwards;
animationgroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[item.layer addAnimation:animationgroup forKey:@ "Expand" ];
item.center = item.endPoint;

关于收起的动画也差不多,就不说了!

结尾

本篇文章主要是想教大家如何去设计一个动画及UI控件,当然笔者所讲的并不一定是最好的,也许你就能想出更简单更优秀的办法来实现。本篇文章的源代码不放出,只放出关键代码。对于学习能力比较强,比较爱研究的小伙伴已经足够了。如果非要源代码,可私聊我购买。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值