// // HDLDiyArcSeekbar.m // HDL_Widget_iOS // // Created by 陈嘉乐 on 2020/6/15. // Copyright © 2020 JLChen. All rights reserved. // #import "HDLDiyArcSeekbar.h" #import "HDLUtlisXM.h" //#define APP_DEFAULT_BG HEXCOLOR(0xF2F3F7) #define APP_DEFAULT_BG_OFFLINE HEXCOLOR(0xDFE1E6) #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 180.0f // 开口角度 @interface HDLDiyArcSeekbar () /** 开口角度 */ @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; /** 进度渐变颜色数组 */ @property (nonatomic, strong) NSArray *mArcOfflineProgressBarColors; /** 边距 */ @property (nonatomic, assign) int DiyBarPadding; /** 是否离线 */ @property (nonatomic, assign) BOOL isOffline; /** 是否显示拖动按钮 */ @property (nonatomic, assign) BOOL isThumbImgBtnShow; @end @implementation HDLDiyArcSeekbar{ int _mProgressAngle; //当前进度值对应的角度 int _fixedAngle; int _mMoveCount; float _mArcStartAngle; float _mArcEndAngle; float _mArcAngle; CGPoint _mCenterPoint; //圆心坐标 CGFloat _mArcRadius; //半径 CGFloat _mArcHeight; bool _bIsInArcProgress; UIImage *_thumbImgBtn; CGFloat _thumbImgBtnWidth; } -(instancetype)initWithFrame:(CGRect)frame{ self = [super initWithFrame:frame]; if (self) { _isClickable = YES; _mMoveCount = 0; _mOpenAngle = DEFAULT_OPEN_ANGLE; _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 = 10.0f; _thumbImgBtnWidth = _mArcWidth * 2; _DiyBarPadding = _thumbImgBtnWidth/2+5; _isThumbImgBtnShow = YES; _mArcBackBarColor = HEXCOLOR(0xF2F3F7); [self setProgressBarColor:HEXCOLOR(0xFFEB3B)]; [self setOfflineProgressBarColor:HEXCOLOR(0xD3D3D3)]; [self refreshFrame]; self.backgroundColor = [UIColor clearColor]; _thumbImgBtn = [UIImage imageNamed:@"ic_wd_curtain_h_open"]; } return self; } /** 刷新布局,更新关键值 */ -(void)refreshFrame{ if(self.frame.size.height > self.frame.size.width){ _mArcHeight = self.frame.size.width; }else{ _mArcHeight = self.frame.size.height; } _mCenterPoint = CGPointMake(self.frame.size.width/2, self.frame.size.height/2); [self onDiyBarSizeChange]; } /** onDiyBarSizeChange 刷新布局,更新关键值 */ -(void)onDiyBarSizeChange{ _mArcRadius = _mArcHeight/2 - _mArcWidth/2 - _DiyBarPadding; } #pragma mark drawRect 绘制图形 -(void)drawRect:(CGRect)rect{ [super drawRect:rect]; CGContextRef ctx = UIGraphicsGetCurrentContext(); //将当前图形状态推入堆栈 CGContextSaveGState(ctx); //*********绘制固定的背景圆弧********* if(_mArcEndAngle == 90){ CGContextAddArc(ctx, self.frame.size.width/2, self.frame.size.height/2, _mArcRadius, M_PI/180*_mArcStartAngle, M_PI/180*450, 0); }else{ 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(); CGGradientRef gradient; if(!_isOffline){ gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef)_mArcProgressBarColors, NULL); }else{ gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef)_mArcOfflineProgressBarColors, 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{ if(!_isThumbImgBtnShow) return; CGPoint mThumbCenterPoint = [self getThumbCenterPointFromAngle: _mProgressAngle]; CGRect rectImage = CGRectMake(mThumbCenterPoint.x - _thumbImgBtnWidth/2, mThumbCenterPoint.y - _thumbImgBtnWidth/2, _thumbImgBtnWidth, _thumbImgBtnWidth); [_thumbImgBtn drawInRect:rectImage]; } ///** // 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; [self setNeedsDisplay]; } /** 设置最大值最小值 @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)initWithFrameSeekBar:(CGRect) mCGRect{ self.frame = mCGRect; [self refreshFrame]; } ///** // 设置渐变效果 // // @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 oneColor 单一颜色 */ -(void)setOfflineProgressBarColor:(UIColor *)oneColor{ _mArcOfflineProgressBarColors = @[ (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; //} /** 设置边距 */ -(void)setSeekBarPadding:(int)Padding{ _DiyBarPadding = Padding; [self onDiyBarSizeChange]; } /** 设置按钮高度 */ -(void)setThumbImgBtnWidth:(int)mWidth{ _thumbImgBtnWidth = mWidth; _DiyBarPadding = _thumbImgBtnWidth/2+5; [self onDiyBarSizeChange]; } /** 设置拖动按钮图片 */ -(void)setThumbImgBtnImg:(UIImage*) mImage{ _thumbImgBtn = mImage; } /** 设置拖动按钮图片 */ -(void)setThumbImgBtnShow:(BOOL) isShow{ _isThumbImgBtnShow = isShow; } /** 设置是否离线 */ -(void)setOffline:(BOOL)isOffline{ _isOffline = isOffline; _isClickable = !_isOffline; [self setNeedsDisplay]; } /** 设置圆弧宽度 */ -(void)setArcWidth:(float)mArcWidth{ _mArcWidth = mArcWidth; [self onDiyBarSizeChange]; } @end