具有多个彩色边框的圆形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的边框,而将UIView放在UIImageView后面,这是一个标准的饼图,但是有一种更聪明的方法吗?
谢谢.
我建议您为此创建一个自定义的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];
结果
完整项目: https://github.com/hamishknight/Pie-Chart -头像
I am trying to set a circular avatar of a player of a game with a piechart representation on the avatar's circular border.
Player 1 - Wins 25% Lost 70% Drawn 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;
I have the UIImageView as a circle already with a single border colour.
At first guess perhaps I will need to clear the border of my UIImageView and have instead a UIView sitting behind my UIImageView that is a standard piechart, but is there a smarter way of doing this?
Thank you in advance.
I would recommend you create a custom UIView
subclass for this, that manages various CALayer
objects to create this effect. I was going to set about doing this in Core Graphics, but if you ever want to add some nice animations to this, you'll want to stick with Core Animation.
So let's first define our interface.
/// 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
Here we can set the avatar image, border width, and provide an array of colors and values. Next, lets work on implementing this. First we'll want to define some variables that we'll want to keep track of.
@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
}
Next, lets setup our avatarImageLayer
, as well as a couple other variables in the initWithFrame
method:
-(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;
}
Next let's define our method that will populate the border layers when the borderValues
property updates, allowing the view to have a dynamic number of border layers.
-(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++;
}
}
Next, we want to make a method that can update the layer's stroke start and end values when borderValues
gets updated. This could be merged into previous method, but if you want to setup animation you'll want to keep it separate.
-(void) updateBorderStrokeValues {
NSUInteger i = 0;
CGFloat cumulativeValue = 0;
for (CAShapeLayer* s in borderLayers) {
s.strokeStart = cumulativeValue;
cumulativeValue += [_borderValues[i] floatValue];
s.strokeEnd = cumulativeValue;
i++;
}
}
Next, we just need to override the setters in order to update certain aspects of the border and avatar image when the values change:
-(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];
}
Finally, we can even take one step further by animating the layers! Let's just add a single of method that can handle this for us.
-(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];
}
And that's it! Here's an example of the usage:
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];
Results
Full project: https://github.com/hamishknight/Pie-Chart-Avatar
这篇关于具有多个彩色边框的圆形UIView就像饼图一样工作的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!