iOS 滑動表盤實(shí)現(xiàn)

仿照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),先看下完成后的效果圖:


p_1.1.1.png

首先,我們可以看到,先不管拖動問題,單純從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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 2018-10-09 Day1 認(rèn)識Excel,突破理論 第四期的訓(xùn)練營如期而至,回顧點(diǎn)點(diǎn)滴滴,感慨頗多,感激那個...
    晨E戰(zhàn)到底閱讀 274評論 0 0
  • /**轉(zhuǎn)載自https://blog.csdn.net/xiaoxiaowenqiang/article/deta...
    Maxsium閱讀 773評論 0 0
  • 來源:WebSocket通信過程與實(shí)現(xiàn) 什么是 WebSocket ? WebSocket 是一種標(biāo)準(zhǔn)協(xié)議,用于在...
    四火流年閱讀 419評論 0 1
  • 隱私政策 引言 我們重視用戶的隱私。您在使用我們的服務(wù)時,我們可能會收集和使用您的相關(guān)信息。我們希望通過本《隱私政...
    jimcode閱讀 254評論 0 0
  • 每天聚焦孩子好的一面,感賞優(yōu)點(diǎn),看似簡單,實(shí)則不容易,擔(dān)心他作業(yè)未完成,擔(dān)心他在學(xué)校不吃飯等等,這又回到了原來的舊...
    揚(yáng)剛閱讀 292評論 0 0

友情鏈接更多精彩內(nèi)容