zoukankan      html  css  js  c++  java
  • iOS开发 QQ粘性动画效果

    QQ(iOS)客户端的粘性动画效果

    qq的app中要是有新的联系人发消息过来,相应联系人的cell右边会有一个红色的圆圈表示消息条数。如果去触碰那个圆圈,可以发现它竟然会跟着手指的移动而移动。

    在一定范围内,手指离开屏幕,会发现红色圆圈会自动弹性的回到原来的位置。而如果超出一定距离,这个圆圈会做一个销毁的动画,从而从view上移除掉。 

    产品要求公司的App也要有效果,并花了些时间去学习它的实现过程,发现其实原理还是比较简单的。

    (由于mac制作gif图片实在过于麻烦,所以效果只能是看看图片。)

    Demo的github地址:https://github.com/wzpziyi1/QQ-Goo

    这是实现过程中的一些效果图片:

    经过分析,可以发现,是两个圆和一个不规则矩形位置、大小的变化。一开始,小的圆圈和大的圆圈的center是相同的,当移动大圆的时候,小圆的半径随着大圆离小圆的距离变远而变小,当大圆距离小圆一定距离时,将小圆隐藏掉,中间的不规则矩形remove掉。

    那么,不规则矩形怎么表示呢?可以利用Core Graphics在drawRect方法里面绘制不规则矩形的path,然后利用颜色fill就行。不规则矩形是随着大圆的移动而不断变化的,如果在drawRect方法里面绘制,那么在移动过程中不断调用setNeedsDisplay方法进行重绘。这是种可行的方案,我所用的也大致是这种思路。

    不过,我没有在drawRect方法里面绘制,而是利用了CAShapeLayer,将不规则矩形的path绘制在shapeLayer里面,这样在移动大圆的过程中不断更新CAShapeLayer的path即可。

    当然,难点并在在这里。而是不规则矩形的各个点的位置。要绘制这个不规则矩形,需要知道六个点的位置:

    有了这些点的坐标,那么就可以用UIBezierPath来绘制相应的路径,代码如下:

    - (UIBezierPath *)pathWithBigCircleView:(UIView *)bigCircleView smallCircleView:(UIView *)smallCircleView
    {
        
        CGPoint bigCenter = bigCircleView.center;
        CGFloat x2 = bigCenter.x;
        CGFloat y2 = bigCenter.y;
        CGFloat r2 = bigCircleView.bounds.size.width / 2;
        
        CGPoint smallCenter = smallCircleView.center;
        CGFloat x1 = smallCenter.x;
        CGFloat y1 = smallCenter.y;
        CGFloat r1 = smallCircleView.bounds.size.width / 2;
        
        
        
        // 获取圆心距离
        CGFloat d = [self distanceWithPointA:bigCenter pointB:smallCenter];
        
        //Θ:(xita)
        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θ);
        
        UIBezierPath *path = [UIBezierPath bezierPath];
        
        // A
        [path moveToPoint:pointA];
        
        // AB
        [path addLineToPoint:pointB];
        
        // 绘制BC曲线
        [path addQuadCurveToPoint:pointC controlPoint:pointP];
        
        
        // CD
        [path addLineToPoint:pointD];
        
        // 绘制DA曲线
        [path addQuadCurveToPoint:pointA controlPoint:pointO];
        
        return path;
    }

    在实现过程中,我是自定义UIButton的,需要注意的是,在监听button的拖动时,最好是给它添加UIPanGestureRecognizer手势,而不要在touchesBegin方法里面去判断它的移动位置,因为Touches系列方法会屏蔽button的点击。

    自定义的这个button默认就是大圆,包含一个小圆(UIView)属性,但是这个小圆并不是添加在自定义的这个button(也就是大圆)里面,而是在button的superView上。因为小圆并不需要随着大圆位置的改变而改变位置,相应的,shapeLayer也是添加在button(大圆)的父控件上。

    给大圆添加了pan手势,在pan:方法里面随之改变小圆的大小和绘制shapeLayer的path。

    当pan手势状态为End的时候,需要判断大圆与小圆的距离有没有超出最大距离,如果超过,那么添加一个gif图片,播放销毁大圆的过程。如果没有被销毁,那么大圆需要复位,相应代码: 

    #import "ZYGooView.h"
    
    #define kMaxDistance 100
    
    @interface ZYGooView ()
    @property (nonatomic, weak) UIView *smallCircleView;
    
    @property (nonatomic, assign) CGFloat smallCircleR;
    
    @property (nonatomic, weak) CAShapeLayer *shapeLayer;
    @end
    
    @implementation ZYGooView
    
    - (instancetype)initWithFrame:(CGRect)frame
    {
        if (self = [super initWithFrame:frame]) {
            [self commitInit];
            
        }
        return self;
    }
    
    - (void)awakeFromNib
    {
        [self commitInit];
    }
    
    - (void)commitInit
    {
        self.layer.cornerRadius = self.frame.size.width * 0.5;
        self.layer.masksToBounds = YES;
        
        self.smallCircleR = self.frame.size.width * 0.5;
        self.smallCircleView.bounds = self.bounds;
        self.smallCircleView.center = self.center;
        self.smallCircleView.layer.cornerRadius = self.smallCircleView.frame.size.width * 0.5;
        
        [self addGesture];
    }
    
    #pragma mark ----懒加载方法
    
    - (UIView *)smallCircleView
    {
        if (_smallCircleView == nil) {
            UIView *view = [[UIView alloc] init];
            
            view.backgroundColor = self.backgroundColor;
            
            [self.superview addSubview:view];
            
            [self.superview insertSubview:view atIndex:0];
            
            _smallCircleView = view;
            
        }
        return _smallCircleView;
    }
    
    - (CAShapeLayer *)shapeLayer
    {
        if (_shapeLayer == nil) {
            CAShapeLayer *shapeLayer = [CAShapeLayer layer];
            shapeLayer.path = [self pathWithBigCircleView:self smallCircleView:self.smallCircleView].CGPath;
            shapeLayer.fillColor = self.backgroundColor.CGColor;
            
            [self.superview.layer addSublayer:shapeLayer];
            
            [self.superview.layer insertSublayer:shapeLayer atIndex:0];
            
            _shapeLayer = shapeLayer;
        }
        return _shapeLayer;
    }
    
    #pragma mark ----其他方法
    
    - (void)addGesture
    {
        UIPanGestureRecognizer *recognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];
        [self addGestureRecognizer:recognizer];
    }
    
    
    
    - (void)pan:(UIPanGestureRecognizer *)recognizer
    {
        CGPoint point = [recognizer translationInView:self.superview];
        
        CGPoint center = self.center;
        center.x += point.x;
        center.y += point.y;
        self.center = center;
        //复位
        [recognizer setTranslation:CGPointZero inView:self];
        
        CGFloat distance = [self distanceWithPointA:self.smallCircleView.center pointB:self.center];
        
        if (distance == 0) return;
        
        CGFloat newR = self.smallCircleR - distance / 15.0;
        NSLog(@"%f", newR);
        self.smallCircleView.bounds = CGRectMake(0, 0, newR * 2, newR * 2);
        self.smallCircleView.layer.cornerRadius = newR;
        
        if (distance > kMaxDistance || newR <= 0) {
            self.smallCircleView.hidden = YES;
            [self.shapeLayer removeFromSuperlayer];
            self.shapeLayer = nil;
        }
        
        if (distance <= kMaxDistance && self.smallCircleView.hidden == NO) {
            self.shapeLayer.path = [self pathWithBigCircleView:self smallCircleView:self.smallCircleView].CGPath;
        }
        
        if (recognizer.state == UIGestureRecognizerStateEnded) {
            if (distance <= kMaxDistance) {
                
                dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.03 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                    [self.shapeLayer removeFromSuperlayer];
                    self.shapeLayer = nil;
                });
                
                [UIView animateWithDuration:0.4 delay:0 usingSpringWithDamping:0.6 initialSpringVelocity:0 options:UIViewAnimationOptionCurveLinear animations:^{
                    self.center = self.smallCircleView.center;
                    
                } completion:^(BOOL finished) {
                    self.smallCircleView.hidden = NO;
                }];
            }
            else {
                UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.bounds];
                [self addSubview:imageView];
                
                NSMutableArray *images = [NSMutableArray array];
                
                for (int i = 1; i <= 8; i++) {
                    NSString *imageName = [NSString stringWithFormat:@"%d", i];
                    UIImage *image = [UIImage imageNamed:imageName];
                    [images addObject:image];
                }
                
                imageView.animationImages = images;
                imageView.animationDuration = 0.6;
                imageView.animationRepeatCount = 1;
                [imageView startAnimating];
                
                dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                    [self removeFromSuperview];
                });
            }
        }
    }
    
    - (CGFloat)distanceWithPointA:(CGPoint)pointA pointB:(CGPoint)pointB
    {
        CGFloat dx = pointB.x - pointA.x;
        CGFloat dy = pointB.y - pointA.y;
        
        return sqrt(dx * dx + dy * dy);
    }
    
    - (UIBezierPath *)pathWithBigCircleView:(UIView *)bigCircleView smallCircleView:(UIView *)smallCircleView
    {
        
        CGPoint bigCenter = bigCircleView.center;
        CGFloat x2 = bigCenter.x;
        CGFloat y2 = bigCenter.y;
        CGFloat r2 = bigCircleView.bounds.size.width / 2;
        
        CGPoint smallCenter = smallCircleView.center;
        CGFloat x1 = smallCenter.x;
        CGFloat y1 = smallCenter.y;
        CGFloat r1 = smallCircleView.bounds.size.width / 2;
        
        
        
        // 获取圆心距离
        CGFloat d = [self distanceWithPointA:bigCenter pointB:smallCenter];
        
        //Θ:(xita)
        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θ);
        
        UIBezierPath *path = [UIBezierPath bezierPath];
        
        // A
        [path moveToPoint:pointA];
        
        // AB
        [path addLineToPoint:pointB];
        
        // 绘制BC曲线
        [path addQuadCurveToPoint:pointC controlPoint:pointP];
        
        
        // CD
        [path addLineToPoint:pointD];
        
        // 绘制DA曲线
        [path addQuadCurveToPoint:pointA controlPoint:pointO];
        
        return path;
    }
    
    @end

     

  • 相关阅读:
    Java中的匿名对象、内部类、包、代码块
    Java中的final和static关键字
    Java中的this与super关键字
    Java中的构造方法
    Java中的多态
    Vue+Element中Table懒加载,新增、删除操作后手动更新
    JQ取消hover事件
    github.com访问慢解决
    vue cli3 子目录问题
    右键事件 contextmenu
  • 原文地址:https://www.cnblogs.com/diweinan/p/6230533.html
Copyright © 2011-2022 走看看