ios 可拖动、点击渐变圆环

年前忙着赶项目,也没时间更新,现在告一段落,把用到的技术点总结总结,这篇介绍介绍可自由拖动圆环的使用.
最开始我认为无非就是简单的动画效果,与下图差不多(项目实际效果图),后来才发现并非那么简单!


ptyhgif.gif

先来看看我们项目中的效果图

shoujigif2.gif

最开始我是想着使用UIView来实现,后来发现有些方法只能使用UIView的子类UIControl来实现,所以自定义视图类继承的是UIControl而非UIView!
如果想实现渐变色滑动圆环,我们要先画出底圆

-(id)initWithFrame:(CGRect)frame lineWidth:(CGFloat)lineWidth circleAngle:(CGFloat)circleAngle imageName:(NSString *)imageName
{
    if ([super initWithFrame:frame]) {
        // 线宽
        _lineWidth = lineWidth;
        // 半径
        radius = self.frame.size.width/2 - _lineWidth/2;
        // 圆起点(角度)
        self.startAngle = -((circleAngle - 180)/2 + 180);
        // 圆终点 (角度)
        self.endAngle = (circleAngle - 180)/2;

        self.imagev.image = [UIImage imageNamed:imageName];
        self.backgroundColor = [UIColor clearColor];
    }
    return self;
}

#pragma mark - 绘制图形
- (void)drawRect:(CGRect)rect {
    [super drawRect:rect];

    CGContextRef context = UIGraphicsGetCurrentContext();

    //1.绘制灰色的背景
    CGContextAddArc(context, self.frame.size.width/2, self.frame.size.height/2, radius, degreesToRadians(self.startAngle),degreesToRadians(self.endAngle) , 0);
    [[UIColor colorWithHexString:@"f2f2f2"] setStroke];
    CGContextSetLineWidth(context, _lineWidth);
    CGContextSetLineCap(context, kCGLineCapRound);
    CGContextDrawPath(context, kCGPathStroke);
}


这样我们就绘制了最底层的灰色圆形背景

WechatIMG4720.jpeg

然后就是绘制颜色渐变效果:
接着上面的代码

// 设置线宽
CGContextSetLineWidth(context, _lineWidth);
// 设置线条端点为圆角
CGContextSetLineCap(context, kCGLineCapRound);
// 设置画笔颜色
CGContextSetFillColorWithColor(context, [UIColor blackColor].CGColor);
//绘制圆弧(这里终点使用的是_angle所以效果图你看到的是一半圆弧,如果使用self.endAngle就是全部了)
CGContextAddArc(context, self.frame.size.width/2, self.frame.size.height/2,radius,degreesToRadians(self.startAngle), degreesToRadians(_angle), 0);



//使用rgb颜色空间
CGColorSpaceRef colorSpace=CGColorSpaceCreateDeviceRGB();

/*指定渐变色
 space:颜色空间
 components:颜色数组,注意由于指定了RGB颜色空间,那么四个数组元素表示一个颜色(red、green、blue、alpha),
 如果有三个颜色则这个数组有4*3个元素
 locations:颜色所在位置(范围0~1),这个数组的个数不小于components中存放颜色的个数
 count:渐变个数,等于locations的个数
 */
       CGFloat compoents[12]={
        248.0/255.0,86.0/255.0,86.0/255.0,1,
        249.0/255.0,127.0/255.0,127.0/255.0,1,
        1.0,1.0,1.0,1.0
    };
    CGFloat locations[3]={0,0.3,1.0};
    CGGradientRef gradient= CGGradientCreateWithColorComponents(colorSpace, compoents, locations, 3);

//    NSArray *colorArr = @[
//                          (id)[[UIColor colorWithHexString:@"56bcff"] CGColor],
//                          (id)[[UIColor colorWithHexString:@"56bcff"] CGColor]
//                          ];
//    CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef)colorArr, NULL);





/*绘制线性渐变
 context:图形上下文
 gradient:渐变色
 startPoint:起始位置
 endPoint:终止位置
 options:绘制方式,kCGGradientDrawsBeforeStartLocation 开始位置之前就进行绘制,到结束位置之后不再绘制,
 kCGGradientDrawsAfterEndLocation开始位置之前不进行绘制,到结束点之后继续填充
 */

