有时候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的基础知识,本文不作详细介绍,如果大家有需要可以留言,我以后有空将给大家翻译官方的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
最终效果如下: