iOS Developer - AnimationiOS 核心动画iOS动效

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

2016-07-26  本文已影响469人  蓝田_Loto

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


本文相关目录:
===================== 所属文集: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、粘性效果:就是在两圆之间绘制一个形变矩形,描述形变矩形路径
注意:这里不能用绘图,因为绘图内容只要超过当前控件尺寸就不会显示,但是当前形变矩形必须显示
在控件之外

先复习下对边、邻边、斜边
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



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

上一篇下一篇

猜你喜欢

热点阅读