偶然發現iPhone QQ 顯示消息條數的小紅點能夠響應動做事件,也有人問我這樣的動畫該怎麼作,這裏就把實現的思路簡單的描述一下。在實現的過程當中,一樣發現該功能並無看到的那麼簡單,要作一個完備的動畫效果須要有必定的功底。所以,本篇會先側重於實現思路,並不會實現一個如出一轍的效果。
下面是iPhone QQ小紅點的動做交互效果:
ide
分析
首先咱們分析拖拽時候的表現:動畫
- 原先的小紅點順着手指移動,並與原來所處位置經過一個小尾巴(即移動的軌跡)鏈接
- 與原先位置在必定範圍內時,小尾巴出現;超過必定範圍時,小尾巴不出現
- 釋放手指,小紅點回到原先位置,並有彈簧動畫效果
- 釋放手指時離原先位置超過必定範圍則不返回原點,而是有消失的泡沫動畫
拋開細節,抓住要點,我概括了幾個要點:atom
- 小原點隨手指移動
- 小尾巴分狀況出現
- 手指釋放後,小紅點彈回原先位置
除此以外,紅點上的文字,消失等情形的處理不是主要問題,咱們先緩一緩。spa
實現
紅點的移動
首先實現一個圓形的view,而且能夠隨手指移動。在必定移動範圍內,手指離開後,view返回原處並帶有彈簧效果;超出範圍,view則停留在手指離開處。
咱們經過drawRect:來畫一個圓;設置一個CGPoint的對象來記錄開始觸摸時的位置;接着就是實現相關的touchEvent:。由於都是很基本的內容,直接上代碼。code
//頭文件 @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) { }]; } }
在touchBegin事件中,由於點擊小紅點的位置與中心會有偏移,經過UIView animation作一個平滑的過分。而在touchEnd事件中,返回彈簧震盪的效果是使用UIView的Spring animation。orm
添加小尾巴(軌跡)
我畫了一張簡化的模擬拖拽過程的圖:
對象
虛線圓是view原來的位置,P0是其圓點;實線圓是移動的位置,P1是圓點。設置兩圓的切線(紅色),把封閉的部分都填充爲同一個顏色的話,就能大體模擬出類似的效果。這裏隱含了幾個前提:blog
- 實際的軌跡是帶有弧度的曲線,這裏使用了切線來代替(紅色的切線)
- 拖拽的時候,原先位置的圓形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
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]; } }
這裏的實現是把tailview添加到springview之上,一般狀況下,clipToBouds默認是NO的,所以這種添加超出父view bound 的子view方案是可行的。須要注意的是,上述的兩個point是在spring view的父view內的,所以,在最後肯定tailView frame的時候須要轉換到springView的座標系。
接下來就是tailView的drawRect實現。這裏主要須要作2件事情:
- 繪製P0爲圓心的圓
- 繪製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); }
計算過程就不詳細描述了,初中數學的知識就夠了。接着運行下,看看效果。
從運行效果看,仍是差強人意的。這顯示了方案的可行性。
那麼相應二三四象限的狀況也能作相似的處理,這裏就不貼代碼了。
因爲時間的關係,暫時研究到此,下一篇會把功能逐步完善。主要會包含添加文字的情形等內容,敬請期待。 若是有更好的實現方式,也請你們賜教!