具有多个彩色边框的圆形 UIView 像饼图一样工作
Circular UIView with multiple coloured border working like a pie chart
我正在尝试设置游戏玩家的圆形头像,并在头像的圆形边框上使用饼图表示。
玩家 1 -
胜率 25%
损失 70%
绘制 5%
cell.selectedPhoto.frame = CGRectMake(cell.selectedPhoto.frame.origin.x, cell.selectedPhoto.frame.origin.y, 75, 75);
cell.selectedPhoto.clipsToBounds = YES;
cell.selectedPhoto.layer.cornerRadius = 75/2.0f;
cell.selectedPhoto.layer.borderColor=[UIColor orangeColor].CGColor;
cell.selectedPhoto.layer.borderWidth=2.5f;
cell.selectedBadge.layer.cornerRadius = 15;
我将 UIImageView 作为一个圆圈,已经具有单一的边框颜色。
乍一看,也许我需要清除 UIImageView 的边框,取而代之的是在 UIImageView 后面放置一个 UIView,这是一个标准的饼图,但是有没有更聪明的方法来做到这一点?
提前致谢。
我建议您为此创建一个自定义 UIView
子类,它管理各种 CALayer
对象来创建此效果。我本来打算在 Core Graphics 中做这件事,但是如果你想给它添加一些漂亮的动画,你会想要坚持使用 Core Animation。
所以让我们首先定义我们的接口。
/// Provides a simple interface for creating an avatar icon, with a pie-chart style border.
@interface AvatarView : UIView
/// The avatar image, to be displayed in the center.
@property (nonatomic) UIImage* avatarImage;
/// An array of float values to define the values of each portion of the border.
@property (nonatomic) NSArray* borderValues;
/// An array of UIColors to define the colors of the border portions.
@property (nonatomic) NSArray* borderColors;
/// The width of the outer border.
@property (nonatomic) CGFloat borderWidth;
/// Animates the border values from their current values to a new set of values.
-(void) animateToBorderValues:(NSArray*)borderValues duration:(CGFloat)duration;
@end
在这里我们可以设置头像图片、边框宽度,并提供颜色和值的数组。接下来,让我们着手实现它。首先,我们要定义一些我们要跟踪的变量。
@implementation AvatarView {
CALayer* avatarImageLayer; // the avatar image layer
NSMutableArray* borderLayers; // the array containing the portion border layers
UIBezierPath* borderLayerPath; // the path used to stroke the border layers
CGFloat radius; // the radius of the view
}
接下来,让我们设置 avatarImageLayer
,以及 initWithFrame
方法中的几个其他变量:
-(instancetype) initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
radius = frame.size.width*0.5;
// create border layer array
borderLayers = [NSMutableArray array];
// create avatar image layer
avatarImageLayer = [CALayer layer];
avatarImageLayer.frame = frame;
avatarImageLayer.contentsScale = [UIScreen mainScreen].nativeScale; // scales the layer to the screen scale
[self.layer addSublayer:avatarImageLayer];
}
return self;
}
接下来让我们定义我们的方法,该方法将在 borderValues
属性 更新时填充边界层,从而允许视图具有动态数量的边界层。
-(void) populateBorderLayers {
while (borderLayers.count > _borderValues.count) { // remove layers if the number of border layers got reduced
[(CAShapeLayer*)[borderLayers lastObject] removeFromSuperlayer];
[borderLayers removeLastObject];
}
NSUInteger colorCount = _borderColors.count;
NSUInteger borderLayerCount = borderLayers.count;
while (borderLayerCount < _borderValues.count) { // add layers if the number of border layers got increased
CAShapeLayer* borderLayer = [CAShapeLayer layer];
borderLayer.path = borderLayerPath.CGPath;
borderLayer.fillColor = [UIColor clearColor].CGColor;
borderLayer.lineWidth = _borderWidth;
borderLayer.strokeColor = (borderLayerCount < colorCount)? ((UIColor*)_borderColors[borderLayerCount]).CGColor : [UIColor clearColor].CGColor;
if (borderLayerCount != 0) { // set pre-animation border stroke positions.
CAShapeLayer* previousLayer = borderLayers[borderLayerCount-1];
borderLayer.strokeStart = previousLayer.strokeEnd;
borderLayer.strokeEnd = previousLayer.strokeEnd;
} else borderLayer.strokeEnd = 0.0; // default value for first layer.
[self.layer insertSublayer:borderLayer atIndex:0]; // not strictly necessary, should work fine with `addSublayer`, but nice to have to ensure the layers don't unexpectedly overlap.
[borderLayers addObject:borderLayer];
borderLayerCount++;
}
}
接下来,我们想要创建一个方法,可以在 borderValues
更新时更新图层的描边开始和结束值。这可以合并到以前的方法中,但是如果你想设置动画,你会想把它分开。
-(void) updateBorderStrokeValues {
NSUInteger i = 0;
CGFloat cumulativeValue = 0;
for (CAShapeLayer* s in borderLayers) {
s.strokeStart = cumulativeValue;
cumulativeValue += [_borderValues[i] floatValue];
s.strokeEnd = cumulativeValue;
i++;
}
}
接下来,我们只需要覆盖设置器,以便在值更改时更新边框和头像图像的某些方面:
-(void) setAvatarImage:(UIImage *)avatarImage {
_avatarImage = avatarImage;
avatarImageLayer.contents = (id)avatarImage.CGImage; // update contents if image changed
}
-(void) setBorderWidth:(CGFloat)borderWidth {
_borderWidth = borderWidth;
CGFloat halfBorderWidth = borderWidth*0.5; // we're gonna use this a bunch, so might as well pre-calculate
// set the new border layer path
borderLayerPath = [UIBezierPath bezierPathWithArcCenter:(CGPoint){radius, radius} radius:radius-halfBorderWidth startAngle:-M_PI*0.5 endAngle:M_PI*1.5 clockwise:YES];
for (CAShapeLayer* s in borderLayers) { // apply the new border layer path
s.path = borderLayerPath.CGPath;
s.lineWidth = borderWidth;
}
// update avatar masking
CAShapeLayer* s = [CAShapeLayer layer];
avatarImageLayer.frame = CGRectMake(halfBorderWidth, halfBorderWidth, self.frame.size.width-borderWidth, self.frame.size.height-borderWidth); // update avatar image frame
s.path = [UIBezierPath bezierPathWithArcCenter:(CGPoint){radius-halfBorderWidth, radius-halfBorderWidth} radius:radius-borderWidth startAngle:0 endAngle:M_PI*2.0 clockwise:YES].CGPath;
avatarImageLayer.mask = s;
}
-(void) setBorderColors:(NSArray *)borderColors {
_borderColors = borderColors;
NSUInteger i = 0;
for (CAShapeLayer* s in borderLayers) {
s.strokeColor = ((UIColor*)borderColors[i]).CGColor;
i++;
}
}
-(void) setBorderValues:(NSArray *)borderValues {
_borderValues = borderValues;
[self populateBorderLayers];
[self updateBorderStrokeValues];
}
最后,我们甚至可以更进一步,为图层设置动画!让我们添加一个可以为我们处理这个的方法。
-(void) animateToBorderValues:(NSArray *)borderValues duration:(CGFloat)duration {
_borderValues = borderValues; // update border values
[self populateBorderLayers]; // do a 'soft' layer update, making sure that the correct number of layers are generated pre-animation. Pre-sets stroke positions to a pre-animation state.
// define stroke animation
CABasicAnimation* strokeAnim = [CABasicAnimation animation];
strokeAnim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
strokeAnim.duration = duration;
CGFloat cumulativeValue = 0;
for (int i = 0; i < borderLayers.count; i++) {
cumulativeValue += [borderValues[i] floatValue];
CAShapeLayer* s = borderLayers[i];
if (i != 0) [s addAnimation:strokeAnim forKey:@"startStrokeAnim"];
// define stroke end animation
strokeAnim.keyPath = @"strokeEnd";
strokeAnim.fromValue = @(s.strokeEnd);
strokeAnim.toValue = @(cumulativeValue);
[s addAnimation:strokeAnim forKey:@"endStrokeAnim"];
strokeAnim.keyPath = @"strokeStart"; // re-use the previous animation, as the values are the same (in the next iteration).
}
// update presentation layer values
[CATransaction begin];
[CATransaction setDisableActions:YES];
[self updateBorderStrokeValues]; // sets stroke positions.
[CATransaction commit];
}
就是这样!这是一个用法示例:
AvatarView* v = [[AvatarView alloc] initWithFrame:CGRectMake(50, 50, 200, 200)];
v.avatarImage = [UIImage imageNamed:@"photo.png"];
v.borderWidth = 10;
v.borderColors = @[[UIColor colorWithRed:122.0/255.0 green:108.0/255.0 blue:255.0/255.0 alpha:1],
[UIColor colorWithRed:100.0/255.0 green:241.0/255.0 blue:183.0/255.0 alpha:1],
[UIColor colorWithRed:0 green:222.0/255.0 blue:255.0/255.0 alpha:1]];
// because the border values default to 0, you can add this without even setting the border values initially!
[v animateToBorderValues:@[@(0.4), @(0.35), @(0.25)] duration:2];
结果
实际上您可以直接从 CALayer
创建自己的图层。这是我自己项目中的示例动画层。
AnimationLayer.h
#import <QuartzCore/QuartzCore.h>
@interface AnimationLayer : CALayer
@property (nonatomic,assign ) float percent;
@property (nonatomic, strong) NSArray *percentValues;
@property (nonatomic, strong) NSArray *percentColours;
@end
percentValues
是您获取零件的值。
它应该是 @[@(35),@(75),@(100)]
,赢率:%35,松散:%40,平局:%25。
percentColors
是 UIColor
个获胜、松散和平局对象。
in `AnimationLayer.m`
#import "AnimationLayer.h"
#import <UIKit/UIKit.h>
@implementation AnimationLayer
@dynamic percent,percentValues,percentColours;
+ (BOOL)needsDisplayForKey:(NSString *)key{
if([key isEqualToString:@"percent"]){
return YES;
}else
return [super needsDisplayForKey:key];
}
- (void)drawInContext:(CGContextRef)ctx
{
CGFloat arcStep = (M_PI *2) / 100 * (1.0-self.percent); // M_PI*2 is equivalent of full cirle
BOOL clockwise = NO;
CGFloat x = CGRectGetWidth(self.bounds) / 2; // circle's center
CGFloat y = CGRectGetHeight(self.bounds) / 2; // circle's center
CGFloat radius = MIN(x, y);
UIGraphicsPushContext(ctx);
// draw colorful circle
CGContextSetLineWidth(ctx, 12);//12 is the width of circle.
CGFloat toDraw = (1-self.percent)*100.0f;
for (CGFloat i = 0; i < toDraw; i++)
{
UIColor *c;
for (int j = 0; j<[self.percentValues count]; j++)
{
if (i <= [self.percentValues[j] intValue]) {
c = self.percentColours[j];
break;
}
}
CGContextSetStrokeColorWithColor(ctx, c.CGColor);
CGFloat startAngle = i * arcStep;
CGFloat endAngle = startAngle + arcStep+0.02;
CGContextAddArc(ctx, x, y, radius-6, startAngle, endAngle, clockwise);//set the radius as radius-(half of your line width.)
CGContextStrokePath(ctx);
}
UIGraphicsPopContext();
}
@end
在你要用到这个效果的地方,你应该这样称呼它
+(void)addAnimationLayerToView:(UIView *)imageOfPlayer withColors:(NSArray *)colors andValues:(NSArray *)values
{
AnimationLayer *animLayer = [AnimationLayer layer];
animLayer.frame = imageOfPlayer.bounds;
animLayer.percentColours = colors;
animLayer.percentValues = values;
[imageOfPlayer.layer insertSublayer:animLayer atIndex:0];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"percent"];
[animation setFromValue:@1];
[animation setToValue:@0];
[animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
[animation setDuration:6];
[animLayer addAnimation:animation forKey:@"imageAnimation"];
}
我正在尝试设置游戏玩家的圆形头像,并在头像的圆形边框上使用饼图表示。
玩家 1 - 胜率 25% 损失 70% 绘制 5%
cell.selectedPhoto.frame = CGRectMake(cell.selectedPhoto.frame.origin.x, cell.selectedPhoto.frame.origin.y, 75, 75);
cell.selectedPhoto.clipsToBounds = YES;
cell.selectedPhoto.layer.cornerRadius = 75/2.0f;
cell.selectedPhoto.layer.borderColor=[UIColor orangeColor].CGColor;
cell.selectedPhoto.layer.borderWidth=2.5f;
cell.selectedBadge.layer.cornerRadius = 15;
我将 UIImageView 作为一个圆圈,已经具有单一的边框颜色。
乍一看,也许我需要清除 UIImageView 的边框,取而代之的是在 UIImageView 后面放置一个 UIView,这是一个标准的饼图,但是有没有更聪明的方法来做到这一点?
提前致谢。
我建议您为此创建一个自定义 UIView
子类,它管理各种 CALayer
对象来创建此效果。我本来打算在 Core Graphics 中做这件事,但是如果你想给它添加一些漂亮的动画,你会想要坚持使用 Core Animation。
所以让我们首先定义我们的接口。
/// Provides a simple interface for creating an avatar icon, with a pie-chart style border.
@interface AvatarView : UIView
/// The avatar image, to be displayed in the center.
@property (nonatomic) UIImage* avatarImage;
/// An array of float values to define the values of each portion of the border.
@property (nonatomic) NSArray* borderValues;
/// An array of UIColors to define the colors of the border portions.
@property (nonatomic) NSArray* borderColors;
/// The width of the outer border.
@property (nonatomic) CGFloat borderWidth;
/// Animates the border values from their current values to a new set of values.
-(void) animateToBorderValues:(NSArray*)borderValues duration:(CGFloat)duration;
@end
在这里我们可以设置头像图片、边框宽度,并提供颜色和值的数组。接下来,让我们着手实现它。首先,我们要定义一些我们要跟踪的变量。
@implementation AvatarView {
CALayer* avatarImageLayer; // the avatar image layer
NSMutableArray* borderLayers; // the array containing the portion border layers
UIBezierPath* borderLayerPath; // the path used to stroke the border layers
CGFloat radius; // the radius of the view
}
接下来,让我们设置 avatarImageLayer
,以及 initWithFrame
方法中的几个其他变量:
-(instancetype) initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
radius = frame.size.width*0.5;
// create border layer array
borderLayers = [NSMutableArray array];
// create avatar image layer
avatarImageLayer = [CALayer layer];
avatarImageLayer.frame = frame;
avatarImageLayer.contentsScale = [UIScreen mainScreen].nativeScale; // scales the layer to the screen scale
[self.layer addSublayer:avatarImageLayer];
}
return self;
}
接下来让我们定义我们的方法,该方法将在 borderValues
属性 更新时填充边界层,从而允许视图具有动态数量的边界层。
-(void) populateBorderLayers {
while (borderLayers.count > _borderValues.count) { // remove layers if the number of border layers got reduced
[(CAShapeLayer*)[borderLayers lastObject] removeFromSuperlayer];
[borderLayers removeLastObject];
}
NSUInteger colorCount = _borderColors.count;
NSUInteger borderLayerCount = borderLayers.count;
while (borderLayerCount < _borderValues.count) { // add layers if the number of border layers got increased
CAShapeLayer* borderLayer = [CAShapeLayer layer];
borderLayer.path = borderLayerPath.CGPath;
borderLayer.fillColor = [UIColor clearColor].CGColor;
borderLayer.lineWidth = _borderWidth;
borderLayer.strokeColor = (borderLayerCount < colorCount)? ((UIColor*)_borderColors[borderLayerCount]).CGColor : [UIColor clearColor].CGColor;
if (borderLayerCount != 0) { // set pre-animation border stroke positions.
CAShapeLayer* previousLayer = borderLayers[borderLayerCount-1];
borderLayer.strokeStart = previousLayer.strokeEnd;
borderLayer.strokeEnd = previousLayer.strokeEnd;
} else borderLayer.strokeEnd = 0.0; // default value for first layer.
[self.layer insertSublayer:borderLayer atIndex:0]; // not strictly necessary, should work fine with `addSublayer`, but nice to have to ensure the layers don't unexpectedly overlap.
[borderLayers addObject:borderLayer];
borderLayerCount++;
}
}
接下来,我们想要创建一个方法,可以在 borderValues
更新时更新图层的描边开始和结束值。这可以合并到以前的方法中,但是如果你想设置动画,你会想把它分开。
-(void) updateBorderStrokeValues {
NSUInteger i = 0;
CGFloat cumulativeValue = 0;
for (CAShapeLayer* s in borderLayers) {
s.strokeStart = cumulativeValue;
cumulativeValue += [_borderValues[i] floatValue];
s.strokeEnd = cumulativeValue;
i++;
}
}
接下来,我们只需要覆盖设置器,以便在值更改时更新边框和头像图像的某些方面:
-(void) setAvatarImage:(UIImage *)avatarImage {
_avatarImage = avatarImage;
avatarImageLayer.contents = (id)avatarImage.CGImage; // update contents if image changed
}
-(void) setBorderWidth:(CGFloat)borderWidth {
_borderWidth = borderWidth;
CGFloat halfBorderWidth = borderWidth*0.5; // we're gonna use this a bunch, so might as well pre-calculate
// set the new border layer path
borderLayerPath = [UIBezierPath bezierPathWithArcCenter:(CGPoint){radius, radius} radius:radius-halfBorderWidth startAngle:-M_PI*0.5 endAngle:M_PI*1.5 clockwise:YES];
for (CAShapeLayer* s in borderLayers) { // apply the new border layer path
s.path = borderLayerPath.CGPath;
s.lineWidth = borderWidth;
}
// update avatar masking
CAShapeLayer* s = [CAShapeLayer layer];
avatarImageLayer.frame = CGRectMake(halfBorderWidth, halfBorderWidth, self.frame.size.width-borderWidth, self.frame.size.height-borderWidth); // update avatar image frame
s.path = [UIBezierPath bezierPathWithArcCenter:(CGPoint){radius-halfBorderWidth, radius-halfBorderWidth} radius:radius-borderWidth startAngle:0 endAngle:M_PI*2.0 clockwise:YES].CGPath;
avatarImageLayer.mask = s;
}
-(void) setBorderColors:(NSArray *)borderColors {
_borderColors = borderColors;
NSUInteger i = 0;
for (CAShapeLayer* s in borderLayers) {
s.strokeColor = ((UIColor*)borderColors[i]).CGColor;
i++;
}
}
-(void) setBorderValues:(NSArray *)borderValues {
_borderValues = borderValues;
[self populateBorderLayers];
[self updateBorderStrokeValues];
}
最后,我们甚至可以更进一步,为图层设置动画!让我们添加一个可以为我们处理这个的方法。
-(void) animateToBorderValues:(NSArray *)borderValues duration:(CGFloat)duration {
_borderValues = borderValues; // update border values
[self populateBorderLayers]; // do a 'soft' layer update, making sure that the correct number of layers are generated pre-animation. Pre-sets stroke positions to a pre-animation state.
// define stroke animation
CABasicAnimation* strokeAnim = [CABasicAnimation animation];
strokeAnim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
strokeAnim.duration = duration;
CGFloat cumulativeValue = 0;
for (int i = 0; i < borderLayers.count; i++) {
cumulativeValue += [borderValues[i] floatValue];
CAShapeLayer* s = borderLayers[i];
if (i != 0) [s addAnimation:strokeAnim forKey:@"startStrokeAnim"];
// define stroke end animation
strokeAnim.keyPath = @"strokeEnd";
strokeAnim.fromValue = @(s.strokeEnd);
strokeAnim.toValue = @(cumulativeValue);
[s addAnimation:strokeAnim forKey:@"endStrokeAnim"];
strokeAnim.keyPath = @"strokeStart"; // re-use the previous animation, as the values are the same (in the next iteration).
}
// update presentation layer values
[CATransaction begin];
[CATransaction setDisableActions:YES];
[self updateBorderStrokeValues]; // sets stroke positions.
[CATransaction commit];
}
就是这样!这是一个用法示例:
AvatarView* v = [[AvatarView alloc] initWithFrame:CGRectMake(50, 50, 200, 200)];
v.avatarImage = [UIImage imageNamed:@"photo.png"];
v.borderWidth = 10;
v.borderColors = @[[UIColor colorWithRed:122.0/255.0 green:108.0/255.0 blue:255.0/255.0 alpha:1],
[UIColor colorWithRed:100.0/255.0 green:241.0/255.0 blue:183.0/255.0 alpha:1],
[UIColor colorWithRed:0 green:222.0/255.0 blue:255.0/255.0 alpha:1]];
// because the border values default to 0, you can add this without even setting the border values initially!
[v animateToBorderValues:@[@(0.4), @(0.35), @(0.25)] duration:2];
结果
实际上您可以直接从 CALayer
创建自己的图层。这是我自己项目中的示例动画层。
AnimationLayer.h
#import <QuartzCore/QuartzCore.h>
@interface AnimationLayer : CALayer
@property (nonatomic,assign ) float percent;
@property (nonatomic, strong) NSArray *percentValues;
@property (nonatomic, strong) NSArray *percentColours;
@end
percentValues
是您获取零件的值。
它应该是 @[@(35),@(75),@(100)]
,赢率:%35,松散:%40,平局:%25。
percentColors
是 UIColor
个获胜、松散和平局对象。
in `AnimationLayer.m`
#import "AnimationLayer.h"
#import <UIKit/UIKit.h>
@implementation AnimationLayer
@dynamic percent,percentValues,percentColours;
+ (BOOL)needsDisplayForKey:(NSString *)key{
if([key isEqualToString:@"percent"]){
return YES;
}else
return [super needsDisplayForKey:key];
}
- (void)drawInContext:(CGContextRef)ctx
{
CGFloat arcStep = (M_PI *2) / 100 * (1.0-self.percent); // M_PI*2 is equivalent of full cirle
BOOL clockwise = NO;
CGFloat x = CGRectGetWidth(self.bounds) / 2; // circle's center
CGFloat y = CGRectGetHeight(self.bounds) / 2; // circle's center
CGFloat radius = MIN(x, y);
UIGraphicsPushContext(ctx);
// draw colorful circle
CGContextSetLineWidth(ctx, 12);//12 is the width of circle.
CGFloat toDraw = (1-self.percent)*100.0f;
for (CGFloat i = 0; i < toDraw; i++)
{
UIColor *c;
for (int j = 0; j<[self.percentValues count]; j++)
{
if (i <= [self.percentValues[j] intValue]) {
c = self.percentColours[j];
break;
}
}
CGContextSetStrokeColorWithColor(ctx, c.CGColor);
CGFloat startAngle = i * arcStep;
CGFloat endAngle = startAngle + arcStep+0.02;
CGContextAddArc(ctx, x, y, radius-6, startAngle, endAngle, clockwise);//set the radius as radius-(half of your line width.)
CGContextStrokePath(ctx);
}
UIGraphicsPopContext();
}
@end
在你要用到这个效果的地方,你应该这样称呼它
+(void)addAnimationLayerToView:(UIView *)imageOfPlayer withColors:(NSArray *)colors andValues:(NSArray *)values
{
AnimationLayer *animLayer = [AnimationLayer layer];
animLayer.frame = imageOfPlayer.bounds;
animLayer.percentColours = colors;
animLayer.percentValues = values;
[imageOfPlayer.layer insertSublayer:animLayer atIndex:0];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"percent"];
[animation setFromValue:@1];
[animation setToValue:@0];
[animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
[animation setDuration:6];
[animLayer addAnimation:animation forKey:@"imageAnimation"];
}