动感小球(iOS)

偶然间发现QQ的消息挺好玩的,应用内收到新消息,红色的提醒圆圈可以拉伸并拖动。很有意思,决定自己试一下。

先上效果图:


效果.gif

接下来,我们一步一步来实现之。

我们在拖动过程中,已知哪些信息:
初始的半圆心位置A,半径R。

接下来我们可以通过用户触摸的位置获取以下信息:
拖动的半圆形位置B。
(注:并不是触摸的点就是新的圆点,需要进行加工。开始触摸的点为Touch,触摸变化的点为Touch',新的圆点为A+(Touch'-Touch))。

我们根据当前拖动的距离与最大拖动距离进行求比率,再乘以初始半径R得到拖动半径R'。

我们先取一个中间状态来分析一下:

动感小球(iOS)_第1张图片
分析图

我们在拖动时,小球会被分为两个部分,两个半圆圆心的连线与水平线的夹角为α。

最关键的一步是:计算出α的值。

根据同角的余角相等,以及三角函数能的到:

sinα = (B.x-A.x) / AB的长度

然后通过反正弦函数,可以得到角α的值。

(注:虽然使用正切函数在数值计算时很方便,但是反正切函数在0位置会发生突变,无法满足我们拖动时渐变的效果,故舍弃。数学不行,在这个坑里困了好久。。)

计算出角α就能开始绘图了。
我们一一计算出图中M、N、P、Q四个点的坐标,然后开始操作:

1 从M点开始,A为圆心,N为终点,绘制半圆。(使用α角)
2 从N点开始,P点为终点,绘制贝塞尔曲线。
3 从P点开始,B为圆心,Q为终点,绘制半圆。(使用α角)
4 从Q点开始,M点为终点,绘制贝塞尔曲线。

绘制时,会发现步骤2和4的控制点不好确定,但是如果不确定的话,没法绘制。如果使用两个圆心的中点为控制点,会发现初始拖动时两个半圆中间缝隙很大,不够平滑。

此时我们精简一下模型,看下部的图:
C是AB中点,过C点作MN的平行线。

NP的控制点位于C点的右上侧。
MQ的控制点位于C点点左下侧。

两个控制点是根据拖动的距离变化的。取极限即可得到两个点对应的一次比率关系。

这样就能够完成步骤2和4了。

此时我们就完成了拖动时的效果了。

