// // HDLArcSeekbar.m // HDL-ArcSeekbar // // Created by HDL on 2019/7/19. // Copyright © 2019 JLChen. All rights reserved. // #import "HDLArcSeekbar.h" #define ToRad(deg) ( (M_PI * (deg)) / 180.0 ) #define ToDeg(rad) ( (180.0 * (rad)) / M_PI ) #define SQR(x) ( (x) * (x) ) #define GETEndAngle(X) ( ((90-X) < 0) ? ((90-X)+360) : (90-X) ) #define GETStartAngle(X) ( 90 + X) //#define DEFAULT_DISTANCE_BETWEEN_TEXTPOINT_AND_ARC 30 // 进度显示文字坐标与进度圆弧的距离 #define DEFAULT_PADDING 40 //圆弧自带PADDING值,为了显示进度test文字 #define DEFAULT_OPEN_ANGLE 30.0f // 开口角度 @interface HDLArcSeekbar () /** 开口角度 */ @property (nonatomic, assign) float mOpenAngle; //* 防止突变 //* 由于进度条时圆弧形状的,因此进度可能会从 0.0 直接突变到 1.0 或者相反,因此在计算进度与当前进度差异过大时,禁止改变当前进度. /** 是否允许突变 */ @property (nonatomic, assign) BOOL bAllowTouchSkip; /** 是否正在移动 */ @property (nonatomic, assign) BOOL bTouchMove; /** 进度单位符号 */ @property (nonatomic, strong) NSString *mProgressBarUnitSring; /** 最小值 */ @property (nonatomic, assign) float mMinValue; /** 最大值 */ @property (nonatomic, assign) float mMaxValue; /** 进度渐变颜色数组 */ @property (nonatomic, strong) NSArray *mArcProgressBarColors; @end @implementation HDLArcSeekbar{ int _mProgressAngle; //当前进度值对应的角度 int _fixedAngle; int _mMoveCount; float _mArcStartAngle; float _mArcEndAngle; float _mArcAngle; CGPoint _mCenterPoint; //圆心坐标 CGFloat _mArcRadius; //半径 CGFloat _mArcHeight; bool _bIsInArcProgress; } -(instancetype)initWithFrame:(CGRect)frame{ self = [super initWithFrame:frame]; if (self) { _isClickable = YES; _mMoveCount = 0; _mOpenAngle = DEFAULT_OPEN_ANGLE; _mThumbMode = HDL_THUMB_STROKE; _bAllowTouchSkip = NO; //禁止突变 _mArcStartAngle = GETStartAngle(_mOpenAngle/2); _mArcEndAngle = GETEndAngle(_mOpenAngle/2); _mArcAngle = 360.0f - _mOpenAngle; _mProgressAngle = _mArcStartAngle; _mMaxValue = 100.0f; _mMinValue = 0.0f; _mProgressCurrentValue = _mMinValue; _mArcWidth = 15.0f; _mThumbWidth = 17.0f; _mArcBackBarColor = APP_DEFAULT_BG; // _mArcProgressBarColor = APP_BACKGROUND; [self setProgressBarColor:UIColor.whiteColor]; _mThumbColor = [UIColor whiteColor]; _mProgressTextColor = [UIColor whiteColor]; _mProgressTextSize = 12; _mTextDefaultDistance = 30; _mProgressBarUnitSring = @"%"; if(self.frame.size.height > self.frame.size.width){ _mArcHeight = self.frame.size.width; }else{ _mArcHeight = self.frame.size.height; } _mArcRadius = _mArcHeight/2 - _mArcWidth/2 - DEFAULT_PADDING; _mCenterPoint = CGPointMake(self.frame.size.width/2, self.frame.size.height/2); self.backgroundColor = [UIColor clearColor]; } return self; } #pragma mark drawRect 绘制图形 -(void)drawRect:(CGRect)rect{ [super drawRect:rect]; CGContextRef ctx = UIGraphicsGetCurrentContext(); //将当前图形状态推入堆栈 CGContextSaveGState(ctx); //*********绘制固定的背景圆弧********* CGContextAddArc(ctx, self.frame.size.width/2, self.frame.size.height/2, _mArcRadius, M_PI/180*_mArcStartAngle, M_PI/180*_mArcEndAngle, 0); [self setEndCornersRounded:ctx mUIColor:_mArcBackBarColor];//设置末端圆角和颜色 //*********绘制动态的进度条圆弧********* CGContextSetLineWidth(ctx, _mArcWidth); // CGContextSetLineCap(ctx, kCGLineCapButt); CGContextSetLineCap(ctx, kCGLineCapRound); CGContextDrawPath(ctx, kCGPathStroke); CGContextAddArc(ctx, self.frame.size.width/2, self.frame.size.height/2, _mArcRadius, M_PI/180*_mArcStartAngle, M_PI/180*(_mProgressAngle), 0); // [self setEndCornersRounded:ctx mUIColor:_mArcProgressBarColor];//设置末端圆角和颜色 [self drawArcWithGradient:ctx rect:rect];//绘制渐变效果 //*********绘制拖动的Thumb圆形按钮********* [self drawThumbHandle:ctx]; //把堆栈顶部的状态弹出 CGContextRestoreGState(ctx); } /** 设置末端圆角 @param ctx ctx @param mUIColor 颜色 */ -(void)setEndCornersRounded:(CGContextRef)ctx mUIColor:(UIColor*)mUIColor{ //**********************末端圆角********************** CGContextSetStrokeColorWithColor(ctx, mUIColor.CGColor); CGContextSetLineWidth(ctx, _mArcWidth); CGContextSetLineCap(ctx, kCGLineCapRound); CGContextDrawPath(ctx, kCGPathStroke); } /** 绘制渐变效果 */ -(void)drawArcWithGradient:(CGContextRef)ctx rect:(CGRect)rect{ // 创建一个渐变色 // 创建RGB色彩空间,创建这个以后,context里面用的颜色都是用RGB表示 CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); // // 渐变色的颜色 // NSArray *colorArr = @[ // (id)[UIColor blueColor].CGColor, // (id)[UIColor yellowColor].CGColor // ]; CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef)_mArcProgressBarColors, NULL); // 释放色彩空间 CGColorSpaceRelease(colorSpace); colorSpace = NULL; // "反选路径" // CGContextReplacePathWithStrokedPath // 将context中的路径替换成路径的描边版本,使用参数context去计算路径(即创建新的路径是原来路径的描边)。用恰当的颜色填充得到的路径将产生类似绘制原来路径的效果。你可以像使用一般的路径一样使用它。例如,你可以通过调用CGContextClip去剪裁这个路径的描边 CGContextReplacePathWithStrokedPath(ctx); // 剪裁路径 CGContextClip(ctx); // 用渐变色填充 CGContextDrawLinearGradient(ctx, gradient, CGPointMake(0, rect.size.height / 2), CGPointMake(rect.size.width, rect.size.height / 2), 0); // 释放渐变色 CGGradientRelease(gradient); gradient = NULL; CGContextRestoreGState(ctx);// 恢复到之前的context CGContextSaveGState(ctx); } /** 绘制拖动Thumb按钮 @param ctx 画布 */ -(void)drawThumbHandle:(CGContextRef)ctx{ CGPoint mThumbCenterPoint = [self getThumbCenterPointFromAngle: _mProgressAngle]; [_mThumbColor setStroke]; [_mThumbColor setFill]; if(_mThumbMode == HDL_THUMB_FILL){ //实心圆 CGContextSetLineWidth(ctx, _mThumbWidth); CGContextFillEllipseInRect(ctx, CGRectMake(mThumbCenterPoint.x - _mThumbWidth/2, mThumbCenterPoint.y - _mThumbWidth/2, _mThumbWidth, _mThumbWidth)); }else{ //空心圆 CGContextSetLineWidth(ctx, _mThumbWidth/3); CGContextStrokeEllipseInRect(ctx, CGRectMake(mThumbCenterPoint.x - _mThumbWidth/2, mThumbCenterPoint.y -_mThumbWidth/2, _mThumbWidth, _mThumbWidth)); // CGContextAddEllipseInRect(ctx, CGRectMake(mThumbCenterPoint.x+2, mThumbCenterPoint.y+2, _mArcWidth-4, _mArcWidth-4)); // CGContextDrawPath(ctx, kCGPathStroke); } //绘制当前进度值文本 CGPoint outsidePoint = [self getCalcOutsidePoint:mThumbCenterPoint centerPoint:_mCenterPoint mRadius:_mArcRadius]; [self drawString:[NSString stringWithFormat:@"%d%@", (int8_t)roundf(_mProgressCurrentValue), _mProgressBarUnitSring] outsidePoint:outsidePoint]; } /** drawString 现在当前进度值 @param mText 显示文本 @param outsidePoint 坐标 */ - (void) drawString:(NSString *)mText outsidePoint:(CGPoint)outsidePoint{ NSMutableParagraphStyle *paragraph = [[NSMutableParagraphStyle alloc] init]; paragraph.alignment = NSTextAlignmentCenter; NSDictionary *dic = @{NSFontAttributeName : [UIFont systemFontOfSize:_mProgressTextSize], NSForegroundColorAttributeName : _mProgressTextColor, NSParagraphStyleAttributeName : paragraph }; // CGRect textRect = CGRectMake(outsidePoint.x, outsidePoint.y, 80, 20); // [mText drawInRect:textRect withAttributes:dic]; //2019-08-15 修改文字绘制方法,最终实现文本居中效果 CGSize textSize = [mText sizeWithAttributes:dic]; CGPoint textPoint = CGPointMake(outsidePoint.x - textSize.width/2, outsidePoint.y - textSize.height/2);//根据中点坐标绘制 [mText drawAtPoint:textPoint withAttributes:dic]; } /** 计算2点坐标之间的距离 @param startPoint 起点坐标 @param endPonit 终点坐标 @return 距离 */ -(float) getTwoPointDistance:(CGPoint)startPoint endPonit:(CGPoint)endPonit{ CGPoint v = CGPointMake(startPoint.x - endPonit.x, startPoint.y - endPonit.y); float d = sqrt(SQR(v.x) + SQR(v.y)); NSLog(@"2点距离:%f ", d); return d; } /* * 根据当前角度 计算出拖动按钮正确的圆心坐标 */ -(CGPoint)getThumbCenterPointFromAngle:(int)angleInt{ //Define the Circle center // CGPoint centerPoint = CGPointMake(self.frame.size.width/2 - _mArcWidth/2, self.frame.size.height/2 - _mArcWidth/2); // //Define The point position on the circumference // CGPoint result; // result.y = round(centerPoint.y + _mArcRadius * sin(ToRad(angleInt))); // result.x = round(centerPoint.x + _mArcRadius * cos(ToRad(angleInt))); CGPoint result; result.y = round(_mCenterPoint.y + _mArcRadius * sin(ToRad(angleInt))); result.x = round(_mCenterPoint.x + _mArcRadius * cos(ToRad(angleInt))); return result; } #pragma mark Touch Event 点击事件 -(BOOL) beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event { [super beginTrackingWithTouch:touch withEvent:event]; if(!_isClickable) return YES;//禁止点击 _bTouchMove = NO; _mMoveCount = 0; CGPoint startPoint = [touch locationInView:self]; _bIsInArcProgress = [self getStartPointIsInArcProgress:startPoint];//判断开始坐标是否在可点击区域 [self.mProgressChangedDelegate onStartTrackingTouch]; return YES; } -(void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event{ [super endTrackingWithTouch:touch withEvent:event]; if(!_isClickable) return;//禁止点击 if(!_bTouchMove){//没移动,仅点击 CGPoint lastPoint = [touch locationInView:self]; [self getEndPointIsInArcProgress:lastPoint];//判断最后离开的坐标是否在可点击区域 [self sendActionsForControlEvents:UIControlEventValueChanged]; } [self.mProgressChangedDelegate onStopTrackingTouch:(int8_t)roundf(_mProgressCurrentValue)]; } -(BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event { [super continueTrackingWithTouch:touch withEvent:event]; if(!_isClickable) return YES;//禁止点击 if(_mMoveCount < 2){ _mMoveCount++; }else{ _bTouchMove = YES; //开始移动 } // CGPoint lastPoint = [touch locationInView:self]; // [self getPointIsInArcProgress:lastPoint];//判断坐标是否在可点击区域 if(_bIsInArcProgress){//如果刚开始点击的坐标在可点击区域 CGPoint lastPoint = [touch locationInView:self]; [self getCurrentProgressWithLastPoint:lastPoint]; } [self sendActionsForControlEvents:UIControlEventValueChanged]; return YES; } /** 判断最后离开坐标是否在可点击区域 @param lastPoint 最后的坐标 */ -(void)getEndPointIsInArcProgress:(CGPoint)lastPoint { //用于排除点在圆外面点与圆心半径一半以内的点 if([self getStartPointIsInArcProgress:lastPoint]){ [self getCurrentProgressWithLastPoint:lastPoint]; } } /* * 判断坐标是否在可点击区域 * In the clickable area * R/2 R/2 * [----(R----.————圆心----.————R)----] * { 可点击区域 }*不可点区域*{ 可点击区域 } */ -(BOOL)getStartPointIsInArcProgress:(CGPoint)lastPoint { BOOL isIn = NO; //用于排除点在圆外面点与圆心半径一半以内的点 if ((lastPoint.x >= 0 &&lastPoint.x <= _mArcHeight)&&(lastPoint.y >= 0 && lastPoint.y <= _mArcHeight)) { CGFloat leftR = (_mArcHeight - _mArcRadius) / 2; CGFloat rightR = (_mArcHeight + _mArcRadius) / 2; if ((lastPoint.x <= leftR || lastPoint.x >= rightR) || (lastPoint.y <= leftR || lastPoint.y >= rightR)) { isIn = YES; } } return isIn; } /** 判断是否为突变坐标 @param oldP 上一次的进度角度 @param newP 当前坐标对应的角度 @return 判断结果 */ -(BOOL)getIfMutationPoint:(int)oldP newP:(int)newP{ return fabsf( (newP - oldP) / (_mArcStartAngle + _mArcAngle)) > 0.5f; } /** 根据坐标计算出当前真正的角度和对应的进度值 @param point 当前坐标 */ -(void)getCurrentProgressWithLastPoint:(CGPoint)point { // NSLog(@"_mProgressAngle start:%f ",_mProgressCurrentValue); int currentAngle = floor(AngleFromNorth(_mCenterPoint, point, NO)); if (currentAngle > _mArcEndAngle && currentAngle < _mArcStartAngle) { }else{ if (currentAngle <= _mArcEndAngle) { currentAngle += 360.0f; } if(_bTouchMove){ //移动的时候才防止突变 if(!_bAllowTouchSkip){ //禁止突变 // 不允许突变 if([self getIfMutationPoint:_mProgressAngle newP:currentAngle]){ return; } } _mProgressAngle = currentAngle; _mProgressCurrentValue = [self getProgressValueFromAngle]; //移动的时候才发送onProgressChanged代理事件 [self.mProgressChangedDelegate onProgressChanged:(int8_t)roundf(_mProgressCurrentValue)]; }else{ _mProgressAngle = currentAngle; _mProgressCurrentValue = [self getProgressValueFromAngle]; } // NSLog(@"_mProgressAngle end:%d ",_mProgressAngle); } [self setNeedsDisplay]; } 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); } /** 获取相同距离下,圆弧对应的第三点坐标 @param mPoint 当前圆弧坐标 @param centerPoint 圆弧圆心坐标 @param mRadius 圆弧半径 @return 圆外坐标 */ -(CGPoint) getCalcOutsidePoint:(CGPoint) mPoint centerPoint:(CGPoint)centerPoint mRadius:(float)mRadius { CGFloat newX = (mPoint.x * (mRadius + _mTextDefaultDistance) - centerPoint.x * _mTextDefaultDistance) / mRadius; CGFloat newY = (mPoint.y * (mRadius + _mTextDefaultDistance) - centerPoint.y * _mTextDefaultDistance) / mRadius; return CGPointMake(newX, newY); } /** 根据角度 计算出当前进度值 在这个地方调整进度条 @return 真正的进度值 */ -(float)getProgressValueFromAngle { if(_mProgressAngle <= _mArcEndAngle) { _mProgressCurrentValue = 360 - _mArcStartAngle + _mProgressAngle; } else if(_mProgressAngle > _mArcEndAngle && _mProgressAngle < _mArcStartAngle){ }else{ _mProgressCurrentValue = _mProgressAngle - _mArcStartAngle; } _fixedAngle = _mProgressCurrentValue; // NSLog(@"KK_mProgressCurrentValueangle:%d",_mProgressAngle); // NSLog(@"KK_mProgressCurrentValue:%f",_mProgressCurrentValue); return (_mProgressCurrentValue*(_mMaxValue - _mMinValue))/_mArcAngle + _mMinValue; } /** 根据进度值,计算当前 _mProgressAngle值 @param mProgress 进度值 */ -(void)setSeekBarProgressToValue:(int)mProgress{ if(mProgress < _mMinValue){ mProgress = _mMinValue; } if(mProgress > _mMaxValue){ mProgress = _mMaxValue; } // _mProgressCurrentValue = mProgress - _mMinValue; _mProgressCurrentValue = mProgress; _mProgressAngle = (mProgress - _mMinValue) * _mArcAngle / (_mMaxValue - _mMinValue) + _mArcStartAngle; // [self.mProgressChangedDelegate onProgressChanged:(int8_t)roundf(_mProgressCurrentValue)]; } #pragma mark 设置进度条位置 /** 设置进度值 @param mProgress 进度值 */ -(void)setProgress:(int)mProgress{ [self setSeekBarProgressToValue:mProgress]; [self setNeedsDisplay]; } /** 设置开口角度 @param mOpenAngle 开口角度 */ -(void)setOpenAngle:(float)mOpenAngle{ _mOpenAngle = mOpenAngle; _mArcStartAngle = GETStartAngle(_mOpenAngle/2); _mArcEndAngle = GETEndAngle(_mOpenAngle/2); _mArcAngle = 360.0f - _mOpenAngle; _mProgressAngle = _mArcStartAngle; } /** 设置进度显示值单位 @param mString 单位字符 */ -(void)setProgressBarUnitSring:(NSString *)mString{ _mProgressBarUnitSring = mString; } /** 设置最大值最小值 @param mMinValue 最小值 @param mMaxValue 最大值 */ -(void)setMinAndMaxValue:(float)mMinValue mMaxValue:(float)mMaxValue{ if(mMinValue < mMaxValue){ _mMinValue = mMinValue; _mMaxValue = mMaxValue; }else{ _mMinValue = mMaxValue; _mMaxValue = mMinValue; } _mProgressCurrentValue = _mMinValue; _mProgressAngle = _mProgressCurrentValue * _mArcAngle / (_mMaxValue - _mMinValue) + _mArcStartAngle; } -(void)initWithFrameArc:(CGRect) mCGRect{ self.frame = mCGRect; if(self.frame.size.height > self.frame.size.width){ _mArcHeight = self.frame.size.width; }else{ _mArcHeight = self.frame.size.height; } _mArcRadius = _mArcHeight/2 - _mArcWidth/2 - DEFAULT_PADDING; _mCenterPoint = CGPointMake(self.frame.size.width/2, self.frame.size.height/2); } /** 设置渐变效果 @param mColors 颜色数组 */ -(void)setProgressBarColors:(NSArray *)mColors{ // NSArray * colors = [NSArray arrayWithObjects:(id)startColor.CGColor,(id)endColor.CGColor,nil]; } /** 设置渐变效果 @param mColors 颜色数组 */ /** 设置渐变效果 @param startColor 开始颜色 @param endColor 结束颜色 */ -(void)setProgressBarColors:(UIColor *)startColor endColor:(UIColor*)endColor{ // _mArcProgressBarColors = [NSArray arrayWithObjects(id)startColor.CGColor,(id)endColor.CGColor,nil]; _mArcProgressBarColors = @[ (id)startColor.CGColor, (id)endColor.CGColor ]; } /** 设置进度条颜色 单一颜色 @param oneColor 单一颜色 */ -(void)setProgressBarColor:(UIColor *)oneColor{ // _mArcProgressBarColors = [NSArray arrayWithObjects(id)startColor.CGColor,(id)endColor.CGColor,nil]; _mArcProgressBarColors = @[ (id)oneColor.CGColor, (id)oneColor.CGColor ]; } ///** // 设置开口角度 // // @param mOpenAngle 开口角度 // */ //-(void)setOpenAngle:(float)mOpenAngle{ // _mOpenAngle = mOpenAngle; // _mArcStartAngle = GETStartAngle(_mOpenAngle/2); // _mArcEndAngle = GETEndAngle(_mOpenAngle/2); // _mArcAngle = 360.0f - _mOpenAngle; // _mProgressAngle = _mArcStartAngle; //} @end