//    NSLog(@"point:%@",NSStringFromCGPoint([self pointFromAngle:_angle]));

//    CGContextDrawLinearGradient(context, gradient, [self pointFromAngle:-225], [self pointFromAngle:_angle], kCGGradientDrawsAfterEndLocation);

//释放颜色空间
CGColorSpaceRelease(colorSpace);
colorSpace = NULL;

// ----------以下为重点----------
// 3. "反选路径"
// CGContextReplacePathWithStrokedPath
// 将context中的路径替换成路径的描边版本,使用参数context去计算路径(即创建新的路径是原来路径的描边)。用恰当的颜色填充得到的路径将产生类似绘制原来路径的效果。你可以像使用一般的路径一样使用它。例如,你可以通过调用CGContextClip去剪裁这个路径的描边
CGContextReplacePathWithStrokedPath(context);
// 剪裁路径
CGContextClip(context);


// 用渐变色填充(吗的,竟然这句话解决了渐变色的问题,艹,艹,艹)

CGContextDrawLinearGradient(context, gradient, CGPointMake(0, rect.size.height / 2), CGPointMake(rect.size.width, rect.size.height / 2), 0);
// 释放渐变色
CGGradientRelease(gradient);


效果如下

屏幕快照 2018-02-27 下午5.27.45.png

这样渐变色的绘制就完成了

重点来了,怎么实现可自主滑动与点击切换的效果呐?
UIControl中有以下两个方法

//返回Yes表示要继续跟踪触摸事件
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
//解决滑动改变的问题
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;


首先来解决点击切换的问题(滑动也是可以的,只是没有连续的动画效果,只取滑动最后一个点做绘制效果)
效果如下:

点击切换无非就是获取到你点击那个点的point,然后根据这个point与圆心点坐标计算出弧度,然后根据这个弧度重新绘制渐变效果,有了这个思路就好办多了

#pragma mark - 解决了点击圆环直接跳转到相应角度(对应相应金额)的问题
-(void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [super touchesEnded:touches withEvent:event];

    //集合转数组,其实只有一个对象
    NSArray *arr = [touches allObjects];
    UITouch *touch = arr[0];
    CGPoint lastPoint = [touch locationInView:self];
    NSLog(@"%@",NSStringFromCGPoint(lastPoint));
    NSLog(@"%d",touches.count);

    // 非 线性范围 则不可点击
    CGFloat deltaX = lastPoint.x - self.frame.size.width/2;
    CGFloat deltaY = lastPoint.y - self.frame.size.width/2;
    //sqrt 平方根 还记得勾股定理吗?手动微笑
    CGFloat distanceBetweenPoints = sqrt(deltaX*deltaX + deltaY*deltaY);
    //    NSLog(@"======%f",distanceBetweenPoints);
    //设置可触发点击或者滑动事件的范围
    if (distanceBetweenPoints>= radius - 50 && distanceBetweenPoints <= radius+ 20) {
        [self movehandle:lastPoint];
    }
}

#pragma mark - 根据点击或者滑动获取角度(弧度)
-(void)movehandle:(CGPoint)lastPoint{

    //获得中心点
    CGPoint centerPoint = CGPointMake(self.frame.size.width/2,
                                  self.frame.size.height/2);

    //计算中心点到任意点的角度
    float currentAngle = AngleFromNorth(centerPoint,
                                    lastPoint,
                                    NO);
    //浮点转整形
    int angleInt = floor(currentAngle);
    NSLog(@"%d",angleInt);

    //保存新角度
     if (angleInt >= 0 && angleInt <= self.endAngle) {
        self.angle = angleInt;
    }else if (angleInt >= 360+self.startAngle && angleInt <= 360){
        self.angle = -(360 - angleInt);
    }else if (angleInt >= self.endAngle && angleInt <= 360+self.startAngle){
        //这部分(非圆弧范围)不做处理
    }
    
    //重新绘制
    [self setNeedsDisplay];
}

