iOS-模仿苹果时钟选择控件

最近看了苹果自带应用时钟上的时间选择工具感觉挺巧妙的,就尝试着模仿它做出一个控件工具。工程Demo运行效果如下:
AppleAlram.gif

根据时钟选择工具上面的功能,大概可以确定,圆环的绘制我们可以通过CAShapeLayer结合UIBezierPath绘制出来,当拖动起始点或者结束点View时,通过手势判断拖动的角度,从而改变UIBezierPath的角度,并且让起始点或者结束点View根据拖动的角度对中心点进行公转,并且自身进行自转。如果拖动圆环的话,改变UIBezierPath角度的同时,还要让起始点和结束点View同时进行公转和自转。

1.时钟AlarmView

时钟View是该控件的核心区,里面包含了图形的绘制和旋转,旋转类型的判断等多种处理。
核心代码:

-(CAShapeLayer *)alramLayer{
    
    if (!_alramLayer) {
        _alramLayer = [[CAShapeLayer alloc] init];
         _alramLayer.bounds = CGRectMake(0,0, self.bounds.size.width, self.bounds.size.height);
        _alramLayer.path = [self drawAlarmPathWithStartAngle:self.startAngle endAngle:self.endAngle].CGPath;
        _alramLayer.fillColor = UIColor.orangeColor.CGColor;
    }
    return _alramLayer;
}

#pragma mark - Method                  - Method -
-(void)beiginRotationWithAngle:(CGFloat)angle beiginPiont:(CGPoint)point{
    
    switch (self.rotationType) {
        case kRotationType_StartAngle:
            [self changeStartAngle:angle];
            break;
        case kRotationType_EndAngle:
            [self changeEndAngle:angle];
            break;
        case kRotationType_CircularingLocation:
            [self changeCircularingLocation:angle];
            break;
        default:
         break;
    }
}

/** 改变起始时间 */
-(void)changeStartAngle:(CGFloat)startAngle{
      NSLog(@"角度差 = %f",fabs(self.endAngle - self.startAngle - startAngle));
    if (fabs(self.endAngle - self.startAngle - startAngle) >360) {//修复BUG
        if (startAngle > 0) {
            startAngle = startAngle -360;
        }else{
            startAngle = startAngle +360;
        }
    }
    NSLog(@"角度差2 = %f",fabs(self.endAngle - self.startAngle - startAngle));
   self.sleepSuperView.transform = CGAffineTransformMakeRotation(kDgreesToRadoans(self.startAngle+startAngle));//公转
   self.sleepView.transform = CGAffineTransformMakeRotation(-kDgreesToRadoans(self.startAngle+startAngle));//自转
   self.alramLayer.path = [self drawAlarmPathWithStartAngle:startAngle+self.startAngle endAngle:self.endAngle].CGPath;
    
}

/** 改变结束时间 */
-(void)changeEndAngle:(CGFloat)endAngle{
    
    if (fabs(self.startAngle - self.endAngle - endAngle) >360) {
        if (endAngle > 0) {
            endAngle = endAngle -360;
        }else{
            endAngle = endAngle +360;
        }
    }
     NSLog(@"角度差 = %f",fabs(self.endAngle - self.startAngle - endAngle));
    self.ringSuperView.transform = CGAffineTransformMakeRotation(kDgreesToRadoans(self.endAngle+endAngle));
    self.ringView.transform = CGAffineTransformMakeRotation(-kDgreesToRadoans(self.endAngle+endAngle));
    self.alramLayer.path = [self drawAlarmPathWithStartAngle:self.startAngle endAngle:self.endAngle+endAngle].CGPath;
}

/** 改变圆环位置 */
-(void)changeCircularingLocation:(CGFloat)angle{
    
    self.sleepSuperView.transform = CGAffineTransformMakeRotation(kDgreesToRadoans(self.startAngle+angle));//公转
    self.sleepView.transform = CGAffineTransformMakeRotation(-kDgreesToRadoans(self.startAngle+angle));//自转
    self.ringSuperView.transform = CGAffineTransformMakeRotation(kDgreesToRadoans(self.endAngle+angle));
    self.ringView.transform = CGAffineTransformMakeRotation(-kDgreesToRadoans(self.endAngle+angle));
    self.alramLayer.path = [self drawAlarmPathWithStartAngle:self.startAngle+angle endAngle:self.endAngle +angle].CGPath;
}

