6.2 核心动画->4.0 常用动画效果

作者: 蓝田_Loto | 来源:发表于2016-07-26 01:29 被阅读469次

    本文并非最终版本,如果想要关注更新或更正的内容请关注文集,联系方式详见文末,如有疏忽和遗漏,欢迎指正。


    本文相关目录:
    ===================== 所属文集:6.0 图形和多媒体 =====================
    6.2 核心动画->1.0 CALayer的简介
    6.2 核心动画->1.1 CALayer的基本属性
    6.2 核心动画->1.2 CALayer的创建 & 非根layer的隐式动画
    6.2 核心动画->2.0 Core Animation(核心动画)
    6.2 核心动画->3.0 核心动画 & UIView动画
    6.2 核心动画->4.0 常用动画效果
    ===================== 所属文集:6.0 图形和多媒体 =====================


    4.1 旋转立体效果 - 折叠图片案例

    1、如何制作图片折叠效果?
    把一张图片分成两部分显示,上面一部分,下面一部分,折叠上面部分的内容。
    
    2、如何把一张图片分成两部分显示。
    - 创建两个View,一个显示上半部分,一个显示下半部分
    
    - 需要用到Layer(图层)的一个属性:contentsRect ,这个属性是可以控制图片显示的尺寸,可以
    让图片只显示上部分或者下部分,注意: 取值范围是0~1
       CGRectMake(0, 0, 1, 0.5)   : 表示显示上半部分
       CGRectMake(0, 0.5, 1, 0.5) : 表示显示下半部分
    
    3、如何快速的把两部分拼接成一张完整的图片
    (1)首先了解折叠,折叠其实就是旋转,既然需要旋转就需要明确锚点,因为默认都是绕着锚点旋转的。
    
    (2)上部分内容绕着底部中心旋转,所以设置上部分的锚点为(0.5,1)
    
    (3)锚点设置好了,就可以确定位置了.
    
    (4)可以把上下部分重合在一起,然后分别设置上下部分的锚点,上部分的锚点为(0.5,1),下部分
    的锚点为(0.5,0),就能快速重叠了。
    
    4、如何折叠上部分内容。
    - 在拖动视图的时候,旋转上部分控件。修改 transform 属性。
    
    - 可以在上部分和下部分底部添加一个拖动控件(拖动控件尺寸就是完整的图片尺寸),给这个控件添
    加一个pan手势,就能制造一个假象,拖动控件的时候,折叠图片。
    
    - 计算Y轴每偏移一点,需要旋转多少角度,假设完整图片尺寸高度为200,当y = 200时,上部分图片
    应该刚好旋转180°,因此 angle = offsetY  * M_PI / 200 ;
    
    - 上部分内容应该是绕着X轴旋转,逆时针旋转,因此角度需要为负数。
    

    代码示例:

    #import "ViewController.h"
    
    @interface ViewController ()
    @property(weak, nonatomic) IBOutlet UIImageView *topView;
    @property(weak, nonatomic) IBOutlet UIImageView *bottomView;
    @property(weak, nonatomic) IBOutlet UIView *dragView;  // 手势拖动的 View
    @property(weak, nonatomic) CAGradientLayer *gradientL; // 渐变图层
    @end
    
    @implementation ViewController
    
    // 快速把两个控件拼接成一个完整图片
    - (void)viewDidLoad {
      [super viewDidLoad];
      // 通过设置contentsRect可以设置图片显示的尺寸,取值0~1
      _topView.layer.contentsRect = CGRectMake(0, 0, 1, 0.5);
      _topView.layer.anchorPoint = CGPointMake(0.5, 1);
    
      _bottomView.layer.contentsRect = CGRectMake(0, 0.5, 1, 0.5);
      _bottomView.layer.anchorPoint = CGPointMake(0.5, 0);
    
      // 添加手势
      UIPanGestureRecognizer *pan =
          [[UIPanGestureRecognizer alloc] initWithTarget:self
                                                  action:@selector(pan:)];
    
      [_dragView addGestureRecognizer:pan];
    
    #pragma mark - 阴影效果(当折叠图片的时候,底部应该有个阴影渐变过程)(利用CAGradientLayer渐变图层,制作阴影效果,添加到底部视图上,并且一开始需要隐藏,在拖动的时候慢慢显示出来。颜色应是由 透明到黑色 渐变,表示阴影从无到有)
      // 创建渐变图层
      CAGradientLayer *gradientL = [CAGradientLayer layer];
    
      // 设置渐变颜色
      gradientL.colors =
          @[ (id)[UIColor clearColor].CGColor, (id)[UIColor blackColor].CGColor ];
    
      // 设置尺寸
      gradientL.frame = _bottomView.bounds;
    
      _gradientL = gradientL;
    
      // 设置图层透明色为不透明
      gradientL.opacity = 0;
    
      // 设置渐变定位点(决定渐变的范围)
      //    gradientL.locations = @[@0.1,@0.4,@0.5];
    
      // 设置渐变开始点,取值0~1(决定渐变的方向)
      //    gradientL.startPoint = CGPointMake(0, 1);
    
      [_bottomView.layer addSublayer:gradientL];
    }
    
    #pragma mark - 拖动的时候旋转上部分内容
    - (void)pan:(UIPanGestureRecognizer *)pan {
      // 获取手指偏移量
      CGPoint transP = [pan translationInView:_dragView];
    
      // 初始化形变
      CATransform3D transfrom = CATransform3DIdentity;
    
      // 设置立体效果,增加旋转的立体感,给形变设置m34属性,近大远小
      //  -1 / z,z表示观察者在z轴上的值,表示距图层的距离:z越小,离我们越近,东西越大
      transfrom.m34 = -1 / 500.0; // 第二个数
    
      // 计算折叠角度,因为往下逆时针旋转,所以取反
      CGFloat angle = -transP.y / 200.0 * M_PI;
    
      // 开始旋转
      transfrom = CATransform3DRotate(transfrom, angle, 1, 0, 0);
    
      _topView.layer.transform = transfrom;
    
      // 设置阴影效果
      // 在拖动的时候计算不透明度值,假设拖动200,阴影完全显示,不透明度应该为1,因此opacity=y轴偏移量*1/200.0;
      _gradientL.opacity = transP.y * 1 / 200.0;
    
    #pragma mark - 反弹效果(当手指抬起的时候,应该把折叠图片还原,其实就是把形变清空)
      // 当手指抬起的时候,设置弹簧效果的动画
      if (pan.state == UIGestureRecognizerStateEnded) {
        // 弹簧效果的动画
        [UIView animateWithDuration:0.6
            delay:0
            usingSpringWithDamping:0.2 // 弹性系数:越小,弹簧效果越明显
            initialSpringVelocity:10  // 弹簧的初始速度
            options:UIViewAnimationOptionCurveEaseInOut // 动画效果:快入快出
            animations:^{
              _topView.layer.transform = CATransform3DIdentity; // 初始化形变量
              _gradientL.opacity = 0;                           // 还原阴影
            }
            completion:^(BOOL finished){
    
            }];
      }
    }
    
    - (void)didReceiveMemoryWarning {
      [super didReceiveMemoryWarning];
      // Dispose of any resources that can be recreated.
    }
    
    @end
    

    运行效果:

    旋转立体效果.gif

    4.2 音量震动条效果


    方法1 :创建多个layer,按顺序播放y轴缩放动画

    方法2:利用 CAReplicatorLayer(复制图层)实现

    (这里只介绍方法2)

    1、什么是CAReplicatorLayer?
    一种可以复制自己子层的layer,并且复制出来的layer和原生子层有同样的属性,位置,形变,动画。
    
    2、CAReplicatorLayer属性
    // 复制层中子层总数:表示复制层里面有多少个子层,包括原始层
    @property NSInteger instanceCount;
    
    // 复制子层动画延迟时长
    @property CFTimeInterval instanceDelay;
    
    // 复制子层形变偏移量(不包括原生子层):相对于原生子层x偏移,每个复制子层都是相对上一个
    @property CATransform3D instanceTransform;
    
    // 子层颜色:会和原生子层背景色冲突,如果设置了原生子层背景色,就不需要设置这个属性
    @property (nullable) CGColorRef instanceColor;
    
    // 颜色通道的偏移量:每个复制子层都是相对上一个的偏移量
    @property float instanceRedOffset;
    @property float instanceGreenOffset;
    @property float instanceBlueOffset;
    @property float instanceAlphaOffset;
    

    代码示例:

    #import "ViewController.h"
    
    @interface ViewController ()
    @property(weak, nonatomic) IBOutlet UIView *lightView;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
      [super viewDidLoad];
    
    #pragma mark - 首先创建复制layer,音乐振动条layer添加到复制layer上,然后复制子层
      // 创建复制图层(CAReplicatorLayer),可以把图层里面所有子层复制
      CAReplicatorLayer *repL = [CAReplicatorLayer layer];
      // 设置复制图层的属性
      repL.frame = _lightView.bounds;
      // 添加复制图层到lightView上
      [_lightView.layer addSublayer:repL];
    
    #pragma mark - 先创建一个音量振动条,并且设置好动画,动画是绕着底部缩放,设置锚点
      // 创建图层
      CALayer *layer = [CALayer layer];
      // 设置图层属性
      layer.anchorPoint = CGPointMake(0.5, 1);
      layer.position = CGPointMake(15, _lightView.bounds.size.height);
      layer.bounds = CGRectMake(0, 0, 30, 150);
      layer.backgroundColor = [UIColor whiteColor].CGColor;
      // 添加图层到复制图层上
      [repL addSublayer:layer];
    
      // 创建动画
      CABasicAnimation *anim = [CABasicAnimation animation];
      // 动画属性
      anim.keyPath = @"transform.scale.y"; // 缩放y值
      anim.toValue = @0.1;
      anim.duration = 0.2;
      anim.repeatCount = MAXFLOAT;
      anim.autoreverses = YES; // 设置动画反转
      // 添加动画
      [layer addAnimation:anim forKey:nil];
    
      // 复制层中子层总数:表示复制层里面有多少个子层,包括原生子层
      repL.instanceCount = 5; // 设置5个子层,其中4个是复制层
    
      // 复制子层形变偏移量(不包括原生子层):相对于原生子层x偏移,每个复制子层都是相对上一个
      // 设置复制子层的相对位置,每个x轴相差45
      repL.instanceTransform = CATransform3DMakeTranslation(45, 0, 0);
    
      // 设置复制子层动画延迟时长
      repL.instanceDelay = 0.1;
    
      // 子层颜色:会和原生子层背景色冲突,如果设置了原生子层背景色,就不需要设置这个属性
      repL.instanceColor = [UIColor blueColor].CGColor;
      // 颜色通道的偏移量:每个复制子层都是相对上一个的偏移量
      repL.instanceBlueOffset = -0.3;
    }
    
    - (void)didReceiveMemoryWarning {
      [super didReceiveMemoryWarning];
      // Dispose of any resources that can be recreated.
    }
    
    @end
    

    运行效果:

    音量震动条效果.gif

    4.3 活动指示器效果

    设计思路:
    1、创建复制图层
    2、创建一个矩形图层,设置缩放动画
    3、复制矩形图层,并且设置每个复制层的角度形变
    4、设置复制动画延长时间
    

    代码示例:

    #import "ViewController.h"
    
    @interface ViewController ()
    @property(weak, nonatomic) IBOutlet UIView *gray;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
      [super viewDidLoad];
    
    #pragma mark - 创建复制图层
      CAReplicatorLayer *repL = [CAReplicatorLayer layer];
      repL.frame = _gray.bounds;
      [_gray.layer addSublayer:repL];
    
    #pragma mark - 创建一个矩形图层,设置缩放动画。
      CALayer *layer = [CALayer layer];
    
      // 设置属性
      layer.transform = CATransform3DMakeScale(0, 0, 0);
      layer.position = CGPointMake(_gray.bounds.size.width / 2, 20);
      layer.bounds = CGRectMake(0, 0, 10, 10);
      layer.backgroundColor = [UIColor grayColor].CGColor;
    
      // 添家图层
      [repL addSublayer:layer];
    
      // 设置缩放动画
      CABasicAnimation *anim = [CABasicAnimation animation];
    
      anim.keyPath = @"transform.scale";
      anim.fromValue = @1; // 动画Value值从1到0变化
      anim.toValue = @0;
      anim.repeatCount = MAXFLOAT;
      CGFloat duration = 1;
      anim.duration = duration;
    
      [layer addAnimation:anim forKey:nil];
    
    #pragma mark - 复制矩形图层,并且设置每个复制层的角度形变
      int count = 20;
    
      CGFloat angle = M_PI * 2 / count; // 设置子层形变角度
      repL.instanceCount = count;       // 设置子层总数
      repL.instanceTransform = CATransform3DMakeRotation(angle, 0, 0, 1);
    
    #pragma mark - 设置复制动画延长时间(需要保证第一个执行完毕之后,绕一圈刚好又是从第一个执行,因此需要把动画时长平均分给每个子层)
      // 公式:子层动画延长时间 = 动画时长 / 子层总数
      // 假设有两个图层,动画时间为1秒,延长时间就为0.5秒。当第一个动画执行到一半的时候(0.5),第二个开始执行。第二个执行完
      repL.instanceDelay = duration / count;
    }
    
    - (void)didReceiveMemoryWarning {
      [super didReceiveMemoryWarning];
      // Dispose of any resources that can be recreated.
    }
    
    @end
    

    运行效果:


    活动指示器效果.gif

    4.4 粒子效果

    思路:搞个画板绘制路径,自定义view
    
    效果:随机绘制一条路径,点击开始按钮,粒子动画
    

    代码示例:
    ViewController.m

    #import "DrawView.h"
    #import "ViewController.h"
    
    @interface ViewController ()
    @end
    
    @implementation ViewController
    #pragma mark - 点击开始动画
    - (IBAction)startAnim:(id)sender {
      DrawView *view = (DrawView *)self.view; // 强转的方式获取
      [view startAnim];
    }
    
    #pragma mark - 点击重绘
    - (IBAction)reDraw:(id)sender {
      DrawView *view = (DrawView *)self.view;
      [view reDraw];
    }
    
    - (void)viewDidLoad {
      [super viewDidLoad];
      // Do any additional setup after loading the view, typically from a nib.
    }
    
    - (void)didReceiveMemoryWarning {
      [super didReceiveMemoryWarning];
      // Dispose of any resources that can be recreated.
    }
    
    @end
    

    DrawView.h

    #import <UIKit/UIKit.h>
    
    @interface DrawView : UIView
    - (void)startAnim; // 开始动画
    - (void)reDraw;    // 重绘
    @end
    

    DrawView.m

    #import "DrawView.h"
    
    @interface DrawView ()
    @property(nonatomic, strong) UIBezierPath *path;    // 路径
    @property(nonatomic, weak) CALayer *dotLayer;       // 点图层
    @property(nonatomic, weak) CAReplicatorLayer *repL; // 复制层
    @end
    
    @implementation DrawView
    
    #pragma mark - 1、加载完xib调用,创建复制层
    - (void)awakeFromNib {
      // 创建复制层
      CAReplicatorLayer *repL = [CAReplicatorLayer layer];
      // 设置复制层属性
      repL.frame = self.bounds;
      // 添加复制层到控件的layer上
      [self.layer addSublayer:repL];
    
      _repL = repL;
    }
    
    #pragma mark - 2、调用drawRect
    - (void)drawRect:(CGRect)rect {
      [_path stroke];
    }
    
    #pragma mark - 3、点击屏幕
    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
      // 获取touch对象
      UITouch *touch = [touches anyObject];
      // 获取当前触摸点
      CGPoint curP = [touch locationInView:self];
      // 设置起点
      [self.path moveToPoint:curP];
    }
    
    #pragma mark - 4、懒加载路径(目的是为了在不点击重绘的情况下,将所有的线添加到一条路径上去)
    // 因为核心动画只能设置一个路径,因此只能创建一个路径,懒加载路径。
    - (UIBezierPath *)path {
      if (_path == nil) {
        _path = [UIBezierPath bezierPath];
      }
      return _path;
    }
    
    static int _instansCount = 0;
    #pragma mark - 5、手指在屏幕上移动的时候
    - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
      // 获取touch对象
      UITouch *touch = [touches anyObject];
      // 获取当前触摸点
      CGPoint curP = [touch locationInView:self];
      // 添加线到某个点
      [_path addLineToPoint:curP];
      // 重绘
      [self setNeedsDisplay];
    
      _instansCount++;
    }
    
    #pragma mark - 6、点击开始动画(重写开始动画的方法)
    - (void)startAnim {
      _dotLayer.hidden = NO; // 开始动画的时候显示图层
    
      // 创建帧动画
      CAKeyframeAnimation *anim = [CAKeyframeAnimation animation];
    
      // 动画属性
      anim.keyPath = @"position";
      anim.path = _path.CGPath;
      anim.duration = 3;
      anim.repeatCount = MAXFLOAT;
    
      // 添加动画到 dotLayer 图层上
      [self.dotLayer addAnimation:anim forKey:nil];
    
      // 复制子层的数(如果复制的子层有动画,先添加动画,在复制)
      _repL.instanceCount = _instansCount;
      // 设置图层延时动画
      _repL.instanceDelay = 0.2;
    }
    
    #pragma mark - 7、懒加载点图层
    - (CALayer *)dotLayer {
      if (_dotLayer == nil) {
        // 创建粒子图层
        CALayer *layer = [CALayer layer];
    
        // 设置图层属性
        CGFloat wh = 10;
        layer.frame = CGRectMake(0, -1000, wh, wh);
        layer.cornerRadius = wh / 2;
        layer.backgroundColor = [UIColor blueColor].CGColor;
    
        // 添加图层到复制层上
        [_repL addSublayer:layer];
    
        _dotLayer = layer;
      }
      return _dotLayer;
    }
    
    #pragma mark - 8、点击重绘(重写重绘方法)
    - (void)reDraw {
      _path = nil;            // 清空绘图信息
      [self setNeedsDisplay]; // 重绘
    
      // 把图层移除父控件,复制层也会移除。
      [_dotLayer removeFromSuperlayer];
      _dotLayer = nil;   // 清空点图层
      _instansCount = 0; // 清空子层总数
    }
    
    @end
    

    运行效果:

    粒子效果.gif

    4.5 倒影效果

    实现思路:

    1、通常做法:用复制图层实现,搞个UIImageView展示图片,然后复制UIImageView.

    注意:复制图层只能复制子层,但是UIImageView只有一个主层,并没有子层,因此不能直接复制UIImageView
    

    正确做法:应该把UIImageView添加到一个UIView上,然后复制UIView的层,就能复制UIImageView

    注意:默认A控件是B控件的子控件,那么A控件的层就是B控件的层的子层。
    

    2、但是有问题,默认UIView的层不是复制层,我们想把UIView的层变成复制层,重写+layerClass方法。

    3、倒影效果:就是就是把复制图片旋转180度,然后往下平移,最好先偏移在,在旋转。


    代码示例:

    RepView.m

    #import "RepView.h"
    
    @implementation RepView
    
    // 设置控件主层的类型
    + (Class)layerClass {
      return [CAReplicatorLayer class]; // 设置为复制层
    }
    
    @end
    

    ViewController.m

    #import "RepView.h"
    #import "ViewController.h"
    
    @interface ViewController ()
    @property(weak, nonatomic) IBOutlet RepView *repView;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
      [super viewDidLoad];
    
      CAReplicatorLayer *layer = (CAReplicatorLayer *)_repView.layer;
    
      layer.instanceCount = 2;
    
      // 设置平移(向下平移)
      CATransform3D transform =
          CATransform3DMakeTranslation(0, _repView.bounds.size.height, 0);
    
      // 设置旋转:绕着X轴旋转(180度)
      transform = CATransform3DRotate(transform, M_PI, 1, 0, 0);
    
      // 设置复制层的形变(将旋转和平移的数据设置在layer上)
      layer.instanceTransform = transform;
    
      // 设置颜色通道的偏移量
      layer.instanceAlphaOffset = -0.2;
      layer.instanceBlueOffset = -0.5;
      layer.instanceGreenOffset = -0.5;
      layer.instanceRedOffset = -0.5;
    }
    
    - (void)didReceiveMemoryWarning {
      [super didReceiveMemoryWarning];
      // Dispose of any resources that can be recreated.
    }
    
    @end
    

    运行效果:

    倒影效果.png

    4.6 QQ粘性效果

    实现思路:

    步骤1、自定义大圆控件(UIButton)可以显示背景图片,和文字

    步骤2、让大圆控件随着手指移动而移动(不能根据形变修改大圆的位置,只能通过center,因为全程都需要用到中心点计算)

    步骤3、在拖动的时候,添加一个小圆控件在原来大圆控件的位置
    - 注意这个小圆控件并不会随着手指移动而移动,因此应该添加到父控件上
    
    - 一开始设置中心点和尺寸和大圆控件一样
    
    - 随着大圆拖动,小圆半径不断减少,可以根据两个圆心的距离,随便生成一段比例,随着圆心距离
    增加,圆心半径不断减少
    
    - 每次小圆改变,需要重新设置小圆的尺寸和圆角半径
    
    

    步骤4、粘性效果:就是在两圆之间绘制一个形变矩形,描述形变矩形路径
    • 这里需要用到CAShapeLayer,可以根据一个路径,生成一个图层,展示出来。把形变图层添加到父控件并且显示在小圆图层下就OK了。因为所有计算出来的点,都是基于父控件。
    注意:这里不能用绘图,因为绘图内容只要超过当前控件尺寸就不会显示,但是当前形变矩形必须显示
    在控件之外
    

    先复习下对边、邻边、斜边
    • 对边:这角的对面的线
    • 邻边:这个角的邻居.组成这个角的两条线
    • 斜边:直角三角形三条线中最长的这条线(直角的对面的这么条)
    cos Θ = 邻边 / 斜边
    sin Θ  =对边 / 斜边
    
    附赠:角度、弧度互转图表

    角度、弧度互转图表.png

    粘性计算图:

    粘性计算图.png

    关于不规则矩形的处理:(使用 CAShapeLayer)
    - 普通CALayer在被初始化时是需要给一个frame值的,这个frame值一般都与给定view的bounds值一致,它本身是有形状的,而且是矩形.
    
    - CAShapeLayer在初始化时也需要给一个frame值,但是,它本身没有形状,它的形状来源于你给定的一个path(因此必须给一个path,而且,即使path不完整也会自动首尾相接)然后它会去取CGPath值
    

    步骤5、粘性业务逻辑处理
    - 当圆心距离超过100,就不需要描述形变矩形(并且把之前的形变矩形移除父层),小圆也需要隐藏。
    - 没有超过100,则相反。
    

    步骤6、手指停止拖动业务逻辑
    - 判断下圆心是否超过100,超过就播放爆炸效果,添加个UIImageView在当前控件上,并且需要取消控制器view的自动布局。
    - 没有超过,就还原。
    

    代码示例:
    ViewController.m

    #import "ViewController.h"
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
      [super viewDidLoad];
      // 取消自动布局
      self.view.translatesAutoresizingMaskIntoConstraints = NO;
    }
    
    - (void)didReceiveMemoryWarning {
      [super didReceiveMemoryWarning];
      // Dispose of any resources that can be recreated.
    }
    
    @end
    

    GooView.m(注意GooView继承自UIButton,且 SB中的UIButton的class要关联GooView)

    #import "GooView.h"
    
    @interface GooView ()
    
    @property(nonatomic, weak) UIView *smallCircleView;  // 小圆
    @property(nonatomic, assign) CGFloat oriSmallRadius; // 小圆半径
    @property(nonatomic, weak) CAShapeLayer *shapeLayer; // 不规则矩形
    
    @end
    
    @implementation GooView
    
    #pragma mark - 不规则矩形的懒加载(两圆产生距离才需要绘制)
    - (CAShapeLayer *)shapeLayer {
        if (_shapeLayer == nil) {
            CAShapeLayer *layer = [CAShapeLayer layer]; // 创建不规则矩形
            _shapeLayer = layer;
            layer.fillColor = self.backgroundColor.CGColor; // 设置不规则矩形的填充颜色
    
            // 不规则矩形添加按钮的父控件上的layer
            [self.superview.layer insertSublayer:layer below:self.layer];
        }
    
        return _shapeLayer;
    }
    
    #pragma mark - 小圆懒加载
    - (UIView *)smallCircleView {
        if (_smallCircleView == nil) {
            UIView *view = [[UIView alloc] init];        // 创建
            view.backgroundColor = self.backgroundColor; // 背景色
            _smallCircleView = view;
    
            // 小圆添加按钮的父控件上
            [self.superview insertSubview:view belowSubview:self];
        }
        return _smallCircleView;
    }
    
    - (void)awakeFromNib {
        [self setUp];
    }
    
    - (instancetype)initWithFrame:(CGRect)frame {
        if (self = [super initWithFrame:frame]) {
            [self setUp];
        }
        return self;
    }
    
    #pragma mark - 初始化
    - (void)setUp {
        CGFloat w = self.bounds.size.width;
    
        // 记录小圆最初始半径
        _oriSmallRadius = w / 2;
        // 设置最初始圆角半径
        self.layer.cornerRadius = w / 2;
        // 设置小圆的文字颜色
        [self setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
        // 设置小圆的文字字体大小
        self.titleLabel.font = [UIFont systemFontOfSize:12];
    
        // 添加手势事件(不用touchesBegan,因为会跟按钮监听事件冲突)
        UIPanGestureRecognizer *pan =
        [[UIPanGestureRecognizer alloc] initWithTarget:self
                                                action:@selector(pan:)];
        // 添加手势到控件上
        [self addGestureRecognizer:pan];
    
        // 设置小圆位置、尺寸和圆角半径
        self.smallCircleView.center = self.center;
        self.smallCircleView.bounds = self.bounds;
        self.smallCircleView.layer.cornerRadius = w / 2;
    }
    
    // 最大圆心距离
    #define kMaxDistance 80
    
    // 计算两个圆心之间的距离
    - (CGFloat)circleCenterDistanceWithBigCircleCenter:(CGPoint)bigCircleCenter
                                     smallCircleCenter:(CGPoint)smallCircleCenter {
    
        CGFloat offsetX = bigCircleCenter.x - smallCircleCenter.x; // x2-x1
        CGFloat offsetY = bigCircleCenter.y - smallCircleCenter.y; // y2-y1
        // 开平方得到两个圆心之间的距离
        return sqrt(offsetX * offsetX + offsetY * offsetY);
    }
    
    // 描述两圆之间一条矩形路径
    - (UIBezierPath *)pathWithBigCirCleView:(UIView *)bigCirCleView
                            smallCirCleView:(UIView *)smallCirCleView {
    
        // 大圆的圆心、x、y、半径
        CGPoint bigCenter = bigCirCleView.center;
        CGFloat x2 = bigCenter.x;
        CGFloat y2 = bigCenter.y;
        CGFloat r2 = bigCirCleView.bounds.size.width / 2;
    
        // 小圆的圆心、x、y、半径
        CGPoint smallCenter = smallCirCleView.center;
        CGFloat x1 = smallCenter.x;
        CGFloat y1 = smallCenter.y;
        CGFloat r1 = smallCirCleView.bounds.size.width / 2;
    
        // 获取圆心距离
        CGFloat d = [self circleCenterDistanceWithBigCircleCenter:bigCenter
                                                smallCircleCenter:smallCenter];
    
        CGFloat sinθ = (x2 - x1) / d;
        CGFloat cosθ = (y2 - y1) / d;
    
        // 坐标系是基于父控件计算的
        CGPoint pointA = CGPointMake(x1 - r1 * cosθ, y1 + r1 * sinθ);
        CGPoint pointB = CGPointMake(x1 + r1 * cosθ, y1 - r1 * sinθ);
        CGPoint pointC = CGPointMake(x2 + r2 * cosθ, y2 - r2 * sinθ);
        CGPoint pointD = CGPointMake(x2 - r2 * cosθ, y2 + r2 * sinθ);
        CGPoint pointO =
        CGPointMake(pointA.x + d / 2 * sinθ, pointA.y + d / 2 * cosθ);
        CGPoint pointP =
        CGPointMake(pointB.x + d / 2 * sinθ, pointB.y + d / 2 * cosθ);
    
        // 1、创建一个路径对象
        UIBezierPath *path = [UIBezierPath bezierPath];
    
        // 2、一个封闭路径(A-B-C-D-A)
        [path moveToPoint:pointA];                             // 起点:A
        [path addLineToPoint:pointB];                          // 添加线AB
        [path addQuadCurveToPoint:pointC controlPoint:pointP]; // 绘制BC曲线
        [path addLineToPoint:pointD];                          // 添加线CD
        [path addQuadCurveToPoint:pointA controlPoint:pointO]; // 绘制DA曲线
    
        // 3、返回path
        return path;
    }
    
    - (void)pan:(UIPanGestureRecognizer *)pan {
    #warning 移动控件位置
        // 获取手指偏移量
        CGPoint transP = [pan translationInView:self];
    
        // 修改按钮的形变,并不会修改中心点,因此要用下面的方法
        //    self.transform = CGAffineTransformTranslate(self.transform, transP.x,
        //    transP.y);
    
        // 通过修改center的xy,用以修改按钮的形变
        CGPoint center = self.center;
        center.x += transP.x;
        center.y += transP.y;
    
        // 将修改后的参数赋值
        self.center = center;
    
        // 复位
        [pan setTranslation:CGPointZero inView:self];
    
    #warning 设置小圆半径(小圆半径随着大小两个圆的圆心的距离不断增加而减小)
        // 计算圆心距离
        CGFloat d = [self
                     circleCenterDistanceWithBigCircleCenter:self.center
                     smallCircleCenter:self.smallCircleView.center];
    
        // 计算小圆的半径(随着圆心距的不断变化而变化)
        CGFloat smallRadius = _oriSmallRadius - d / 10;
    
        // 设置小圆的尺寸(随着小圆半径的不断变化而变化)
        self.smallCircleView.bounds =
        CGRectMake(0, 0, smallRadius * 2, smallRadius * 2);
    
        // 设置小圆的圆角半径(随着小圆半径的不断变化而变化)
        self.smallCircleView.layer.cornerRadius = smallRadius;
    
    #warning 绘制不规则矩形
        /*
         遇到的问题:不能通过绘图,因为绘图只能在当前控件上画,超出部分不会显示
         解决方法:展示不规则矩形,通过不规则矩形路径生成一个图层
         */
    
        // 当圆心距离大于最大圆心距离的时候,可以拖出来
        if (d > kMaxDistance) {
            self.smallCircleView.hidden = YES;      // 隐藏小圆
            [self.shapeLayer removeFromSuperlayer]; // 移除不规则的矩形
            self.shapeLayer = nil;                  // 清空不规则的矩形的内容
        } else if (d > 0 && self.smallCircleView.hidden == NO) {
            // 当有圆心距离,并且圆心距离不大的时候,展示不规则矩形
            self.shapeLayer.path =
            [self pathWithBigCirCleView:self smallCirCleView:self.smallCircleView]
            .CGPath;
        }
    
    #warning 手指抬起的时候,还原
        if (pan.state == UIGestureRecognizerStateEnded) {
    
            // 当圆心距离大于最大圆心距离的时候,展示gif动画
            if (d > kMaxDistance) {
                // 创建imageView,设置Frame
                UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.bounds];
    
                // for循环展示图片
                NSMutableArray *arrM = [NSMutableArray array];
                for (int i = 1; i < 9; i++) {
                    UIImage *image =
                    [UIImage imageNamed:[NSString stringWithFormat:@"%d", i]];
                    [arrM addObject:image];
                }
    
                imageView.animationImages = arrM;   // 将图片添加到数组
                imageView.animationRepeatCount = 1; // 设置动画次数
                imageView.animationDuration = 0.5;  // 设置动画时间
                [imageView startAnimating];         // 开始动画
                [self addSubview:imageView];        // 添加图片
    
                // 延迟0.4s,移除控件
                dispatch_after(
                               dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.4 * NSEC_PER_SEC)),
                               dispatch_get_main_queue(), ^{
                                   [self removeFromSuperview];
                               });
    
            } else { // 当圆心距离小于最大圆心距离的时候
                [self.shapeLayer removeFromSuperlayer]; // 移除不规则矩形
                self.shapeLayer = nil; // 清空不规则的矩形的内容
    
                // 当抬起手指的时候,还原位置、显示小圆
                [UIView animateWithDuration:0.5
                                      delay:0
                     usingSpringWithDamping:0.2
                      initialSpringVelocity:0
                                    options:UIViewAnimationOptionCurveLinear
                                 animations:^{
                                     self.center = self.smallCircleView.center; // 设置大圆中心点位置
                                 }
                                 completion:^(BOOL finished) {
                                     self.smallCircleView.hidden = NO; // 显示小圆
                                 }];
            }
        }
    }
    
    @end
    

    运行效果:

    QQ粘性效果.gif

    本文源码 Demo 详见 Github
    https://github.com/shorfng/iOS_6.0_Graphics_and_multimedia.git




    作者:蓝田(Loto)
    出处: 简书

    如果你觉得本篇文章对你有所帮助,请点击文章末尾下方“喜欢”
    如有疑问,请通过以下方式交流:
    评论区回复微信(加好友请注明“简书+称呼”)发送邮件shorfng@126.com



    本文版权归作者和本网站共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。

    相关文章

      网友评论

      本文标题:6.2 核心动画->4.0 常用动画效果

      本文链接:https://www.haomeiwen.com/subject/umbgjttx.html