【iOS】自定义控件入门:可拖动的环形进度

有时候UIKit的标准控件并不能满足我们的需求,因此我们可以通过自定义控件得到满足我们需求的控件,例如这篇文章将教你如何自定义一个圆形的进度条,并且用户可以通过拖动进度条上的手柄来改变进度值。主要参考了这篇文章:HOW TO BUILD A CUSTOM CONTROL IN IOS。广告时间:我的一个免费APP:午睡闹钟 使用了这个控件,欢迎大家在AppStore搜索午睡闹钟进行下载使用。

我们的自定义控件继承自UIControl类,它是UIView的子类,是所有UIKit控件(UIButton, UISlider, UISwitch等等等)的父类。UIControl实例的作用是创建相应的逻辑来将action分发到相应的target,90%的情况下,它会根据自身的状态(例如Highlighted, Selected和Disabled等)来绘制用户界面。

UIControl主要完成三个重要任务:

  • 绘制用户界面
  • 跟踪用户的交互操作
  • Target-Action模式

因此,对于这篇文章中要完成的圆形进度条,我们将要完成以下任务:

绘制一个用户可以通过拖动手柄滑块来进行交互的用户界面,用户的操作会被转化为target对应的actions,控件将滑块的frame origin转换为0-360之间的一个值,并用于target/action上。下面将分三步进行讲解,这三步对应上面提到的三个重要任务。

1.1绘制用户界面


如图,我们将通过Core Graphics来绘制灰色的进度条背景、红色的进度条、滑块手柄,关于Core Graphics的基础知识,本文不作详细介绍.。

- (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);
    [[UIColor grayColor] setStroke];
    CGContextSetLineWidth(context, _lineWidth);
    CGContextSetLineCap(context, kCGLineCapButt);
    CGContextDrawPath(context, kCGPathStroke);
    
    //2.绘制进度
    CGContextAddArc(context, self.frame.size.width/2, self.frame.size.height/2,radius,0, ToRad(_angle), 0);
    [[UIColor redColor] setStroke];
    CGContextSetLineWidth(context, _lineWidth);
    CGContextSetLineCap(context, kCGLineCapRound);
    CGContextDrawPath(context, kCGPathStroke);
    
    //3.绘制拖动小块
    CGPoint handleCenter =  [self pointFromAngle: (self.angle)];
    CGContextSetShadowWithColor(context, CGSizeMake(0, 0), 3,[UIColor blueColor].CGColor);
    [[UIColor redColor] setStroke];
    CGContextSetLineWidth(context, _lineWidth*2);
    CGContextAddEllipseInRect(context, CGRectMake(handleCenter.x, handleCenter.y, _lineWidth*2, _lineWidth*2));
    CGContextDrawPath(context, kCGPathStroke);
}

其中pointFromAngle方法是从给定的角度得到圆环上对应的经纬度,只是简单的使用了一下基本的三角函数关系:
-(CGPoint)pointFromAngle:(int)angleInt{
    //中心点
    CGPoint centerPoint = CGPointMake(self.frame.size.width/2 - _lineWidth, self.frame.size.height/2 - _lineWidth);
    
    //根据角度得到圆环上的坐标
    CGPoint result;
    result.y = round(centerPoint.y + radius * sin(ToRad(angleInt))) ;
    result.x = round(centerPoint.x + radius * cos(ToRad(angleInt)));
    
    return result;
}


1.2跟踪用户的交互操作

完成了1.1,现在我们已经绘制好了用户界面,但是它还不能响应我们的触摸事件。我们只要重写(override)UIControl的三个方法,就可以跟踪用户操作。

1.2.1开始跟踪触摸事件

当用户在UIControl的bound内进行触摸,beginTrackingWithTouch方法会被调用,此方法会返回BOOL类型,返回Yes表示要继续跟踪触摸事件。

-(BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event{
    [super beginTrackingWithTouch:touch withEvent:event];
    return YES;
}

1.2.2 持续跟踪触摸时间

上面的方法返回了Yes,表示要继续跟踪触摸事件,所以当用户在屏幕上拖动时,continueTrackingWithTouch会被调用,该方法返回的BOOL值表示是否继续跟踪touch事件。通过该方法我们将根据用户触摸的位置更新手柄的位置。

-(BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event{
    [super continueTrackingWithTouch:touch withEvent:event];
    //获取触摸点
    CGPoint lastPoint = [touch locationInView:self];
    //使用触摸点来移动小块
    [self movehandle:lastPoint];
    //发送值改变事件
    [self sendActionsForControlEvents:UIControlEventValueChanged];
    return YES;
}
其中,我们调用了movehandle:方法来更新滑动手柄的位置:

-(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);
    //保存新角度
    self.angle = angleInt;
    //重新绘制
    [self setNeedsDisplay];
}

AngleFromNorth(CGPoint p1,CGPoint p2,BOOL flipped)方法是计算中心点到任意点的角度,

是从苹果是示例代码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 = ToDeg(radians);
    return (result >=0  ? result : result + 360.0);
}

1.2.3结束跟踪

当结束跟踪时,这个方法会被调用,我们的例子中不需要override这个方法

现在,圆形滑块控件可以工作了,拖动滑动手柄看看。

1.3 Target-Action模式

如果希望自己定制的控件与UIControl行为保持一致,那么当控件的值发生变化时,需要进行通知处理:使用sendActionsForControlEvents方法,并制定特定的事件类型,值改变对应的事件一般是UIControlEventValueChanged。

苹果已经预定义了许多事件类型(Xcode中,在UIControlEventValueChanged上cmd + 鼠标单击)。如果你的控件是继承自UITextField,那么我们可能会对UIControlEventEdigitingDidBegin感兴趣,如果要做一个touch Up action,可以使用UIControlTouchUpInside。在本文前部分的continueTrackingWithTouch方法里面,我们已经调用了sendActionsForControlEvents方法。这样处理之后,当控件值发生变化时,每一个对象(观察者——注册该事件)都会收到响应的通知。


2.使用自定义控件

除了通过用户触摸事件来改变进度,有时我们还想通过代码进行修改,我们只要自己实现存储角度的属性angle的set方法:
-(void)changeAngle:(int)angle{
    _angle = angle;
     [self sendActionsForControlEvents:UIControlEventValueChanged];
    [self setNeedsDisplay];
}

 
 好了,现在我们在viewcontroller中使用我们的控件: 
    JXCircleSlider *slider = [[JXCircleSlider alloc] initWithFrame:CGRectMake(0, 0, 250, 250)];
    slider.center = self.view.center;
    
    [slider addTarget:self action:@selector(newValue:) forControlEvents:UIControlEventValueChanged];

    [slider changeAngle:120];
    [self.view addSubview:slider];

我们先进行初始化,然后设置为居中显示,注册了值改变响应事件,当进度值改变时,会调用一下方法
-(void)newValue:(JXCircleSlider*)slider{
    NSLog(@"newValue:%d",slider.angle);
}

项目源代码:https://github.com/dolacmeng/JXCircleSlider

最终效果如下:






  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值