- (void)updateCircleWithOriginCenter:(CGPoint)originCenter withNewCenter:(CGPoint)newCenter
{
   
     //拖拽的距离,等于两个圆心的距离
   double moveDistance = (double)sqrt((newCenter.y-originCenter.y)*(newCenter.y-originCenter.y) + (newCenter.x-originCenter.x)*(newCenter.x-originCenter.x));
   if (moveDistance == 0) {
       return;
   }
   //移动的水平角度(两圆心连线与水平面夹角)
   //反正切函数很方便,但因为反正切函数在0的位置突变,从-M_PI/2变为 M_PI/2,无法满足我们拖动时的渐变需求,故舍弃。我们使用反正弦函数。
   //正弦函数=对边/斜边  两个圆心之间的连线为斜边,对边是Y轴的垂直距离
   double sinValue = (double)(newCenter.y-originCenter.y)/(double)sqrt((newCenter.y-originCenter.y)*(newCenter.y-originCenter.y) + (newCenter.x-originCenter.x)*(newCenter.x-originCenter.x));
   //获取弧度
   double angle = asin(sinValue);
   
   double rate = moveDistance/self.pullDis;
   
   if (rate >= 1) {
       rate = 1;
       
       //如果拖拽结束了,那就不做操作,否则会死循环。因为拖拽结束,会进行恢复绘制,恢复绘制方法中会调用本方法,然后本方法再调用恢复绘制方法。。。
       if (self.isEnding) {
           return;
       }
       
       //达到最大值,断开
       self.isEnding = YES;
       
       //如果有回调,则触发
       if (self.pullBlock) {
           self.pullBlock();
       }        
       
       if (self.needAnimation) {
           
           //将原始位置的圆进行恢复绘制,就是把原始位置的小圆拉过来。在手指触摸的位置合二为一
           [self backCircleWithTotal:100 withCurrent:0 withTotalTime:0.05 withOriginCenter:newCenter withEndCenter:originCenter];
       }else{
           
           //拉伸到最大值时,如果不需要动画,且是用户设置的类型,那么隐藏layer
           if (self.circleType == CircleTypeSet) {
               self.isSetMaxValue = YES;
               [self resetOriginCircle];
               self.pointLayer.hidden = YES;
           }

       }
       
       //如果有文本,隐藏文本
       if (self.contentString) {
           self.contentTextLayer.hidden = YES;
       }
       return;
   }
   
   CGFloat bigCircleRate = self.bigChangeRate > 0 ? self.bigChangeRate:1/3;
   CGFloat smallCircleRate = self.smallChangeRate > 0 ? self.smallChangeRate:1;
   
   //新的半径
   CGFloat newRadius = 0;
   switch (self.pullType) {
       case PullTypeLeaveBig:
       {
           //如果是拉到最大值,结束绘制,合二为一的过程,那么起始圆和目标圆半径互换
           //比如说:大半径留在原地,小半径被拖拽走的这种情况
           //当我向外拉的时候:拖动的圆半径较小,小圆移动
           //当我未拉到最大值,小圆回去,小圆移动
           //当我拉到最大值,大圆向小圆合并。大圆移动
           if (self.isEnding) {
               self.circleR = self.originR*((1-rate*smallCircleRate)>self.minRate?(1-rate*smallCircleRate):self.minRate);
               newRadius = self.originR*((1-rate*bigCircleRate)>self.minRate?(1-rate*bigCircleRate):self.minRate);

           }else{
               newRadius = self.originR*((1-rate*smallCircleRate)>self.minRate?(1-rate*smallCircleRate):self.minRate);
               self.circleR = self.originR*((1-rate*bigCircleRate)>self.minRate?(1-rate*bigCircleRate):self.minRate);
           }
       }
           break;
           
       case PullTypeMoveBig:
       {
           if (self.isEnding) {
               newRadius = self.originR*((1-rate*smallCircleRate)>self.minRate?(1-rate*smallCircleRate):self.minRate);
               self.circleR = self.originR*((1-rate*bigCircleRate)>self.minRate?(1-rate*bigCircleRate):self.minRate);

           }else{
               self.circleR = self.originR*((1-rate*smallCircleRate)>self.minRate?(1-rate*smallCircleRate):self.minRate);
               newRadius = self.originR*((1-rate*bigCircleRate)>self.minRate?(1-rate*bigCircleRate):self.minRate);
           }
           
       }
           break;
       default:
           break;
   }
       
   //创建新的BezierPath
   UIBezierPath *path = [UIBezierPath bezierPath];
   if (newCenter.x-originCenter.x < 0) {
       
       //圆的中心对称轴,左侧与右侧的计算不一样
       
       //初始圆的底部的点
       CGPoint originBottomPoint = CGPointMake(originCenter.x+sin(angle)*self.circleR, originCenter.y+cos(angle)*self.circleR);
       //初始圆的顶部的点
       CGPoint originTopPoint = CGPointMake(originCenter.x-sin(angle)*self.circleR, originCenter.y-cos(angle)*self.circleR);
       
       //新圆心的左上角的点
       CGPoint newTopPoint = CGPointMake(newCenter.x-sin(angle)*newRadius, newCenter.y-cos(angle)*newRadius);
       //新圆心的下部的点
       CGPoint newBottomPoint = CGPointMake(newCenter.x+sin(angle)*newRadius, newCenter.y+cos(angle)*newRadius);
       
       //两圆心连线中点
       CGPoint controlPoint = CGPointMake(originCenter.x+(newCenter.x-originCenter.x)/2, originCenter.y+(newCenter.y-originCenter.y)/2);
       
       //初始圆顶部点与新圆的顶部点连线的中点
       CGPoint topMiddlePoint = CGPointMake((newTopPoint.x+originTopPoint.x)/2, (newTopPoint.y+originTopPoint.y)/2);
       
       //上部控制点的X坐标,与拉伸比例有关,未拉伸时,取顶部点连线中点,拉伸最大时,取两圆心连线中点
       CGFloat topX = topMiddlePoint.x + (controlPoint.x-topMiddlePoint.x)*rate;
       //上部控制点的Y坐标,与拉伸比例有关
       CGFloat topY = topMiddlePoint.y + (controlPoint.y-topMiddlePoint.y)*rate;
       
       //拉伸时,上部的控制点。不断变化的
       CGPoint topControlPoint = CGPointMake(topX, topY);
       
       //两个圆下部点连线的中点
       CGPoint bottomMiddlePoint = CGPointMake((newBottomPoint.x+originBottomPoint.x)/2, (newBottomPoint.y+originBottomPoint.y)/2);
       //下部点的x,随比例变化
       CGFloat bottomX = bottomMiddlePoint.x + (controlPoint.x-bottomMiddlePoint.x)*rate;
       //下部点的y,随比例变化
       CGFloat bottomY = bottomMiddlePoint.y + (controlPoint.y-bottomMiddlePoint.y)*rate;
       //拉伸时,下部控制点
       CGPoint bottomControlPoint = CGPointMake(bottomX, bottomY);
       
        //移动到初始圆的下部点
       [path moveToPoint:originBottomPoint];
       
       //原始的圆,右半侧,逆时针画圆
       [path addArcWithCenter:originCenter radius:self.circleR startAngle:M_PI/2-angle endAngle:M_PI*3/2-angle clockwise:NO];
       
       //从原始圆的顶部,连线到新圆的顶部,上部点为控制点
       [path addQuadCurveToPoint:newTopPoint controlPoint:topControlPoint];
       
       //新圆的左侧,逆时针画圆
       [path addArcWithCenter:newCenter radius:newRadius startAngle:M_PI*3/2-angle endAngle:M_PI*5/2-angle clockwise:NO];
       
        //从新圆的底部,连接到初始圆的底部点,下部点为控制点
       [path addQuadCurveToPoint:originBottomPoint controlPoint:bottomControlPoint];
       
        }else{
       
       //初始圆的下部点
       CGPoint originBottomPoint = CGPointMake(originCenter.x-sin(angle)*self.circleR, originCenter.y+cos(angle)*self.circleR);
       
       CGPoint originTopPoint = CGPointMake(originCenter.x+sin(angle)*self.circleR, originCenter.y-cos(angle)*self.circleR);
       
       //新圆心的左上角的点
       CGPoint newTopPoint = CGPointMake(newCenter.x+sin(angle)*newRadius, newCenter.y-cos(angle)*newRadius);
       CGPoint newBottomPoint = CGPointMake(newCenter.x-sin(angle)*newRadius, newCenter.y+cos(angle)*newRadius);
       
       //两圆心连线中点
       CGPoint controlPoint = CGPointMake(originCenter.x+(newCenter.x-originCenter.x)/2, originCenter.y+(newCenter.y-originCenter.y)/2);
       
       
       //初始圆顶部点与新圆的顶部点连线的中点
       CGPoint topMiddlePoint = CGPointMake((newTopPoint.x+originTopPoint.x)/2, (newTopPoint.y+originTopPoint.y)/2);
       
       //上部控制点的X坐标,与拉伸比例有关,未拉伸时,取顶部点连线中点,拉伸最大时,取两圆心连线中点
       CGFloat topX = topMiddlePoint.x + (controlPoint.x-topMiddlePoint.x)*rate;
       //上部控制点的Y坐标,与拉伸比例有关
       CGFloat topY = topMiddlePoint.y + (controlPoint.y-topMiddlePoint.y)*rate;
       
       //拉伸时,上部的控制点。不断变化的
       CGPoint topControlPoint = CGPointMake(topX, topY);
       
       //两个圆下部点连线的中点
       CGPoint bottomMiddlePoint = CGPointMake((newBottomPoint.x+originBottomPoint.x)/2, (newBottomPoint.y+originBottomPoint.y)/2);
       //下部点的x,随比例变化
       CGFloat bottomX = bottomMiddlePoint.x + (controlPoint.x-bottomMiddlePoint.x)*rate;
       //下部点的y,随比例变化
       CGFloat bottomY = bottomMiddlePoint.y + (controlPoint.y-bottomMiddlePoint.y)*rate;
       //拉伸时,下部控制点
       CGPoint bottomControlPoint = CGPointMake(bottomX, bottomY);
       
       [path moveToPoint:originBottomPoint];
       
       //原始的圆,左半侧,顺时针画圆
       [path addArcWithCenter:originCenter radius:self.circleR startAngle:M_PI/2+angle endAngle:M_PI*3/2+angle clockwise:YES];
       
       //添加曲线到新圆的顶部
       [path addQuadCurveToPoint:newTopPoint controlPoint:topControlPoint];
       
       //新圆的右侧,顺时针画圆
       [path addArcWithCenter:newCenter radius:newRadius startAngle:M_PI*3/2+angle endAngle:M_PI*5/2+angle clockwise:YES];
       
       //添加曲线到新圆的底部点
       [path addQuadCurveToPoint:originBottomPoint controlPoint:bottomControlPoint];
       
       }
   
   
   //更新layer
   self.pointLayer.path = path.CGPath;

}

