高级动画-圆形树展开、收起动画

MarcellaAnd 5年前
   <h2>概述</h2>    <p>前段时间帮某某做了一个动画效果,今天分享给大家。关于动画的基础知识,这里不会细说,如果您还没有核心动画的基础知识,请先阅读相关文章,了解核心动画如何使用,然后再继续阅读本篇文章。</p>    <p>本篇文章,涉及到以下知识点:</p>    <ul>     <li>如何添加缩放动画</li>     <li>如何添加平移动画</li>     <li>如何添加旋转动画</li>     <li>如何添加关键帧动画</li>     <li>如何使用组合动画</li>     <li>如何实现渐变图层</li>     <li>如何实现圆形渐变进度条</li>    </ul>    <p>温馨提示:新手不适合阅读本篇文章哦,不过可以初步阅读了解一下!</p>    <p>如果没有了解过动画的基础知识,可先看看笔者之前的一些文章:</p>    <ol>     <li><a href="/misc/goto?guid=4959673740530142910" rel="nofollow,noindex">CALayer精讲</a></li>     <li><a href="/misc/goto?guid=4959673740614489281" rel="nofollow,noindex">UIBezierPath精讲</a></li>     <li><a href="/misc/goto?guid=4959673740701658898" rel="nofollow,noindex">iOS CAShapeLayer精讲</a></li>     <li><a href="/misc/goto?guid=4959673740777692695" rel="nofollow,noindex">CAAnimation解读</a></li>     <li><a href="/misc/goto?guid=4959673740847291905" rel="nofollow,noindex">CABasicAnimation精讲</a></li>    </ol>    <p>本篇文章不深入讲基础知识,只讲如何实现及实现的要点,并放出关键代码。对于伸手党,请不要私聊我要完整的源代码。如果您正好在项目中有这样的需求,可以尝试根据本篇文章讲解动手做一个!</p>    <h2>最终效果图</h2>    <ol>     <li>无缩放动画的效果图:</li>    </ol>    <p><img src="https://simg.open-open.com/show/26cf2b990a083665d72abdaeaed0fbf8.gif"></p>    <p>从动画效果可以看出来,有平移、旋转、关键帧动画,同时还有渐变进度条充满的动画。另外还要注意移动的距离。请忽略样式丑陋的问题~</p>    <ol>     <li>有缩放动画的效果图:</li>    </ol>    <p><img src="https://simg.open-open.com/show/fcbe952ef37b597604d0973142d026b2.gif"></p>    <p>从动画效果可以看出来,个人变成了6个,且是平分的,比上面的效果图多了缩放的动画!这个缩放动画,生成GIF图的效果真丑,跟手机运行起来看到的差别比较大!</p>    <h2>设计思路</h2>    <p>这里是封装里了通用的组件,如果是在项目中使用,可以轻松调用且可以复用。从动画效果可以看到,这个整体是由以下几个部分组成的:</p>    <ol>     <li>中间带进度图的圆形控件(这里叫叶子吧)</li>     <li>从中心圆出来到四周的N个控件,其中每个都是拥有同样的特性的</li>    </ol>    <p>所以,我们设计成三个类,分别是:</p>    <ul>     <li>HYBCurveItemView:代表散开的每个子项控件</li>     <li>HYBCurveMainView:代表中间的圆形控件</li>     <li>HYBCurveMenuView:由前两个控件组合而形成的整体控件</li>    </ul>    <h2>设计HYBCurveItemView</h2>    <p>假设叫叶子。那么每个叶子就有相同的特性,需要知道自己的归属:</p>    <ul>     <li>自身的大小</li>     <li>展示的元素</li>     <li>可移动到最远的哪个位置</li>     <li>可移动到最近的哪个位置</li>     <li>起始位置</li>     <li>最终展开后停留的位置</li>    </ul>    <p>可设置以下几个位置属性:</p>    <pre>  <code class="language-objectivec">   @property (nonatomic, assign) CGPoint startPoint;  @property (nonatomic, assign) CGPoint endPoint;  @property (nonatomic, assign) CGPoint nearPoint;  @property (nonatomic, assign) CGPoint farPoint;     </code></pre>    <p>这个类只需要有相关属性即可!</p>    <h2>设计HYBCurveMainView</h2>    <p>假设中大圆。大圆主要需要属性以下特性:</p>    <ul>     <li>自身大小</li>     <li>展示的元素</li>     <li>带有渐变圆形进度条</li>    </ul>    <p>可执行的操作:</p>    <ul>     <li>更新进度</li>     <li>展开、收起叶子</li>    </ul>    <p>对于这个大圆类,细读如何添加圆形进度条。</p>    <h2>设计渐变进度圆环</h2>    <h3>首先,添加白色固定圆形</h3>    <p>底部是一个白色的圆环,当白色圆环填满时,表示0%。那么白色圆环可以用什么来实现呢?其实CAShapeLayer就是非常好的,它可以添加圆形路径来实现的,记得设置填充色为透明哦,不然连中间的内容也看不见了。代码如下:</p>    <pre>  <code class="language-objectivec">   self.outLayer = [CAShapeLayer layer];  CGRect rect = {kLineWidth / 2, kLineWidth / 2, frame.size.width - kLineWidth, frame.size.height - kLineWidth};  UIBezierPath *path = [UIBezierPathbezierPathWithOvalInRect: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.layeraddSublayer:self.outLayer];     </code></pre>    <p>因为要设置为白色圆环,所以画笔颜色设置为白色,线宽就设置为圆环的大小。这样就可以初步形成了带有白色圆环的底色了。此时就是0%。</p>    <h3>其次,设置可调进度的进度圆环图层</h3>    <p>下面所创建的图层,会用于设置渐变颜色图层的mask,这样才能显示中间的内容,而不是渐变的图层。它的大小与白底圆环一样大小:</p>    <pre>  <code class="language-objectivec">   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;     </code></pre>    <h3>然后,增加渐变图层</h3>    <p>要实现渐变图层,可以通过CAGradientLayer来创建,这里的颜色是随意指定的,所以效果不太协调,大家可自由调整。这里呢使用了两个渐变图层,然后放到一个大的渐变图层中,两个小的图层各占一半。 当添加了mask后,就只有进度这一部分渐变可显示了。</p>    <pre>  <code class="language-objectivec">   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;  [gradientLayer1setColors:@[(__bridgeid)red, (__bridgeid)purple, (__bridgeid)yellow, (__bridgeid)orange]];  [gradientLayer1setLocations:@[@0.3, @0.6, @0.8, @1.0]];  [gradientLayer1setStartPoint:CGPointMake(0.5, 1)];  [gradientLayer1setEndPoint: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;  [gradientLayer2setColors:@[(__bridgeid)blue, (__bridgeid)purple1, (__bridgeid)r1, (__bridgeid)o1]];  [gradientLayer2setLocations:@[@0.3, @0.6, @0.8, @1.0]];  [gradientLayer2setStartPoint:CGPointMake(0.5, 0)];  [gradientLayer2setEndPoint:CGPointMake(0.5, 1)];     CAGradientLayer *gradientLayer = [CAGradientLayer layer];  gradientLayer.frame = self.bounds;  [gradientLayeraddSublayer:gradientLayer1];  [gradientLayer1addSublayer:gradientLayer2];  gradientLayer.mask = self.progressLayer;  [self.layeraddSublayer:gradientLayer];     </code></pre>    <p>注意,一定要设置gradientLayer.mask = self.progressLayer;这样才能显示中间的内容,如果不设置mask,那么就只有渐变图层了。</p>    <h3>最后,动画更新进度</h3>    <p>在需要更新进度的时候,可以调用这个API来更新进度,带有动画效果。</p>    <pre>  <code class="language-objectivec">   - (void)updateProgressWithNumber:(NSUInteger)number {    [CATransaction begin];    [CATransactionsetAnimationTimingFunction:[CAMediaTimingFunctionfunctionWithName:kCAMediaTimingFunctionEaseIn]];    [CATransactionsetAnimationDuration:0.5];    self.progressLayer.strokeEnd =  number / 100.0;    self.percentLabel.text = [NSStringstringWithFormat:@"%@%%", @(number)];    [CATransaction commit];  }     </code></pre>    <h2>设计HYBCurveMenuView</h2>    <p>这个类就是圆形菜单类了,整合前两个。它主要具备以下特性:</p>    <ul>     <li>自身大小</li>     <li>是否添加缩放动画</li>     <li>更换叶子</li>    </ul>    <p>可执行的操作:</p>    <ul>     <li>展开、收起</li>     <li>点击大圆回调</li>    </ul>    <h2>更换叶子</h2>    <p>当更换所有的叶子时,需要调整所有叶子的位置:</p>    <pre>  <code class="language-objectivec">   - (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)[menuItemscount];      CGFloat cnt = 1;      for (int i = 0; i < count; i ++) {        HYBCurveItemView *item = [menuItemsobjectAtIndex: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;        [selfaddSubview:item];                cnt += 2;      }            [selfbringSubviewToFront:_mainView];    }  }     </code></pre>    <p>其中,这几个属性带有默认值(分别表示起点、最近点、最远点、展开后最终停留点):</p>    <pre>  <code class="language-objectivec">   self.startPoint = self.center;  // 修改这时的参数来调整大圆与圆之间的距离  self.nearDistance = 30;  self.farDistance = 60;  self.endDistance = 30;     </code></pre>    <p>对于上面的点的计算,主要是一点点的数学知识,需要懂得象限与角度的关系。</p>    <h2>展开或者收起</h2>    <p>调用下面的方法来展开或者收起。这里会遍历所有的叶子,让每个叶子都添加对应的动画变换,就可以看到动画轨迹了:</p>    <pre>  <code class="language-objectivec">   - (void)expend:(BOOL)isExpend {    _isExpend = isExpend;        [self.menuItemsenumerateObjectsUsingBlock:^(HYBCurveItemView *obj, NSUInteger idx, BOOL * _Nonnullstop) {      if (self.scale) {        if (isExpend) {          obj.transform = CGAffineTransformIdentity;        } else {          obj.transform = CGAffineTransformMakeScale(0.001, 0.001);        }      }            [selfaddRotateAndPostisionForItem:objtoShow:isExpend];    }];  }     </code></pre>    <p>接下来是最关键的动画核心代码:</p>    <pre>  <code class="language-objectivec">   - (void)addRotateAndPostisionForItem:(HYBCurveItemView *)itemtoShow:(BOOL)show {    if (show) {      CABasicAnimation *scaleAnimation = nil;      if (self.scale) {        scaleAnimation = [CABasicAnimationanimationWithKeyPath:@"transform.scale"];        scaleAnimation.fromValue = [NSNumbernumberWithFloat:0.2];        scaleAnimation.toValue = [NSNumbernumberWithFloat:1.0];        scaleAnimation.duration = 0.5f;        scaleAnimation.timingFunction = [CAMediaTimingFunctionfunctionWithName:kCAMediaTimingFunctionEaseInEaseOut];      }            CAKeyframeAnimation *rotateAnimation = [CAKeyframeAnimationanimationWithKeyPath:@"transform.rotation.z"];      rotateAnimation.values = @[@(M_PI), @(0.0)];      rotateAnimation.duration = 0.5f;      rotateAnimation.keyTimes = @[@(0.3), @(0.4)];            CAKeyframeAnimation *positionAnimation = [CAKeyframeAnimationanimationWithKeyPath:@"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 = [CAMediaTimingFunctionfunctionWithName:kCAMediaTimingFunctionEaseIn];      [item.layeraddAnimation:animationgroupforKey:@"Expand"];      item.center = item.endPoint;    } else {      CABasicAnimation *scaleAnimation = nil;      if (self.scale) {        scaleAnimation = [CABasicAnimationanimationWithKeyPath:@"transform.scale"];        scaleAnimation.fromValue = [NSNumbernumberWithFloat:1.0];        scaleAnimation.toValue = [NSNumbernumberWithFloat:0.2];        scaleAnimation.duration = 0.5f;        scaleAnimation.timingFunction = [CAMediaTimingFunctionfunctionWithName:kCAMediaTimingFunctionEaseInEaseOut];      }            CAKeyframeAnimation *rotateAnimation = [CAKeyframeAnimationanimationWithKeyPath:@"transform.rotation.z"];      rotateAnimation.values = @[@0, @(M_PI * 2), @(0)];      rotateAnimation.duration = 0.5f;      rotateAnimation.keyTimes = @[@0, @(0.4), @(0.5)];      CAKeyframeAnimation *positionAnimation = [CAKeyframeAnimationanimationWithKeyPath:@"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 = [CAMediaTimingFunctionfunctionWithName:kCAMediaTimingFunctionEaseIn];      [item.layeraddAnimation:animationgroupforKey:@"Close"];      item.center = item.startPoint;    }  }     </code></pre>    <h3>讲讲展开缩放动画</h3>    <p>展开时缩放动画可以通过修改transform.scale来实现,这是x、y方向都缩放了。也就是在刚出来时,缩放不断变大到最终大小。</p>    <pre>  <code class="language-objectivec">   scaleAnimation = [CABasicAnimationanimationWithKeyPath:@"transform.scale"];  scaleAnimation.fromValue = [NSNumbernumberWithFloat:0.2];  scaleAnimation.toValue = [NSNumbernumberWithFloat:1.0];     </code></pre>    <h3>讲讲展开旋转动画</h3>    <p>我们旋转的是z轴,而不是x、y轴,通过transform.rotation.z来实现。这里使用的是关键帧来实现,其中设置values及keyTimes的个数是对应的:</p>    <pre>  <code class="language-objectivec">   CAKeyframeAnimation *rotateAnimation = [CAKeyframeAnimationanimationWithKeyPath:@"transform.rotation.z"];  rotateAnimation.values = @[@(M_PI), @(0.0)];  rotateAnimation.duration = 0.5f;  rotateAnimation.keyTimes = @[@(0.3), @(0.4)];     </code></pre>    <h3>讲讲展开移动动画</h3>    <p>通过position可以实现平移动画,这里也是使用关键帧来实现,因为需要到path。通过添加路径,来实现起点、最近、最远、最终停留的路径:</p>    <pre>  <code class="language-objectivec">   CAKeyframeAnimation *positionAnimation = [CAKeyframeAnimationanimationWithKeyPath:@"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);     </code></pre>    <h3>讲讲展开组合动画</h3>    <p>上面创建了好几种动画,那么要实现组合,就需要通过CAAnimationGroup来实现了。然后添加到叶子中的动画,就是组合动画:</p>    <pre>  <code class="language-objectivec">   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 = [CAMediaTimingFunctionfunctionWithName:kCAMediaTimingFunctionEaseIn];  [item.layeraddAnimation:animationgroupforKey:@"Expand"];  item.center = item.endPoint;     </code></pre>    <p>关于收起的动画也差不多,就不说了!</p>    <h2>结尾</h2>    <p>本篇文章主要是想教大家如何去设计一个动画及UI控件,当然笔者所讲的并不一定是最好的,也许你就能想出更简单更优秀的办法来实现。</p>    <p>本篇文章的源代码不放出,只放出关键代码。对于学习能力比较强,比较爱研究的小伙伴已经足够了。如果非要源代码,可私聊我购买!</p>    <h2>关注我</h2>    <table>     <thead>      <tr>       <th>联系方式</th>       <th>关注</th>       <th>备注</th>      </tr>     </thead>     <tbody>      <tr>       <td>付费解答群</td>       <td>347363861(付费解答群)</td>       <td>有需求或者私活可入群私聊</td>      </tr>      <tr>       <td>标哥博客iOS交流群</td>       <td>552095943(新)|259290340(新)</td>       <td>群里很活跃,定期清理</td>      </tr>      <tr>       <td>标哥博客iOS交流群</td>       <td>324400294(满)|494669518(满)|494669518(满)|250351140(满)</td>       <td>群里很活跃,定期清理</td>      </tr>      <tr>       <td>微信公众号</td>       <td><a href="http://mp.weixin.qq.com/s?__biz=MzIzMzA4NjA5Mw==&mid=404311320&idx=1&sn=805d4b9fb92c5b6ff721bb1d9966f41b#rd" rel="nofollow,noindex">iOSDevShares</a></td>       <td>关注公众号阅读好文章</td>      </tr>      <tr>       <td>新浪微博</td>       <td>@标哥的技术博客</td>       <td>关注微博动态</td>      </tr>      <tr>       <td>GITHUB</td>       <td><a href="/misc/goto?guid=4959654077363809730" rel="nofollow,noindex">CoderJackyHuang</a></td>       <td>文章Demo都在GITHUB</td>      </tr>      <tr>       <td>联系标哥</td>       <td><a href="/misc/goto?guid=4959664424999406415" rel="nofollow,noindex">关于标哥</a></td>       <td>保持活跃在最前线</td>      </tr>     </tbody>    </table>    <p>版权声明:本文为【标哥的技术博客】原创出品,欢迎转载,转载时请注明出处!</p>    <p><img src="https://simg.open-open.com/show/b77d35c72eabd171708611869441ee7c.png"> <img src="https://simg.open-open.com/show/c09c408fb39c645e0ff0f49993b78c1a.png"> <img src="https://simg.open-open.com/show/4352e4a2283050d141922e4e23bc7ffb.png"></p>    <p> </p>    <p>来自: <a href="/misc/goto?guid=4959673741080118046" rel="nofollow">http://www.henishuo.com/coreanimation-tree-circle-expend/</a></p>    <p> </p>