仿照iOS 系统就寝效果实现可拖动表盘效果
最近项目里有需求要实现就寝提醒功能,这一点苹果系统的就寝功能已经做的很完善了,但由于我们需要和自己的硬件设备进行交互,需要开发一套自己的就寝流程,效果上要按照苹果系统就寝闹钟来实现,先看下完成后的效果图:
p_1.1.1.png
首先,我们可以看到,先不管拖动问题,单纯从UI上来说,和我们平时常用的圆形进度条比较类似,那么,我们先来创建两个圆环,一个作为背景,一个作为有效的进度:
- (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.绘制起点->终点圆弧
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.绘制起点
CGPoint startHandleCenter = [self pointFromAngle: (_startAngle)];
self.startDot.center = startHandleCenter;
//4.绘制终点
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);
}
创建好静态背景,我们开始考虑怎么响应用户交互,通过使用iOS 系统就寝功能可以看到,苹果设计了三种交互响应,拖动起点、拖动终点、以及滑动圆环中部,这里我们通过监听用户对屏幕的触摸事件来实现对三种交互的响应:
#pragma mark - 监听屏幕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; //精确度 增加触发几率
//点击坐标在不在圆环上
if (dis > _radius+_lineWidth + accuracy|| dis < _radius - accuracy) return;
//判断该点是否在起点->终点圆弧上
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];
//根据触摸点移动相应的图片
[self movehandle:lastPoint];
//转换成相应的时间
[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];
}
}
这里,我们就要想到了,用户的手指触摸轨迹是不规则的,而我们的环形运动轨迹是规则的,那么,怎么把用户触摸的点映射到我们圆环上呢?这里我们就需要用到一些几何知识,通过对用户touch的监听我们可以获取到当前手指点击的坐标P1(x1, y1), 圆环的中心坐标为P0(x0, y0), 那么我们就可以计算P1与P0水平方向的夹角,有了角度,又知道圆环的半径,我们就可以通过三角函数来计算出P1对应到圆弧上的坐标:
//计算两点间的角度
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);
}
//计算角度对应内圆弧上的坐标
-(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;
}
//计算外圆弧的坐标
- (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;
}
解决了圆弧轨迹问题,我们就可以通过监听滑动的轨迹来转换成相应的时间了,这里需要注意两点:
1.我们获取到的起止点的角度,需要判断时间是增量的还是减量的 :
CGFloat difAngleM = [self difAngleBetweenStartAngle:_endAngle andEndAngle:currentAngle];
CGFloat difAngleP = [self difAngleBetweenStartAngle:_endAngle andEndAngle:_prevAngle];
//精确度 防止触发太灵敏出现抖动
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 监听的点并不是连续的,不能够仅仅通过判断起点终点是否重合过来确定圈数,这里通过计算拖动角度是否在起止点角度的最小区间内来判断是否重合过。
//判断目标值是否在圆弧上两点的最小区间内 如在,则认为重合过
- (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;
}
解决了以上的问题,基本上这个功能就可以实现了,剩下的就是将表盘上的角度换算成相应的时间,或者通过时间换算成相应的角度,我将时间角度的换算写在在了一个NSString+Date.h 分类里面。这里只是写了一下大概的实现思路,具体的项目代码请移步Github https://github.com/AndyChuan/JCAlertClockView
网友评论