#pragma mark - 从苹果是示例代码clockControl中拿来的函数,计算中心点到任意点的角度(弧度)
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 = radiansToDegrees(radians);
    return (result >=0  ? result : result + 360.0);
}


效果如下:

ggtouchgif.gif

如果想在滑动过程中有连续的绘制效果,则必须添加下面的方法了

#pragma mark - 持续滑动触发事件
-(BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event{
    [super continueTrackingWithTouch:touch withEvent:event];
    //获取触摸点
    CGPoint lastPoint = [touch locationInView:self];
    NSLog(@"%@",NSStringFromCGPoint(lastPoint));

    // 非 money图片 不可点击
    CGFloat deltaX = lastPoint.x - self.frame.size.width/2;
    CGFloat deltaY = lastPoint.y - self.frame.size.width/2;
    CGFloat distanceBetweenPoints = sqrt(deltaX*deltaX + deltaY*deltaY);
    //    NSLog(@"======%f",distanceBetweenPoints);

    if (distanceBetweenPoints>= radius - 50 && distanceBetweenPoints <= radius+ 20) {
        //使用触摸点来移动小块
        [self movehandle:lastPoint];
    }

    //发送值改变事件
    [self sendActionsForControlEvents:UIControlEventValueChanged];
    return YES;
}


效果如下:

ggcontinuegif.gif

此时感觉功能完善了,可是当你滑动到最底部的时候会发现有异常,如下所示:

ggcontinueycgif.gif

导致这种情况出现的原因是在你滑动过程中我们调用了continueTrackingWithTouch这个方法追踪你滑动或点击的位置,不在绘制圆环内没什么问题,但是当你从绘制圆环外到绘制圆环内的时候,捕捉到了现在点击点的位置调用movehandle这个方法对视图进行了绘制,所以这个问题怎么解决呐?
很简单,首先获取绘制的圆弧的最大Y值maxY,然后在continueTrackingWithTouch这个方法里面做判断,只要最终触摸点的y值大于等于此maxY(超出了圆弧),直接return NO就好了。修改后的代码如下

#pragma mark - 持续滑动触发事件
-(BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event{
    [super continueTrackingWithTouch:touch withEvent:event];
    //获取触摸点
    CGPoint lastPoint = [touch locationInView:self];

    // 超出圆弧部分直接返回NO(解决滑动超出圆弧范围的异常问题)
    if (lastPoint.y >= self.maxY) {
        return NO;
    }

    NSLog(@"%@",NSStringFromCGPoint(lastPoint));

    // 非 money图片 不可点击
    CGFloat deltaX = lastPoint.x - self.frame.size.width/2;
    CGFloat deltaY = lastPoint.y - self.frame.size.width/2;
    CGFloat distanceBetweenPoints = sqrt(deltaX*deltaX + deltaY*deltaY);
    //    NSLog(@"======%f",distanceBetweenPoints);

    if (distanceBetweenPoints>= radius - 50 && distanceBetweenPoints <= radius+ 20) {
        //使用触摸点来移动小块
        [self movehandle:lastPoint];
    }

    //发送值改变事件
    [self sendActionsForControlEvents:UIControlEventValueChanged];
    return YES;
}


效果如下:

jjwtgif.gif

问题得到完美解决
关于怎么获取最大maxY值以及钱标图片的定位问题,在drawRect方法中直接调用

//最大Y值
self.maxY = [self pointFromAngle:self.startAngle].y;

//3.绘制拖动小块
CGPoint handleCenter =  [self pointFromAngle: (self.angle)];
// 图片作进一步处理
self.imagev.frame = CGRectMake(handleCenter.x-moneyImgWidth/2, handleCenter.y - moneyImgWidth/2, moneyImgWidth, moneyImgWidth);


现在我们滑动或点击绘制的功能完成了,那怎么用此功能来根据弧度计算我们要展示的数字(金额)呐?
首先看看我们VC中的代码

- (void)viewDidLoad {
    [super viewDidLoad];

    self.circleView = [[SXCircleView alloc]initWithFrame:CGRectMake(self.view.frame.size.width/2 - 219/2, 100, 219, 219) lineWidth:10 circleAngle:240 imageName:@"qian"];

    //ControlEvents记得选择UIControlEventValueChanged
    [self.circleView addTarget:self action:@selector(newValue:) forControlEvents:UIControlEventValueChanged];
    // 设置初始角度
    [self.circleView changeAngle:-90];
    [self.view addSubview:self.circleView];

    self.moneyLabel = [[UILabel alloc]initWithFrame:CGRectMake(0, 100+219/2-25, self.view.frame.size.width, 30)];
    self.moneyLabel.textColor = [UIColor redColor];
    self.moneyLabel.font = [UIFont systemFontOfSize:30];
    self.moneyLabel.textAlignment = NSTextAlignmentCenter;
    self.moneyLabel.text = @"6000";
    [self.view addSubview:self.moneyLabel];
}

- (void) newValue:(SXCircleView*)slider{
    NSLog(@"newValue:%d",slider.angle);
}


运行后打印结果如下:

屏幕快照 2018-02-28 下午3.01.52.png
到一定角度时候执行了多次,猜想是

-(BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event 方法导致的结果

目前还没有想到好的解决办法(不影响功能的实现,但总体性能上总会有些许影响),看到的童鞋如果有好的办法希望私聊我,在此谢过!

竟然可以获取弧度,那么根据弧度的变化更改UILabel的数字就简单了

/*
 *
 *   @param 2000                       最小金额
 *   @param 10000                      最大金额
 *   @param (slider.angle+210)         当前弧度
 *   @param 240                        总弧度
 *
 */
- (void) newValue:(SXCircleView*)slider{

    NSLog(@"newValue:%d",slider.angle);

    CGFloat xl;
    xl = 2000/100+(slider.angle+210) * (10000/100 - 2000/100)/240;
    self.moneyLabel.text = [NSString stringWithFormat:@"%.f00",xl];
}


运行效果如下

zzlgif2.gif

最后的最后就是关于怎么使用这个封装类的问题了
直接将demo中的SXCircleView、UIColor+SX(颜色处理的分类)类引入到项目中
在你需要用的的VC中按照你们的需求修改相应数据就好了

- (void)viewDidLoad {
    [super viewDidLoad];

    self.circleView = [[SXCircleView alloc]initWithFrame:CGRectMake(self.view.frame.size.width/2 - 219/2, 100, 219, 219) lineWidth:10 circleAngle:240 imageName:@"qian"];


    [self.circleView addTarget:self action:@selector(newValue:) forControlEvents:UIControlEventValueChanged];
    // 设置初始弧度(我设的局中,因为总弧度240,-210 -> 30 所以-90就是居中的弧度)
    [self.circleView changeAngle:-90];
    [self.view addSubview:self.circleView];

    self.moneyLabel = [[UILabel alloc]initWithFrame:CGRectMake(0, 100+219/2-25, self.view.frame.size.width, 30)];
    self.moneyLabel.textColor = [UIColor redColor];
    self.moneyLabel.font = [UIFont systemFontOfSize:30];
    self.moneyLabel.textAlignment = NSTextAlignmentCenter;
    //因为设置了弧度-90(居中) 所以label初始值也应该是中间值(假设最小值2000,最大值10000)(2000+10000)/ 2
    self.moneyLabel.text = @"6000";
    [self.view addSubview:self.moneyLabel];
}

#pragma mark - 滑动或点击刻度表触发事件
/*
 * @param 2000             最小值
 * @param 10000            最大值
 * @param slider.angle+210 当前弧度
 * @param 240              总弧度
 *
 */
- (void) newValue:(SXCircleView*)slider{
    NSLog(@"newValue:%d",slider.angle);

    CGFloat xl;
    xl = 2000/100+(slider.angle+210) * (10000/100 - 2000/100)/240;
    self.moneyLabel.text = [NSString stringWithFormat:@"%.f00",xl];
}


如果在集成过程中有什么疑问欢迎私信!

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

推荐阅读更多精彩内容