偶然間發(fā)現(xiàn)QQ的消息挺好玩的,應(yīng)用內(nèi)收到新消息,紅色的提醒圓圈可以拉伸并拖動(dòng)。很有意思,決定自己試一下。
先上效果圖:
接下來(lái),我們一步一步來(lái)實(shí)現(xiàn)之。
我們?cè)谕蟿?dòng)過(guò)程中,已知哪些信息:
初始的半圓心位置A,半徑R。
接下來(lái)我們可以通過(guò)用戶觸摸的位置獲取以下信息:
拖動(dòng)的半圓形位置B。
(注:并不是觸摸的點(diǎn)就是新的圓點(diǎn),需要進(jìn)行加工。開始觸摸的點(diǎn)為Touch,觸摸變化的點(diǎn)為Touch',新的圓點(diǎn)為A+(Touch'-Touch))。
我們根據(jù)當(dāng)前拖動(dòng)的距離與最大拖動(dòng)距離進(jìn)行求比率,再乘以初始半徑R得到拖動(dòng)半徑R'。
我們先取一個(gè)中間狀態(tài)來(lái)分析一下:
我們?cè)谕蟿?dòng)時(shí),小球會(huì)被分為兩個(gè)部分,兩個(gè)半圓圓心的連線與水平線的夾角為α。
最關(guān)鍵的一步是:計(jì)算出α的值。
根據(jù)同角的余角相等,以及三角函數(shù)能的到:
sinα = (B.x-A.x) / AB的長(zhǎng)度
然后通過(guò)反正弦函數(shù),可以得到角α的值。
(注:雖然使用正切函數(shù)在數(shù)值計(jì)算時(shí)很方便,但是反正切函數(shù)在0位置會(huì)發(fā)生突變,無(wú)法滿足我們拖動(dòng)時(shí)漸變的效果,故舍棄。數(shù)學(xué)不行,在這個(gè)坑里困了好久。。)
計(jì)算出角α就能開始繪圖了。
我們一一計(jì)算出圖中M、N、P、Q四個(gè)點(diǎn)的坐標(biāo),然后開始操作:
1 從M點(diǎn)開始,A為圓心,N為終點(diǎn),繪制半圓。(使用α角)
2 從N點(diǎn)開始,P點(diǎn)為終點(diǎn),繪制貝塞爾曲線。
3 從P點(diǎn)開始,B為圓心,Q為終點(diǎn),繪制半圓。(使用α角)
4 從Q點(diǎn)開始,M點(diǎn)為終點(diǎn),繪制貝塞爾曲線。
繪制時(shí),會(huì)發(fā)現(xiàn)步驟2和4的控制點(diǎn)不好確定,但是如果不確定的話,沒(méi)法繪制。如果使用兩個(gè)圓心的中點(diǎn)為控制點(diǎn),會(huì)發(fā)現(xiàn)初始拖動(dòng)時(shí)兩個(gè)半圓中間縫隙很大,不夠平滑。
此時(shí)我們精簡(jiǎn)一下模型,看下部的圖:
C是AB中點(diǎn),過(guò)C點(diǎn)作MN的平行線。
NP的控制點(diǎn)位于C點(diǎn)的右上側(cè)。
MQ的控制點(diǎn)位于C點(diǎn)點(diǎn)左下側(cè)。
兩個(gè)控制點(diǎn)是根據(jù)拖動(dòng)的距離變化的。取極限即可得到兩個(gè)點(diǎn)對(duì)應(yīng)的一次比率關(guān)系。
這樣就能夠完成步驟2和4了。
此時(shí)我們就完成了拖動(dòng)時(shí)的效果了。
- (void)updateCircleWithOriginCenter:(CGPoint)originCenter withNewCenter:(CGPoint)newCenter
{
//拖拽的距離,等于兩個(gè)圓心的距離
double moveDistance = (double)sqrt((newCenter.y-originCenter.y)*(newCenter.y-originCenter.y) + (newCenter.x-originCenter.x)*(newCenter.x-originCenter.x));
if (moveDistance == 0) {
return;
}
//移動(dòng)的水平角度(兩圓心連線與水平面夾角)
//反正切函數(shù)很方便,但因?yàn)榉凑泻瘮?shù)在0的位置突變,從-M_PI/2變?yōu)?M_PI/2,無(wú)法滿足我們拖動(dòng)時(shí)的漸變需求,故舍棄。我們使用反正弦函數(shù)。
//正弦函數(shù)=對(duì)邊/斜邊 兩個(gè)圓心之間的連線為斜邊,對(duì)邊是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;
//如果拖拽結(jié)束了,那就不做操作,否則會(huì)死循環(huán)。因?yàn)橥献ЫY(jié)束,會(huì)進(jìn)行恢復(fù)繪制,恢復(fù)繪制方法中會(huì)調(diào)用本方法,然后本方法再調(diào)用恢復(fù)繪制方法。。。
if (self.isEnding) {
return;
}
//達(dá)到最大值,斷開
self.isEnding = YES;
//如果有回調(diào),則觸發(fā)
if (self.pullBlock) {
self.pullBlock();
}
if (self.needAnimation) {
//將原始位置的圓進(jìn)行恢復(fù)繪制,就是把原始位置的小圓拉過(guò)來(lái)。在手指觸摸的位置合二為一
[self backCircleWithTotal:100 withCurrent:0 withTotalTime:0.05 withOriginCenter:newCenter withEndCenter:originCenter];
}else{
//拉伸到最大值時(shí),如果不需要?jiǎng)赢嫞沂怯脩粼O(shè)置的類型,那么隱藏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:
{
//如果是拉到最大值,結(jié)束繪制,合二為一的過(guò)程,那么起始圓和目標(biāo)圓半徑互換
//比如說(shuō):大半徑留在原地,小半徑被拖拽走的這種情況
//當(dāng)我向外拉的時(shí)候:拖動(dòng)的圓半徑較小,小圓移動(dòng)
//當(dāng)我未拉到最大值,小圓回去,小圓移動(dòng)
//當(dāng)我拉到最大值,大圓向小圓合并。大圓移動(dòng)
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;
}
//創(chuàng)建新的BezierPath
UIBezierPath *path = [UIBezierPath bezierPath];
if (newCenter.x-originCenter.x < 0) {
//圓的中心對(duì)稱軸,左側(cè)與右側(cè)的計(jì)算不一樣
//初始圓的底部的點(diǎn)
CGPoint originBottomPoint = CGPointMake(originCenter.x+sin(angle)*self.circleR, originCenter.y+cos(angle)*self.circleR);
//初始圓的頂部的點(diǎn)
CGPoint originTopPoint = CGPointMake(originCenter.x-sin(angle)*self.circleR, originCenter.y-cos(angle)*self.circleR);
//新圓心的左上角的點(diǎn)
CGPoint newTopPoint = CGPointMake(newCenter.x-sin(angle)*newRadius, newCenter.y-cos(angle)*newRadius);
//新圓心的下部的點(diǎn)
CGPoint newBottomPoint = CGPointMake(newCenter.x+sin(angle)*newRadius, newCenter.y+cos(angle)*newRadius);
//兩圓心連線中點(diǎn)
CGPoint controlPoint = CGPointMake(originCenter.x+(newCenter.x-originCenter.x)/2, originCenter.y+(newCenter.y-originCenter.y)/2);
//初始圓頂部點(diǎn)與新圓的頂部點(diǎn)連線的中點(diǎn)
CGPoint topMiddlePoint = CGPointMake((newTopPoint.x+originTopPoint.x)/2, (newTopPoint.y+originTopPoint.y)/2);
//上部控制點(diǎn)的X坐標(biāo),與拉伸比例有關(guān),未拉伸時(shí),取頂部點(diǎn)連線中點(diǎn),拉伸最大時(shí),取兩圓心連線中點(diǎn)
CGFloat topX = topMiddlePoint.x + (controlPoint.x-topMiddlePoint.x)*rate;
//上部控制點(diǎn)的Y坐標(biāo),與拉伸比例有關(guān)
CGFloat topY = topMiddlePoint.y + (controlPoint.y-topMiddlePoint.y)*rate;
//拉伸時(shí),上部的控制點(diǎn)。不斷變化的
CGPoint topControlPoint = CGPointMake(topX, topY);
//兩個(gè)圓下部點(diǎn)連線的中點(diǎn)
CGPoint bottomMiddlePoint = CGPointMake((newBottomPoint.x+originBottomPoint.x)/2, (newBottomPoint.y+originBottomPoint.y)/2);
//下部點(diǎn)的x,隨比例變化
CGFloat bottomX = bottomMiddlePoint.x + (controlPoint.x-bottomMiddlePoint.x)*rate;
//下部點(diǎn)的y,隨比例變化
CGFloat bottomY = bottomMiddlePoint.y + (controlPoint.y-bottomMiddlePoint.y)*rate;
//拉伸時(shí),下部控制點(diǎn)
CGPoint bottomControlPoint = CGPointMake(bottomX, bottomY);
//移動(dòng)到初始圓的下部點(diǎn)
[path moveToPoint:originBottomPoint];
//原始的圓,右半側(cè),逆時(shí)針畫圓
[path addArcWithCenter:originCenter radius:self.circleR startAngle:M_PI/2-angle endAngle:M_PI*3/2-angle clockwise:NO];
//從原始圓的頂部,連線到新圓的頂部,上部點(diǎn)為控制點(diǎn)
[path addQuadCurveToPoint:newTopPoint controlPoint:topControlPoint];
//新圓的左側(cè),逆時(shí)針畫圓
[path addArcWithCenter:newCenter radius:newRadius startAngle:M_PI*3/2-angle endAngle:M_PI*5/2-angle clockwise:NO];
//從新圓的底部,連接到初始圓的底部點(diǎn),下部點(diǎn)為控制點(diǎn)
[path addQuadCurveToPoint:originBottomPoint controlPoint:bottomControlPoint];
}else{
//初始圓的下部點(diǎn)
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);
//新圓心的左上角的點(diǎn)
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);
//兩圓心連線中點(diǎn)
CGPoint controlPoint = CGPointMake(originCenter.x+(newCenter.x-originCenter.x)/2, originCenter.y+(newCenter.y-originCenter.y)/2);
//初始圓頂部點(diǎn)與新圓的頂部點(diǎn)連線的中點(diǎn)
CGPoint topMiddlePoint = CGPointMake((newTopPoint.x+originTopPoint.x)/2, (newTopPoint.y+originTopPoint.y)/2);
//上部控制點(diǎn)的X坐標(biāo),與拉伸比例有關(guān),未拉伸時(shí),取頂部點(diǎn)連線中點(diǎn),拉伸最大時(shí),取兩圓心連線中點(diǎn)
CGFloat topX = topMiddlePoint.x + (controlPoint.x-topMiddlePoint.x)*rate;
//上部控制點(diǎn)的Y坐標(biāo),與拉伸比例有關(guān)
CGFloat topY = topMiddlePoint.y + (controlPoint.y-topMiddlePoint.y)*rate;
//拉伸時(shí),上部的控制點(diǎn)。不斷變化的
CGPoint topControlPoint = CGPointMake(topX, topY);
//兩個(gè)圓下部點(diǎn)連線的中點(diǎn)
CGPoint bottomMiddlePoint = CGPointMake((newBottomPoint.x+originBottomPoint.x)/2, (newBottomPoint.y+originBottomPoint.y)/2);
//下部點(diǎn)的x,隨比例變化
CGFloat bottomX = bottomMiddlePoint.x + (controlPoint.x-bottomMiddlePoint.x)*rate;
//下部點(diǎn)的y,隨比例變化
CGFloat bottomY = bottomMiddlePoint.y + (controlPoint.y-bottomMiddlePoint.y)*rate;
//拉伸時(shí),下部控制點(diǎn)
CGPoint bottomControlPoint = CGPointMake(bottomX, bottomY);
[path moveToPoint:originBottomPoint];
//原始的圓,左半側(cè),順時(shí)針畫圓
[path addArcWithCenter:originCenter radius:self.circleR startAngle:M_PI/2+angle endAngle:M_PI*3/2+angle clockwise:YES];
//添加曲線到新圓的頂部
[path addQuadCurveToPoint:newTopPoint controlPoint:topControlPoint];
//新圓的右側(cè),順時(shí)針畫圓
[path addArcWithCenter:newCenter radius:newRadius startAngle:M_PI*3/2+angle endAngle:M_PI*5/2+angle clockwise:YES];
//添加曲線到新圓的底部點(diǎn)
[path addQuadCurveToPoint:originBottomPoint controlPoint:bottomControlPoint];
}
//更新layer
self.pointLayer.path = path.CGPath;
}
接下來(lái)就是要處理拖動(dòng)到一半,松手的處理了。
我們需要拉動(dòng)的那一部分按照拖動(dòng)出去的效果,反過(guò)來(lái)合并到初始圓中。
這里,我們已知這些信息:
初始圓心A,半徑R。
松手時(shí)圓心B,半徑R'。
接下來(lái)我們需要做這些操作:
1 計(jì)算出直線AB的關(guān)系式,然后按比率進(jìn)行縮小。
2 更新當(dāng)前的圖形。可使用拖動(dòng)時(shí)的函數(shù),只需要
把起始點(diǎn)和結(jié)束點(diǎn)調(diào)整一下即可。
3 會(huì)到初始位置后,反彈動(dòng)畫。
- (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;
//回彈時(shí),不同時(shí)刻的圓心位置
CGPoint backCenter = CGPointMake(endCenter.x-xChangeValue*rate, endCenter.y-yChangeValue*rate);
//繪制曲線
[self updateCircleWithOriginCenter:originCenter withNewCenter:backCenter];
//遞歸調(diào)用,繼續(xù)更新
[self backCircleWithTotal:total withCurrent:value+1 withTotalTime:totalTime withOriginCenter:originCenter withEndCenter:endCenter];
if (value == total) {
//當(dāng)完成整個(gè)回彈時(shí),設(shè)置屬性,重置圓的位置
self.isEndAnimation = YES;
}
});
}else{
//如果本次拖拽到最大值了,不需要回彈動(dòng)畫了
if (self.isEnding) {
return;
}
//未拉到最大值,放手的動(dòng)畫處理
CGFloat rate = 0.1;
//X,Y的變化值
CGFloat xChangeValue = endCenter.x-originCenter.x;
CGFloat yChangeValue = endCenter.y-originCenter.y;
//初始位置的左上側(cè)
CGPoint backCenter = CGPointMake(originCenter.x-xChangeValue*rate-self.originR, originCenter.y-yChangeValue*rate-self.originR);
//初始位置的右下側(cè)
CGPoint foreCenter = CGPointMake(originCenter.x+xChangeValue*rate/2-self.originR, originCenter.y+yChangeValue*rate/2-self.originR);
//初始位置的左上側(cè),較靠近圓心
CGPoint backTwoCenter = CGPointMake(originCenter.x-xChangeValue*rate/3-self.originR, originCenter.y-yChangeValue*rate/3-self.originR);
//動(dòng)畫
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
animation.calculationMode = kCAAnimationLinear;
CGMutablePathRef path = CGPathCreateMutable();
//移動(dòng)到圓心
CGPathMoveToPoint(path, NULL,originCenter.x-self.originR, originCenter.y-self.originR);
//圓心左上側(cè)
CGPathAddLineToPoint(path, NULL, backCenter.x, backCenter.y);
//圓心的右下側(cè)
CGPathAddLineToPoint(path, NULL, foreCenter.x, foreCenter.y);
//圓心的左上側(cè),近圓心
CGPathAddLineToPoint(path, NULL, backTwoCenter.x, backTwoCenter.y);
//圓心的右下側(cè),近圓心
CGPathAddLineToPoint(path, NULL, originCenter.x-self.originR, originCenter.y-self.originR);
animation.path = path;
animation.duration = 0.35;
[self.pointLayer addAnimation:animation forKey:@"pointBackAnimation"];
}
}
至此,我們就能實(shí)現(xiàn)動(dòng)感的小球了。
有什么意見(jiàn)或者建議請(qǐng)留言哈,共同進(jìn)步~