Circular UIView with multiple coloured border working like a pie chart

六眼飞鱼酱① 提交于 2020-01-11 12:14:12

问题


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.


回答1:


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




回答2:


Actually you can directly create your own layer from CALayer. here is a sample Animation layer from my own project.

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 are your values for which part is gotten. it should be @[@(35),@(75),@(100)] for win ratio:%35, loose:%40 and draw:%25. percentColors are UIColor objects for win, loose and draw.

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

and in some place where you will use this effect, you should call this like

+(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"];
}



来源:https://stackoverflow.com/questions/35090451/circular-uiview-with-multiple-coloured-border-working-like-a-pie-chart

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!