仿照iOS 系統(tǒng)就寢效果實(shí)現(xiàn)可拖動表盤效果
最近項(xiàng)目里有需求要實(shí)現(xiàn)就寢提醒功能,這一點(diǎn)蘋果系統(tǒng)的就寢功能已經(jīng)做的很完善了,但由于我們需要和自己的硬件設(shè)備進(jìn)行交互,需要開發(fā)一套自己的就寢流程,效果上要按照蘋果系統(tǒng)就寢鬧鐘來實(shí)現(xiàn),先看下完成后的效果圖:

首先,我們可以看到,先不管拖動問題,單純從UI上來說,和我們平時常用的圓形進(jìn)度條比較類似,那么,我們先來創(chuàng)建兩個圓環(huán),一個作為背景,一個作為有效的進(jìn)度:
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
CGContextRef context = UIGraphicsGetCurrentContext();
//1.繪制背景圓
CGContextAddArc(context, self.frame.size.width/2, self.frame.size.height/2, _radius, 0, M_PI*2, 0);
[UIColorFromHexValue(0xE5E5E5) setStroke];
CGContextSetLineWidth(context, _lineWidth);
CGContextSetLineCap(context, kCGLineCapRound);
CGContextDrawPath(context, kCGPathStroke);
//2.繪制起點(diǎn)->終點(diǎn)圓弧
CGContextAddArc(context, self.frame.size.width/2, self.frame.size.height/2, _radius, ToRad(_startAngle), ToRad(_endAngle), 0);
[UIColorFromHexValue(0xB178FF) setStroke];
CGContextSetLineWidth(context, _lineWidth);
CGContextSetLineCap(context, kCGLineCapRound);
//3.繪制起點(diǎn)
CGPoint startHandleCenter = [self pointFromAngle: (_startAngle)];
self.startDot.center = startHandleCenter;
//4.繪制終點(diǎn)
CGPoint endHandleCenter = [self pointFromAngle: (_endAngle)];
self.endDot.center = endHandleCenter;
//5.漸變色
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
NSArray *colorArr = @[
(id)UIColorFromHexValue(0x6D6EFE).CGColor,
(id)UIColorFromHexValue(0xB178FF).CGColor
];
CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef)colorArr, NULL);
CGColorSpaceRelease(colorSpace);
colorSpace = NULL;
CGContextReplacePathWithStrokedPath(context);
CGContextClip(context);
CGContextDrawLinearGradient(context, gradient, [self pointFromAngleForOuterRing:_startAngle], [self pointFromAngleForOuterRing:_startAngle + 180], 0);
CGGradientRelease(gradient);
}
創(chuàng)建好靜態(tài)背景,我們開始考慮怎么響應(yīng)用戶交互,通過使用iOS 系統(tǒng)就寢功能可以看到,蘋果設(shè)計(jì)了三種交互響應(yīng),拖動起點(diǎn)、拖動終點(diǎn)、以及滑動圓環(huán)中部,這里我們通過監(jiān)聽用戶對屏幕的觸摸事件來實(shí)現(xiàn)對三種交互的響應(yīng):
#pragma mark - 監(jiān)聽屏幕touch
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
_movedType = GWCycleMovedTypeNone;
_prevAngle = 0;
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:self];
if (CGRectContainsPoint(self.startDot.frame, point))
{
_movedType = GWCycleMovedTypeStartDot;
_prevAngle = _startAngle;
}else if (CGRectContainsPoint(self.endDot.frame, point))
{
_movedType = GWCycleMovedTypeEndDot;
_prevAngle = _endAngle;
}else
{
float dis = [self distanceBetweenCycleCenterAndPoint:point];
int accuracy = 15; //精確度 增加觸發(fā)幾率
//點(diǎn)擊坐標(biāo)在不在圓環(huán)上
if (dis > _radius+_lineWidth + accuracy|| dis < _radius - accuracy) return;
//判斷該點(diǎn)是否在起點(diǎn)->終點(diǎn)圓弧上
CGFloat angleP = [self angleBetweenCycleCenterAndPoint:point];
int difAngleP = [self difAngleBetweenStartAngle:_startAngle andEndAngle:angleP];
int difAngleE = [self difAngleBetweenStartAngle:_startAngle andEndAngle:_endAngle];
if (difAngleE < difAngleP) return;
_prevAngle = [self angleBetweenCycleCenterAndPoint:point];
_movedType = GWCycleMovedTypeMiddle;
}
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesMoved:touches withEvent:event];
@autoreleasepool {
switch (_movedType)
{
case GWCycleMovedTypeNone:
return;
case GWCycleMovedTypeStartDot:
case GWCycleMovedTypeEndDot:
{
UITouch *touch = [touches anyObject];
CGPoint lastPoint = [touch locationInView:self];
//根據(jù)觸摸點(diǎn)移動相應(yīng)的圖片
[self movehandle:lastPoint];
//轉(zhuǎn)換成相應(yīng)的時間
[self angleToTime];
}
break;
case GWCycleMovedTypeMiddle:
{
UITouch *touch = [touches anyObject];
CGPoint middlePoint = [touch locationInView:self];
[self moveVirtuaArc:middlePoint];
[self angleToTime];
}
break;
default:
break;
}
}
if (self.delegate && [self.delegate respondsToSelector:@selector(gwCustomClockView:changTimeWithStartTime:endTime:)]) {
[self.delegate gwCustomClockView:self changTimeWithStartTime:_startDate endTime:_endDate];
}
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
if (self.delegate && [self.delegate respondsToSelector:@selector(gwCustomClockView:TouchesEndedTimeWithStartTime:endTime:)]) {
[self.delegate gwCustomClockView:self TouchesEndedTimeWithStartTime:_startDate endTime:_endDate];
}
}
這里,我們就要想到了,用戶的手指觸摸軌跡是不規(guī)則的,而我們的環(huán)形運(yùn)動軌跡是規(guī)則的,那么,怎么把用戶觸摸的點(diǎn)映射到我們圓環(huán)上呢?這里我們就需要用到一些幾何知識,通過對用戶touch的監(jiān)聽我們可以獲取到當(dāng)前手指點(diǎn)擊的坐標(biāo)P1(x1, y1), 圓環(huán)的中心坐標(biāo)為P0(x0, y0), 那么我們就可以計(jì)算P1與P0水平方向的夾角,有了角度,又知道圓環(huán)的半徑,我們就可以通過三角函數(shù)來計(jì)算出P1對應(yīng)到圓弧上的坐標(biāo):
//計(jì)算兩點(diǎn)間的角度
static inline float AngleFromNorth(CGPoint p1, CGPoint p2, BOOL flipped) {
CGPoint v = CGPointMake(p2.x-p1.x,p2.y-p1.y);
float vmag = sqrt(SQR(v.x) + SQR(v.y)), result = 0;
v.x /= vmag;
v.y /= vmag;
double radians = atan2(v.y,v.x);
result = ToDeg(radians);
return (result >=0 ? result : result + 360.0);
}
//計(jì)算角度對應(yīng)內(nèi)圓弧上的坐標(biāo)
-(CGPoint)pointFromAngle:(int)angleInt {
CGPoint centerPoint = CGPointMake(self.frame.size.width/2, self.frame.size.height/2);
CGPoint result;
result.y = round(centerPoint.y + _radius * sin(ToRad(angleInt))) ;
result.x = round(centerPoint.x + _radius * cos(ToRad(angleInt)));
return result;
}
//計(jì)算外圓弧的坐標(biāo)
- (CGPoint)pointFromAngleForOuterRing:(int)angleInt {
CGPoint centerPoint = CGPointMake(self.frame.size.width/2,
self.frame.size.height/2);
CGPoint result;
result.y = round(centerPoint.y + (_radius + _lineWidth) * sin(ToRad(angleInt))) ;
result.x = round(centerPoint.x + (_radius + _lineWidth) * cos(ToRad(angleInt)));
return result;
}
解決了圓弧軌跡問題,我們就可以通過監(jiān)聽滑動的軌跡來轉(zhuǎn)換成相應(yīng)的時間了,這里需要注意兩點(diǎn):
1.我們獲取到的起止點(diǎn)的角度,需要判斷時間是增量的還是減量的 :
CGFloat difAngleM = [self difAngleBetweenStartAngle:_endAngle andEndAngle:currentAngle];
CGFloat difAngleP = [self difAngleBetweenStartAngle:_endAngle andEndAngle:_prevAngle];
//精確度 防止觸發(fā)太靈敏出現(xiàn)抖動
CGFloat accuracy = 2.5;
if (fabs(difAngleP - difAngleM) < accuracy) {
return;
}
float difAngle = difAngleM - difAngleP;// difAngleM > difAngleP ? 2.5 : -2.5;
//時間以五分鐘為單位
int a = (int)(difAngle/accuracy);
difAngle = (float)a * 2.5;
prevAngle = _startAngle;
targetAngle = _endAngle;
_startAngle = [self sumAngleBetweenStartAngle:_startAngle andDifAngle:difAngle];
resAngle = _startAngle;
2.表盤是12小時制,而我們一天是24小時制,所以表盤是可以累積一圈的,由于UITouchEvent 監(jiān)聽的點(diǎn)并不是連續(xù)的,不能夠僅僅通過判斷起點(diǎn)終點(diǎn)是否重合過來確定圈數(shù),這里通過計(jì)算拖動角度是否在起止點(diǎn)角度的最小區(qū)間內(nèi)來判斷是否重合過。
//判斷目標(biāo)值是否在圓弧上兩點(diǎn)的最小區(qū)間內(nèi) 如在,則認(rèn)為重合過
- (Boolean)containAngle:(CGFloat)targetAngle ByAngleRangeWith:(CGFloat)prevAngle :(CGFloat)lastAngle {
int32_t biggerAngle = MAX(prevAngle, lastAngle);
int32_t smallAngle = MIN(prevAngle, lastAngle);
int32_t diffAngle = biggerAngle - ToDeg(M_PI);
if (biggerAngle > targetAngle && smallAngle < targetAngle)
{
return diffAngle < smallAngle ? YES : NO;
}else if (biggerAngle < targetAngle || smallAngle > targetAngle)
{
return diffAngle > smallAngle ? YES : NO;
}
return NO;
}
解決了以上的問題,基本上這個功能就可以實(shí)現(xiàn)了,剩下的就是將表盤上的角度換算成相應(yīng)的時間,或者通過時間換算成相應(yīng)的角度,我將時間角度的換算寫在在了一個NSString+Date.h 分類里面。這里只是寫了一下大概的實(shí)現(xiàn)思路,具體的項(xiàng)目代碼請移步Github https://github.com/AndyChuan/JCAlertClockView