/** 旋转类型 */
-(kRotationType)rotationTypeWithPiont:(CGPoint)piont{
    
    CGPoint alarmViewCenter = CGPointMake(kAlarmViewRadius, kAlarmViewRadius);
    CGPoint startCenter = CGPointMake(cos(((_currentStartAngle-90)/180)*M_PI)*(kAlarmViewRadius-kIconViewHW/2) +kAlarmViewRadius, sin(((_currentStartAngle-90)/180)*M_PI)*(kAlarmViewRadius-kIconViewHW/2) +kAlarmViewRadius);
    CGPoint endCenter = CGPointMake(cos(((_currentEndAngle-90)/180)*M_PI)*(kAlarmViewRadius-kIconViewHW/2) +kAlarmViewRadius, sin(((_currentEndAngle-90)/180)*M_PI)*(kAlarmViewRadius-kIconViewHW/2) +kAlarmViewRadius);
    
    if ([UIView distanceBetweenPointA:alarmViewCenter AndPiontB:piont] >= kAlarmViewRadius-kIconViewHW && [UIView distanceBetweenPointA:alarmViewCenter AndPiontB:piont] <= kAlarmViewRadius) {
        if ([UIView distanceBetweenPointA:startCenter AndPiontB:piont] < kIconViewHW/2) {
            return kRotationType_StartAngle;
        }else if ([UIView distanceBetweenPointA:endCenter AndPiontB:piont] < kIconViewHW/2){
            return kRotationType_EndAngle;
        }else{
            return kRotationType_CircularingLocation;
        }
    }
    return kRotationType_None;
}

/** 绘制BezierPath */
-(UIBezierPath *)drawAlarmPathWithStartAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle{
    
    CGRect circleRect = CGRectMake(kAlarmViewRadius,kAlarmViewRadius, self.bounds.size.width, self.bounds.size.height);
    UIBezierPath* circlePath = [UIBezierPath bezierPath];
    [circlePath addArcWithCenter: CGPointMake(CGRectGetMidX(circleRect), CGRectGetMidY(circleRect)) radius: circleRect.size.width/2 startAngle: kDgreesToRadoans(startAngle) endAngle: kDgreesToRadoans(endAngle) clockwise: YES];
    [circlePath addLineToPoint: CGPointMake(CGRectGetMidX(circleRect), CGRectGetMidY(circleRect))];
    [circlePath closePath];
    
    _currentStartAngle = fmodf(startAngle,360);
    _currentEndAngle = fmodf(endAngle, 360);
    
    self.costTimeLbl.attributedText = [self timeBlockWithAngle:_currentEndAngle - _currentStartAngle];
    self.beginTime = [self beginTimeWithAngle:_currentStartAngle];
    self.endTime = [self endTimeWithAngle:_currentEndAngle];
    
    if (self.delegate && [self.delegate respondsToSelector:@selector(alramViewIsChangedWithBeginTime:endTime:)]) {
        [self.delegate alramViewIsChangedWithBeginTime:self.beginTime endTime:self.endTime];
    }
    return circlePath;
}

2.手势的处理类 DWRotaionGestureRecognizer

根据上面圆盘的效果,拖动手势时,我们需要知道起始点与拖动点之间的角度和方向。
核心代码:

/**拖动结束后自动重置 */
- (void)reset
{
    [super reset];
//    _previousRotation = [self rotation];
    _previousRotation = 0;
    _currentRotation = 0;
}

#pragma mark - eventResponse                - Method -
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [super touchesBegan:touches withEvent:event];
    
    self.startingPoint = [[touches anyObject] locationInView:self.view];
    self.state = UIGestureRecognizerStateBegan;
    
    if (self.rotaionGestureRecognizerDelegate && [self.rotaionGestureRecognizerDelegate respondsToSelector:@selector(touchesBegan:withEvent:)]) {
        [self.rotaionGestureRecognizerDelegate touchesBegan:touches withEvent:event];
    }
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    [super touchesMoved:touches withEvent:event];
    
    CGPoint point = [[touches anyObject] locationInView:self.view];
     self.currentRotation = [UIView angleBetweenPoint1:self.startingPoint point2:point AndCenter:self.center];
    self.state = UIGestureRecognizerStateChanged;
    
    if (self.rotaionGestureRecognizerDelegate && [self.rotaionGestureRecognizerDelegate respondsToSelector:@selector(touchesMoved:withEvent:)]) {
        [self.rotaionGestureRecognizerDelegate touchesMoved:touches withEvent:event];
    }
}

- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    [super touchesEnded:touches withEvent:event];
    
    self.endPoint = [[touches anyObject] locationInView:self.view];
    self.currentRotation = [UIView angleBetweenPoint1:self.startingPoint point2:self.endPoint AndCenter:self.center];
    self.state = UIGestureRecognizerStateEnded;
    
    if (self.rotaionGestureRecognizerDelegate && [self.rotaionGestureRecognizerDelegate respondsToSelector:@selector(touchesEnded:withEvent:)]) {
        [self.rotaionGestureRecognizerDelegate touchesEnded:touches withEvent:event];
    }
}

