Using a circular progress bar with a masked image?

I am working on a circular progress indicator in the presentation of achievements in the game center, and I have a β€œwall strike”. I have been struggling with this for more than two hours and still cannot get it to work.

The fact is that I need a circular progress bar, where I could install (at least) the image of the track (fill out). Since my progress is being filled with a rainbow gradient, and I cannot achieve the same results using only code.

I fiddled with CALayers and the drawRect method, but I failed. Could you give me a little guidance?

Here are examples of circular progress indicators: https://github.com/donnellyk/KDGoalBar https://github.com/danielamitay/DACircularProgress

I need the fill to be a masked image, depending on the progress. If you could even make it work, that progress would be animated, that would be great, but I don't need help with this :)

Thanks Nick

+6
source share
2 answers

You just need to build a path that defines the area to be filled (for example, using CGPathAddArc ), copy the graphics context to this path using CGContextClip , and then just draw your image.

Here is an example drawRect: method that you can use in a custom view:

 - (void)drawRect:(CGRect)rect { CGContextRef ctx = UIGraphicsGetCurrentContext(); CGFloat progress = 0.7f; //This would be a property of your view CGFloat innerRadiusRatio = 0.5f; //Adjust as needed //Construct the path: CGMutablePathRef path = CGPathCreateMutable(); CGFloat startAngle = -M_PI_2; CGFloat endAngle = -M_PI_2 + MIN(1.0f, progress) * M_PI * 2; CGFloat outerRadius = CGRectGetWidth(self.bounds) * 0.5f - 1.0f; CGFloat innerRadius = outerRadius * innerRadiusRatio; CGPoint center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)); CGPathAddArc(path, NULL, center.x, center.y, innerRadius, startAngle, endAngle, false); CGPathAddArc(path, NULL, center.x, center.y, outerRadius, endAngle, startAngle, true); CGPathCloseSubpath(path); CGContextAddPath(ctx, path); CGPathRelease(path); //Draw the image, clipped to the path: CGContextSaveGState(ctx); CGContextClip(ctx); CGContextDrawImage(ctx, self.bounds, [[UIImage imageNamed:@"RadialProgressFill"] CGImage]); CGContextRestoreGState(ctx); } 

To make it simple, I hard-coded a few things - you obviously need to add a property for progress and call setNeedsDisplay in the installer. This also assumes that you have an image named RadialProgressFill in your project.

Here is an example of what it would look like:

Screen shot

Hope you have a more attractive background image .;)

+23
source

The solution omz suggested worked lags when I tried to revitalize the property, so I continued to search. I found a great solution - to use the Quartz Core framework and a shell called CADisplayLink. β€œThis class is specifically designed for animations, whereβ€œ your data is likely to change in every frame. ”It tries to send a message to the target every time something is redrawn on the screen.β€œ Source

And a library that works that way.

Edit: But there is a more efficient solution. For animating a mask that applies to the image layer. I do not know why I did not do this from the very beginning. CABasicAnimation is faster than any custom drawing. Here is my implementation:

 class ProgressBar: UIView { var imageView:UIImageView! var maskLayer: CAShapeLayer! var currentValue: CGFloat = 1 override init(frame: CGRect) { super.init(frame: frame) updateUI() } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) updateUI() } func updateUI(){ makeGradient() setUpMask() } func animateCircle(strokeEnd: CGFloat) { let oldStrokeEnd = maskLayer.strokeEnd maskLayer.strokeEnd = strokeEnd //override strokeEnd implicit animation let animation = CABasicAnimation(keyPath: "strokeEnd") animation.duration = 0.5 animation.fromValue = oldStrokeEnd animation.toValue = strokeEnd animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) maskLayer.add(animation, forKey: "animateCircle") currentValue = strokeEnd } func setUpMask(){ let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: -CGFloat.pi/2, endAngle: CGFloat.pi*1.5, clockwise: true) maskLayer = CAShapeLayer() maskLayer.path = circlePath.cgPath maskLayer.fillColor = UIColor.clear.cgColor maskLayer.strokeColor = UIColor.red.cgColor maskLayer.lineWidth = 8.0; maskLayer.strokeEnd = currentValue maskLayer.lineCap = "round" imageView.layer.mask = maskLayer } func makeGradient(){ imageView = UIImageView(image: UIImage(named: "gradientImage")) imageView.frame = bounds imageView.contentMode = .scaleAspectFill addSubview(imageView) } } 

By the way, if you want to improve the animation, I suggest the excellent book "IOS Core Animation: Advanced Techniques Nick Lockwood Book"

0
source

Source: https://habr.com/ru/post/944770/


All Articles