接下来就是要处理拖动到一半,松手的处理了。
我们需要拉动的那一部分按照拖动出去的效果,反过来合并到初始圆中。

这里,我们已知这些信息:
初始圆心A,半径R。
松手时圆心B,半径R'。

接下来我们需要做这些操作:
1 计算出直线AB的关系式,然后按比率进行缩小。
2 更新当前的图形。可使用拖动时的函数,只需要
把起始点和结束点调整一下即可。
3 会到初始位置后,反弹动画。

- (void)backCircleWithTotal:(NSInteger)total withCurrent:(NSInteger)current withTotalTime:(CGFloat)totalTime withOriginCenter:(CGPoint)originCenter withEndCenter:(CGPoint)endCenter
{
    NSTimeInterval duration = totalTime/total;
    
    __block NSInteger value = current;
    
    //采用递归处理帧
    if (current <= total) {
        
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(duration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            
            //比率
            CGFloat rate = value*1.f/total*1.f;
            
            //X,Y值的变化率
            CGFloat xChangeValue = endCenter.x-originCenter.x;
            CGFloat yChangeValue = endCenter.y-originCenter.y;
            
            //回弹时,不同时刻的圆心位置
            CGPoint backCenter = CGPointMake(endCenter.x-xChangeValue*rate, endCenter.y-yChangeValue*rate);
            //绘制曲线
            [self updateCircleWithOriginCenter:originCenter withNewCenter:backCenter];
            
            //递归调用,继续更新
            [self backCircleWithTotal:total withCurrent:value+1 withTotalTime:totalTime withOriginCenter:originCenter withEndCenter:endCenter];
            
            if (value == total) {
                //当完成整个回弹时,设置属性,重置圆的位置
                self.isEndAnimation = YES;
            }
        });
    
    }else{
        //如果本次拖拽到最大值了,不需要回弹动画了
        if (self.isEnding) {
           
            return;
        }
        
        //未拉到最大值,放手的动画处理
        CGFloat rate = 0.1;
        
        //X,Y的变化值
        CGFloat xChangeValue = endCenter.x-originCenter.x;
        CGFloat yChangeValue = endCenter.y-originCenter.y;
        
        //初始位置的左上侧
        CGPoint backCenter = CGPointMake(originCenter.x-xChangeValue*rate-self.originR, originCenter.y-yChangeValue*rate-self.originR);
        //初始位置的右下侧
        CGPoint foreCenter = CGPointMake(originCenter.x+xChangeValue*rate/2-self.originR, originCenter.y+yChangeValue*rate/2-self.originR);
        //初始位置的左上侧,较靠近圆心
        CGPoint backTwoCenter = CGPointMake(originCenter.x-xChangeValue*rate/3-self.originR, originCenter.y-yChangeValue*rate/3-self.originR);

        //动画
        CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
        animation.calculationMode = kCAAnimationLinear;
        
        CGMutablePathRef path = CGPathCreateMutable();
        //移动到圆心
        CGPathMoveToPoint(path, NULL,originCenter.x-self.originR, originCenter.y-self.originR);
        //圆心左上侧
        CGPathAddLineToPoint(path, NULL, backCenter.x, backCenter.y);
        //圆心的右下侧
        CGPathAddLineToPoint(path, NULL, foreCenter.x, foreCenter.y);
        //圆心的左上侧,近圆心
        CGPathAddLineToPoint(path, NULL, backTwoCenter.x, backTwoCenter.y);
        //圆心的右下侧,近圆心
        CGPathAddLineToPoint(path, NULL, originCenter.x-self.originR, originCenter.y-self.originR);
        animation.path = path;
        animation.duration = 0.35;
        [self.pointLayer addAnimation:animation forKey:@"pointBackAnimation"];
        
        
        
    }
}

至此,我们就能实现动感的小球了。

有什么意见或者建议请留言哈,共同进步~

代码在这里

你可能感兴趣的:(动感小球(iOS))