- (void) touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
    [super touchesCancelled:touches withEvent:event];
    self.state = UIGestureRecognizerStateCancelled;
    
    if (self.rotaionGestureRecognizerDelegate && [self.rotaionGestureRecognizerDelegate respondsToSelector:@selector(touchesCancelled:withEvent:)]) {
        [self.rotaionGestureRecognizerDelegate touchesCancelled:touches withEvent:event];
    }
}
计算两点间角度和距离的类别 UIView+DWAngle

核心代码:

/** 两个坐标点的角度 */
+ (CGFloat)angleBetweenPoint1:(CGPoint)first point2:(CGPoint)second AndCenter:(CGPoint)center{
     // θ=arctan[(y2-y0)/(x2-x0)]-arctan[(y1-y0)/(x1-x0)];
    CGPoint centeredPoint1 = CGPointMake(first.x - center.x, first.y - center.y);
    CGPoint centeredPoint2 = CGPointMake(second.x - center.x, second.y - center.y);
    
    CGFloat firstAngle = angleBetweenOriginAndPointA(centeredPoint1);
    CGFloat secondAngle = angleBetweenOriginAndPointA(centeredPoint2);
    
    CGFloat rads = secondAngle - firstAngle;
    
    return rads;
}

/** 两点的距离 */
+(CGFloat)distanceBetweenPointA:(CGPoint)pointA AndPiontB:(CGPoint)pointB{
    // (y2-y1)²+(x2-x1)²=d²  sqrt(<#double#>)   pow(5, 2)
    CGFloat a = pow(pointB.x-pointA.x, 2);
    CGFloat b = pow(pointB.y-pointA.y, 2);
    
    return sqrt(a+b);
}

/** 某点和原点间的角度 */
+(CGFloat)angleBetweenOriginAndPointA:(CGPoint)p{
    return angleBetweenOriginAndPointA(p);
}

CGFloat angleBetweenOriginAndPointA(CGPoint p) {
    if (p.x  == 0) {
        return signA(p.y) * M_PI;
    }
    
    CGFloat angle = atan(-p.y / p.x); // '-' because negative ordinates are positive in UIKit
    
    // atan() is defined in [-pi/2, pi/2], but we want a value in [0, 2*pi]
    // so we deal with these special cases accordingly
    switch (quadrantForPointA(p)) {
        case 1:
        case 2: angle += M_PI; break;
        case 3: angle += 2* M_PI; break;
    }
    return angle;
}

/** 点的象限 */
NSInteger quadrantForPointA(CGPoint p) {
    if (p.x > 0 && p.y < 0) {
        return 0;
    } else if (p.x < 0 && p.y < 0) {
        return 1;
    } else if (p.x < 0 && p.y > 0) {
        return 2;
    } else if (p.x > 0 && p.y > 0)  {
        return 3;
    }
    return 0;
}

NSInteger signA(CGFloat num) {
    if (num == 0) {
        return 0;
    } else if (num > 0) {
        return 1;
    } else {
        return -1;
    }
}

以上是核心代码部分,感兴趣的读者可以到Github进行查阅:Github传送门
希望项目工程对您有所帮助,谢谢阅读。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,710评论 4 376
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,839评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,295评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,776评论 0 223
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,198评论 3 297
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,074评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,200评论 2 322
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,986评论 0 214
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,733评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,877评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,348评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,675评论 3 265
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,393评论 3 246
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,209评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,996评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,212评论 2 287
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 36,003评论 2 280

推荐阅读更多精彩内容

  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥ios动画全貌。在这里你可以看...
    每天刷两次牙阅读 8,327评论 6 30
  • 常用的表格图绘制主要用到折线图和饼图。也有不错的第三方框架,比如:Charts。如果不是专门做统计的,没有必要引入...
    张小西的BUG阅读 918评论 0 1
  • 前言 本文只要描述了iOS中的Core Animation(核心动画:隐式动画、显示动画)、贝塞尔曲线、UIVie...
    GitHubPorter阅读 3,538评论 7 11
  • 目录 ** UIView 动画 ** ** Core Animation ** ** FaceBook POP动画...
    方向_4d0d阅读 1,500评论 0 3
  • 今天感恩节哎,感谢一直在我身边的亲朋好友。感恩相遇!感恩不离不弃。 中午开了第一次的党会,身份的转变要...
    迷月闪星情阅读 10,503评论 0 11