仿手機iPhone QQ訊息小紅點動畫1

scorpiozj發表於2015-05-18

前言

 

偶然發現iPhone QQ 顯示訊息條數的小紅點可以響應動作事件,也有人問我這樣的動畫該怎麼做,這裡就把實現的思路簡單的描述一下。在實現的過程中,同樣發現該功能並沒有看到的那麼簡單,要做一個完備的動畫效果需要有一定的功底。因此,本篇會先側重於實現思路,並不會實現一個一模一樣的效果。
下面是iPhone QQ小紅點的動作互動效果:

分析

首先我們分析拖拽時候的表現:

  1. 原先的小紅點順著手指移動,並與原來所處位置通過一個小尾巴(即移動的軌跡)連線
  2. 與原先位置在一定範圍內時,小尾巴出現;超過一定範圍時,小尾巴不出現
  3. 釋放手指,小紅點回到原先位置,並有彈簧動畫效果
  4. 釋放手指時離原先位置超過一定範圍則不返回原點,而是有消失的泡沫動畫

拋開細節,抓住要點,我歸納了幾個要點:

  1. 小原點隨手指移動
  2. 小尾巴分情況出現
  3. 手指釋放後,小紅點彈回原先位置

除此之外,紅點上的文字,消失等情形的處理不是主要問題,我們先緩一緩。

實現

紅點的移動

首先實現一個圓形的view,並且可以隨手指移動。在一定移動範圍內,手指離開後,view返回原處並帶有彈簧效果;超出範圍,view則停留在手指離開處。
我們通過drawRect:來畫一個圓;設定一個CGPoint的物件來記錄開始觸控時的位置;接著就是實現相關的touchEvent:。因為都是很基本的內容,直接上程式碼。

//標頭檔案
@interface ZZSpringView : UIView
- (instancetype)initWithSquareLength:(CGFloat)length originPoint:(CGPoint)oPoint;
@end
//類檔案
const CGFloat kOffset = 100.0;//拖拽的範圍限制
@interface ZZSpringView ()
{
    CGPoint pointOriginCenter;
}
@end

@implementation ZZSpringView
- (instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame])
    {
        pointOriginCenter = self.center;            
        self.backgroundColor = [UIColor clearColor];
    }
    return self;
}
- (instancetype)initWithSquareLength:(CGFloat)length originPoint:(CGPoint)oPoint
{
    if (self = [self initWithFrame:CGRectMake(oPoint.x, oPoint.y, length, length)])
    {

    }
    return self;
}
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
    // Drawing code
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetAllowsAntialiasing(context, true);
    CGContextSetShouldAntialias(context, true);

    CGContextAddEllipseInRect(context, rect);
    CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor);
    CGContextFillPath(context);
}

- (BOOL)_isDistanceEnough:(CGPoint)point
{

    CGFloat distance = (point.x - pointOriginCenter.x)*(point.x - pointOriginCenter.x) + (point.y - pointOriginCenter.y)*(point.y - pointOriginCenter.y);
    if (distance > kOffset * kOffset)
    {
        return YES;
    }
    return NO;
}

//touch event
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch =  [touches anyObject];
    pointOriginCenter = [touch locationInView:self.superview];

    [UIView animateWithDuration:.3 animations:^{
        self.center = pointOriginCenter;
    }];
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{

}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{

    UITouch *touch =  [touches anyObject];
    CGPoint pointMove = [touch locationInView:self.superview];
    self.center = pointMove;
 }

 - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch =  [touches anyObject];
    CGPoint pointEnd = [touch locationInView:self.superview];
    CGFloat distance = (pointEnd.x - pointOriginCenter.x)*(pointEnd.x - pointOriginCenter.x) + (pointEnd.y - pointOriginCenter.y)*(pointEnd.y - pointOriginCenter.y);
    if ([self _isDistanceEnough:pointEnd])
    {
        //may be destory self animation
        self.center = pointEnd;
        pointOriginCenter = self.center;
    }
    else
    {
        [UIView animateWithDuration:1.0
                              delay:.0
             usingSpringWithDamping:0.1
              initialSpringVelocity:.0
                            options:0
                         animations:^{
            self.center = pointOriginCenter;
        }
                         completion:^(BOOL finished) {

        }];

    }
}
ZZSrpingView

 

在touchBegin事件中,因為點選小紅點的位置與中心會有偏移,通過UIView animation做一個平滑的過度。而在touchEnd事件中,返回彈簧震盪的效果是使用UIView的Spring animation。

新增小尾巴(軌跡)

我畫了一張簡化的模擬拖拽過程的圖:

虛線圓是view原來的位置,P0是其圓點;實線圓是移動的位置,P1是圓點。設定兩圓的切線(紅色),把封閉的部分都填充為同一個顏色的話,就能大致模擬出相似的效果。這裡隱含了幾個前提:

  1. 實際的軌跡是帶有弧度的曲線,這裡使用了切線來代替(紅色的切線)
  2. 拖拽的時候,原先位置的圓形view會隨拖拽距離變小,這裡設定為一個固定大小的圓(半徑為原來的一半)

鑑於此,我們需要求出的是兩對切點的位置,使之成為一個封閉圖形進行填充。同時,虛線位置的小圓也進行填充。這樣,就基本完成類似的功能。
首先我們需要擴充套件當前context的範圍,為了簡便,通過新增尾巴的子view來實線,這樣可以利用原先的紅點view。現在我們已知P0,P1,以及各自的半徑,然後求外圍矩形的位置和長度。因為可以按任意方向拖拽,按當前的計算方式,需要分四種情況討論。按笛卡爾座標系的劃分,圖例是第一象限的情形。同理還有二三四的可能。為了迅速驗證方案的可行性,這裡只對第一象限進行討論和模擬。
定義新view:

typedef enum : NSUInteger {
    ZZLineDirection1=1,//northease
    ZZLineDirection2,//northwest
    ZZLineDirection3,//southwest
    ZZLineDirection4//southeast
} ZZLineDirection;

@interface ZZSpringTailView : UIView
@property (nonatomic, assign) ZZLineDirection lineDirection;
@property (nonatomic, assign) CGFloat radius;//centerradius
@property (nonatomic, assign) CGFloat moveRadius;
@end
ZZSpringTailView

 

ZZLineDirection代表的是某象限,radius是P0的半徑,moveRadius為P1半徑。 我們在touchMove事件中新增一個view,在此之前,我們會在ZZSpringView中新增一個ZZSpringTailView例項,用於內部訪問。touchMove的實現更新為:

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{

    UITouch *touch =  [touches anyObject];
    CGPoint pointMove = [touch locationInView:self.superview];

    if ([self _isDistanceEnough:pointMove])
    {
        //beyond the offset, hide the view
        if (tailView)
        {
            tailView.hidden = YES;
        }
    }
    else
    {
        //redraw the view
        self.center = pointMove;
        if (!tailView)
        {
            tailView = [[ZZSpringTailView alloc] init];
            [self addSubview:tailView];
        }
        CGFloat widthHalf = self.bounds.size.width/2.0;

        CGFloat minX = 0;//= MIN(pointMove.x, pointOriginCenter.x);
        CGFloat minY = 0;//= MIN(pointMove.y, pointOriginCenter.y);
        CGFloat radius = widthHalf;


        //the width: the distance betweent two points and the origin size's width/2
        CGRect frameInSuper = CGRectMake(minX, minY, fabsf(pointMove.x - pointOriginCenter.x)  + widthHalf + radius, fabsf(pointMove.y - pointOriginCenter.y)  + widthHalf + radius);

        tailView.radius = radius/2;
        tailView.moveRadius = radius;

        if (pointMove.x >= pointOriginCenter.x && pointMove.y <= pointOriginCenter.y)
        {
            NSLog(@"direnction1");
            tailView.lineDirection = ZZLineDirection1;
            frameInSuper.origin.x = pointOriginCenter.x - radius;
            frameInSuper.origin.y = pointMove.y - radius;

        }
        else if (pointMove.x <= pointOriginCenter.x && pointMove.y <= pointOriginCenter.y)
        {
            NSLog(@"direnction2");
            tailView.lineDirection = ZZLineDirection2;
            frameInSuper.origin.x = pointMove.x ;
            frameInSuper.origin.y = pointMove.y;
        }
        else if (pointMove.x <= pointOriginCenter.x && pointMove.y >= pointOriginCenter.y)
        {
            NSLog(@"direnction3");
            tailView.lineDirection = ZZLineDirection3;
            frameInSuper.origin.x = pointMove.x - radius;
            frameInSuper.origin.y = pointOriginCenter.y;
        }
        else
        {
            NSLog(@"direnction4");
            tailView.lineDirection = ZZLineDirection4;
            frameInSuper.origin.x = pointOriginCenter.x - radius;
            frameInSuper.origin.y = pointOriginCenter.y - radius;
        }
        tailView.frame = [self convertRect:frameInSuper fromView:self.superview];
        [tailView setNeedsDisplay];
    }

}
touchMoveEvent:

 

這裡的實現是把tailview新增到springview之上,通常情況下,clipToBouds預設是NO的,因此這種新增超出父view bound 的子view方案是可行的。需要注意的是,上述的兩個point是在spring view的父view內的,因此,在最後確定tailView frame的時候需要轉換到springView的座標系。
接下來就是tailView的drawRect實現。這裡主要需要做2件事情:

  1. 繪製P0為圓心的圓
  2. 繪製2對切點構成的封閉圖形

drawRect的部分實現:

- (void)drawRect:(CGRect)rect
{

    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetAllowsAntialiasing(context, true);
    CGContextSetShouldAntialias(context, true);

    CGContextSetStrokeColorWithColor(context, [UIColor greenColor].CGColor);
    CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor);
    CGContextSetLineWidth(context, 2);

    CGPoint pointStart, pointEnd;//center

    CGPoint movePoint1, movePoint2;//移動圓的2個切點
    CGPoint centerPoint1, centerPoint2;//原有圓的2個切點
    CGFloat moveRadius = _moveRadius;//移動圓 弧的半徑

    CGFloat sinval = 0, csinval = 0;
    CGFloat distance = 0;
    switch (_lineDirection) {
        case ZZLineDirection1:
        {
            pointStart = CGPointMake(rect.size.width - moveRadius, 0 + moveRadius);
            pointEnd = CGPointMake(0 + _radius, rect.size.height - _radius);

            distance = CGRectGetHeight(rect) * CGRectGetHeight(rect) + CGRectGetWidth(rect) * CGRectGetWidth(rect);

            sinval = CGRectGetHeight(rect) * CGRectGetHeight(rect)/distance;
            csinval = CGRectGetWidth(rect) * CGRectGetWidth(rect)/distance;



            movePoint2 = CGPointMake(pointStart.x - moveRadius * sinval, pointStart.y - moveRadius*csinval);
            movePoint1 = CGPointMake(pointStart.x + moveRadius*sinval, pointStart.y + moveRadius*csinval);

            centerPoint2 = CGPointMake(pointEnd.x + _radius*sinval, pointEnd.y + _radius*csinval);
            centerPoint1 = CGPointMake(pointEnd.x - _radius * sinval, pointEnd.y - _radius*csinval);
            break;
        }
        case ZZLineDirection2:
        {
            break;
        }
        case ZZLineDirection3:
        {

            break;
        }
        case ZZLineDirection4:
        {
            break;
        }

    }

    CGContextMoveToPoint(context, movePoint1.x, movePoint1.y);
    CGContextAddLineToPoint(context, movePoint2.x, movePoint2.y);
    CGContextAddLineToPoint(context, centerPoint1.x, centerPoint1.y);
    CGContextAddLineToPoint(context, centerPoint2.x, centerPoint2.y);
    CGContextClosePath(context);

    CGContextFillPath(context);
    CGContextStrokePath(context);


    CGContextAddArc(context, pointEnd.x, pointEnd.y, _radius, 0, 2*M_PI, 0);
    CGContextFillPath(context);

}
ZZSpringTailView

 

計算過程就不詳細描述了,初中數學的知識就夠了。接著執行下,看看效果。

從執行效果看,還是差強人意的。這顯示了方案的可行性。
那麼相應二三四象限的情況也能做類似的處理,這裡就不貼程式碼了。

由於時間的關係,暫時研究到此,下一篇會把功能逐步完善。主要會包含新增文字的情形等內容,敬請期待。
如果有更好的實現方式,也請大家賜教!

 

相關文章