I have a UIImageView displaying an image. I want to "highlight" a portion of the image by drawing a rounded rectangle outline. I would like to have the outline drawn with a thick, dashed line that "animates" by continually varying where the "beginning" of the line starts.
I thought about drawing a circle that had the look I want and then simply animating it, but I really need a rectangular solution, so that's out.
Background:
I'm drawing the rounded rectangle border by calculating 8 points and drawing 4 straight lines and 4 curves. (Maybe this can be easier, but it's not the broken part!)
My thinking is that I'll use an "offset" variable that starts at the top-left of the rounded rectangle, where the top-left curve meets the top straight piece. Then, I will increment this "offset" across the top of the rounded rectangle until it reaches the top-right curve, whereupon I will "reset" the "offset" variable to its original value.
This is working pretty much as I'd like, until the "reset" occurs. At this point, the animation is jerky (kind of expected), but it also appears to travel in reverse for a small portion of the time, before resuming "forward" motion. Finally, at the beginning/end of my dashed line, I get an extra long segment on the dashed line. I know they can't all be equal-length (can they? how to calculate?), but how can I make 2 shorter segments rather than 1 longer segment?
Anybody have an idea of what I can do to get a smooth "marching ants" kind of look? Any other ideas on a good way to (using animation) call the user's eye to a particular area of the screen? (It needs to surround a particular area without obscuring it.)
Current code:
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextClearRect(context, rect);
// Rounded corner will be 10% of average side length (i.e., (w + h) / 2)
float averageSide = ([self HighlightRect].size.width + [self HighlightRect].size.height) / 2.0;
float roundSize = averageSide * 0.10;
// offset is a static, class variable
offset += roundSize / 4.0;
if ([WhereIAmView offset] < roundSize) {
offset = roundSize;
}
if ([WhereIAmView offset] > ([self HighlightRect].size.width - roundSize)) {
offset = roundSize;
}
// Set the "main" color of the rounded rectangle
UIColor *lineColor = [UIColor colorWithRed:027.0/255.0 green:050.0/255.0 blue:224.0/255.0 alpha:1.0];
CGContextSetStrokeColorWithColor(context, [lineColor CGColor]);
CGContextSetLineWidth(context, 16.0);
CGFloat pattern[] = {25.0, 5.0};
CGContextSetLineDash(context, offset, pattern, 2);
CGRect rRect = [self HighlightRect];
// The top left corner
CGPoint topLeft = CGPointMake(rRect.origin.x, rRect.origin.y);
// The top right corner
CGPoint topRight = CGPointMake(rRect.origin.x + rRect.size.width, rRect.origin.y);
// The bottom right corner
CGPoint bottomRight = CGPointMake(rRect.origin.x + rRect.size.width, rRect.origin.y + rRect.size.height);
// The bottom left corner
CGPoint bottomLeft = CGPointMake(rRect.origin.x, rRect.origin.y + rRect.size.height);
// The two points across the top of the rounded rectangle (left to right)
CGPoint point1 = CGPointMake(rRect.origin.x + roundSize, rRect.origin.y);
CGPoint point2 = CGPointMake(rRect.origin.x + rRect.size.width - roundSize, rRect.origin.y);
// The two points along the right of the rounded rectangle (top to bottom)
CGPoint point3 = CGPointMake(rRect.origin.x + rRect.size.width, rRect.origin.y + roundSize);
CGPoint point4 = CGPointMake(rRect.origin.x + rRect.size.width, rRect.origin.y + rRect.size.height - roundSize);
// The two points along the bottom of the rounded rectangle (right to left)
CGPoint point5 = CGPointMake(rRect.origin.x + rRect.size.width - roundSize, rRect.origin.y + rRect.size.height);
CGPoint point6 = CGPointMake(rRect.origin.x + roundSize, rRect.origin.y + rRect.size.height);
// The two points along the left of the rounded rectangle (bottom to top)
CGPoint point7 = CGPointMake(rRect.origin.x, rRect.origin.y + rRect.size.height - roundSize);
CGPoint point8 = CGPointMake(rRect.origin.x, rRect.origin.y + roundSize);
// Move to point 1
CGContextMoveToPoint(context, point1.x, point1.y);
// Add line to point 2 (this is the straight portion across the top)
CGContextAddLineToPoint(context, point2.x, point2.y);
// Add curve to point 3 (this is the rounded portion in top right)
CGContextAddArcToPoint(context, topRight.x, topRight.y, point3.x, point3.y, roundSize);
// Add line to point 4 (this is the straight portion across the right)
CGContextAddLineToPoint(context, point4.x, point4.y);
// Add curve to point 5 (this is the rounded portion in bottom right)
CGContextAddArcToPoint(context, bottomRight.x, bottomRight.y, point5.x, point5.y, roundSize);
// Add line to point 6 (this is the straight portion across the bottom)
CGContextAddLineToPoint(context, point6.x, point6.y);
// Add curve to point 7 (this is the rounded portion in bottom left)
CGContextAddArcToPoint(context, bottomLeft.x, bottomLeft.y, point7.x, point7.y, roundSize);
// Add line to point 8 (this is the straight portion across the left)
CGContextAddLineToPoint(context, point8.x, point8.y);
// Add curve to point 1 (this is the rounded portion in top left)
CGContextAddArcToPoint(context, topLeft.x, topLeft.y, point1.x, point1.y, roundSize);
// Stroke the path
CGContextStrokePath(context);
}
bump
bump bump
Try Using CAShapeLayer with CGPath of your shape.
Rounded rectangle path can be constructed using Uibezierpath convenience method.
You can set a line pattern for the shape layer. Animating the line property of the shape layer would give the "marching ants like effect".
shapeLayer = [CAShapeLayer layer];
CGRect shapeRect = CGRectMake(0.0f, 0.0f, 200.0f, 100.0f);
[shapeLayer setBounds:shapeRect];
[shapeLayer setPosition:CGPointMake(160.0f, 140.0f)];
[shapeLayer setFillColor:[[UIColor clearColor] CGColor]];
[shapeLayer setStrokeColor:[[UIColor blackColor] CGColor]];
[shapeLayer setLineWidth:1.0f];
[shapeLayer setLineJoin:kCALineJoinRound];
[shapeLayer setLineDashPattern:
[NSArray arrayWithObjects:[NSNumber numberWithInt:10],
[NSNumber numberWithInt:5],
nil]];
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:shapeRect cornerRadius:15.0];
[shapeLayer setPath:path.CGPath];
[[imageview layer] addSublayer:shapeLayer];
The animation function can be,
- (void)toggleMarching
{
if ([shapeLayer animationForKey:#"linePhase"])
[shapeLayer removeAnimationForKey:#"linePhase"];
else {
CABasicAnimation *dashAnimation;
dashAnimation = [CABasicAnimation
animationWithKeyPath:#"lineDashPhase"];
[dashAnimation setFromValue:[NSNumber numberWithFloat:0.0f]];
[dashAnimation setToValue:[NSNumber numberWithFloat:15.0f]];
[dashAnimation setDuration:0.75f];
[dashAnimation setRepeatCount:10000];
[shapeLayer addAnimation:dashAnimation forKey:#"linePhase"];
}
}
Related
Is there a way to draw a UIView circle with a dotted line border? I want to have control over the spacing between the dots, and the size of the dots. I tried specifying my own pattern image, but when I make it into a circle it doesn't look good:
UIView *mainCircle = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
[mainCircle.layer setCornerRadius:100];
[mainCircle.layer setBorderWidth:5.0];
[mainCircle.layer setBorderColor:[[UIColor colorWithPatternImage:[UIImage imageNamed:#"dotted"]] CGColor]];
[self.view addSubview:mainCircle];
[mainCircle setCenter:self.view.center];
Following on from aksh1t's answer and rob's answer, you should use a round line cap, along with a dash pattern to do this.
The only thing I would add is that with the current code, you can end up with results like this:
Notice how at the top, you get an overlap of the dots. This is due to the fact that the circumference of the circle isn't entirely divisible by the number of dots.
You can fix this relatively easily by doing a simple bit of maths before. I wrote a few lines of code that'll allows you to provide an dot diameter value, along with an expected dot spacing - and it will try and approximate the nearest dot spacing that will result in an integral number of dots.
Also, I recommend you take an 100% layered approach, using CAShapeLayer to draw your circle. That way you can easily add animations to it without having to completely re-draw it for each frame.
Something like this should do the trick:
// your dot diameter.
CGFloat dotDiameter = 10.0;
// your 'expected' dot spacing. we'll try to get as closer value to this as possible.
CGFloat expDotSpacing = 20.0;
// the size of your view
CGSize s = self.view.frame.size;
// the radius of your circle, half the width or height (whichever is smaller) with the dot radius subtracted to account for stroking
CGFloat radius = (s.width < s.height) ? s.width*0.5-dotDiameter*0.5 : s.height*0.5-dotDiameter*0.5;
// the circumference of your circle
CGFloat circum = M_PI*radius*2.0;
// the number of dots to draw as given by the circumference divided by the diameter of the dot plus the expected dot spacing.
NSUInteger numberOfDots = round(circum/(dotDiameter+expDotSpacing));
// the calculated dot spacing, as given by the circumference divided by the number of dots, minus the dot diameter.
CGFloat dotSpacing = (circum/numberOfDots)-dotDiameter;
// your shape layer
CAShapeLayer* l = [CAShapeLayer layer];
l.frame = (CGRect){0, 0, s.width, s.height};
// set to the diameter of each dot
l.lineWidth = dotDiameter;
// your stroke color
l.strokeColor = [UIColor blackColor].CGColor;
// the circle path - given the center of the layer as the center and starting at the top of the arc.
UIBezierPath* p = [UIBezierPath bezierPathWithArcCenter:(CGPoint){s.width*0.5, s.height*0.5} radius:radius startAngle:-M_PI*0.5 endAngle:M_PI*1.5 clockwise:YES];
l.path = p.CGPath;
// prevent that layer from filling the area that the path occupies
l.fillColor = [UIColor clearColor].CGColor;
// round shape for your stroke
l.lineCap = kCALineCapRound;
// 0 length for the filled segment (radius calculated from the line width), dot diameter plus the dot spacing for the un-filled section
l.lineDashPattern = #[#(0), #(dotSpacing+dotDiameter)];
[self.view.layer addSublayer:l];
You'll now get the following output:
If you want to use this in a UIView, I would suggest subclassing it and adding the CAShapeLayer as a sublayer. You'll also want to add a masking layer in order to mask the view's contents to inside the border.
I have added an example of this in the full project below.
Full Project: https://github.com/hamishknight/Dotted-Circle-View
The best way to do what you are trying would be to draw a circle UIBezierPath, and set the path to a dotted style. The dotted style path code was taken from this answer.
UIBezierPath * path = [[UIBezierPath alloc] init];
[path addArcWithCenter:center radius:50 startAngle:0 endAngle:2 * M_PI clockwise:YES];
[path setLineWidth:8.0];
CGFloat dashes[] = { path.lineWidth, path.lineWidth * 2 };
[path setLineDash:dashes count:2 phase:0];
[path setLineCapStyle:kCGLineCapRound];
// After you have the path itself, you can either make
// an image and set it in a view or use the path directly
// in the layer of the view you want to.
// This is the code for the image option.
UIGraphicsBeginImageContextWithOptions(CGSizeMake(300, 20), false, 2);
[path stroke];
UIImage * image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
I'm new to core graphics and I'm struggling with a simple task of putting a sweeping circle inside a square. The outcome I got looks like this:
The circle won't appear at the center of the square, and the size of the circle appears much smaller than I specified.
Below is my drawRect method for drawing the circle. I have put the printed-out variable values while debugging in the comments for your convenience. I also printed out the value passed to initWithFrame: frame=(0 0; 256 256). The frame is the orange square you see in the picture.
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGFloat midX = CGRectGetMidX(self.bounds); // bounds = (0 0; 256 256); midX = 128
CGFloat midY = CGRectGetMidY(self.bounds); // midY = 128
CGFloat radius = midY - 4; // radius = 124
// Outer grey pie
[endColor setFill];
CGContextMoveToPoint(context, midX, midY); // move to center
CGContextAddEllipseInRect(context, CGRectMake(midX - radius, midY - radius, radius * 2, radius * 2)); // adds a circle of radius = square_side_length - 4
CGContextFillPath(context); // fill the circle above with grey
// Show the clock
NSTimeInterval seconds = [[NSDate date] timeIntervalSince1970];
CGFloat mod = fmod(seconds, self.period);
CGFloat percent = mod / self.period;
[fillColor setFill];
CGFloat start = -M_PI_2;
CGFloat end = 2 * M_PI;
CGFloat sweep = end * percent + start;
CGContextMoveToPoint(context, midX, midY);
CGContextAddArc(context, midX, midY, radius, start, sweep, 0); // radius = square_side_length - 24
CGContextFillPath(context);
// Innermost white pie
radius -= 50; // radius = square_side_length - 54
[bgColor setFill]; // white
CGContextMoveToPoint(context, midX, midY);
CGContextAddEllipseInRect(context, CGRectMake(midX - radius, midY - radius, radius * 2, radius * 2));
CGContextFillPath(context);
}
And below is the code that adds the clock to its superview:
clock = [[ProgressClock alloc] initWithFrame:self.clockHolder.bounds // bounds=[0 0; 256 256]
period:[TOTPGenerator defaultPeriod]
bgColor:[UIColor whiteColor]
strokeColor:[UIColor colorWithWhite:0 alpha:0.2]
fillColor:[UIColor blueColor]
endColor:[UIColor grayColor]
shade:NO];
[self.clockHolder addSubview:clock];
Can anyone spot the mistake I made? Thanks in advance.
Thanks a lot to #originaluser2's comment, I have fixed this issue simply by moving the clock presenting logic from viewDidLoad to viewDidAppear and the clock showed up perfectly. There was nothing wrong with the drawing code I posted; however the auto-layout initialization and the animation of my clock happened in a sequence that gave my drawing canvas a wrong frame. By putting the drawing logic in viewDidAppear, we are guaranteed that all the auto-layout setup has been completed, thus frames are fixed, before continue onto drawing the circle.
I am trying to draw an annotation for map , my view is a subclass of MKAnnotationView
I need a shape something like shown below
What I am getting is like this :
Here is the code which I am using :
- (void)drawRect:(CGRect)rect
{
CGContextRef ctx= UIGraphicsGetCurrentContext();
UIGraphicsPushContext(ctx);
CGRect bounds = [self bounds];
CGPoint topLeft = CGPointMake(CGRectGetMinX(rect), CGRectGetMinY(rect));
CGPoint topRight = CGPointMake(CGRectGetMaxX(rect), CGRectGetMinY(rect));
CGPoint midBottom = CGPointMake(CGRectGetMidX(rect), CGRectGetMaxY(rect));
CGFloat height = bounds.size.height;
CGFloat width = bounds.size.width;
//draw semi circle
CGContextBeginPath(ctx);
CGContextAddArc(ctx, width/2, height/2, width/2, 0 ,M_PI, YES);
//draw bottom cone
CGContextAddLineToPoint(ctx, midBottom.x, midBottom.y);
CGContextAddLineToPoint(ctx, topRight.x, topRight.y + height/2); // mid right
CGContextClosePath(ctx);
CGContextSetFillColorWithColor(ctx, [UIColor redColor].CGColor);
CGContextFillPath(ctx);
UIGraphicsPopContext();
}
You can achieve the desired effect if you replace your lines with quad curves:
- (void)drawRect:(CGRect)rect
{
CGContextRef ctx= UIGraphicsGetCurrentContext();
UIGraphicsPushContext(ctx);
CGRect bounds = [self bounds];
CGPoint topLeft = CGPointMake(CGRectGetMinX(rect), CGRectGetMinY(rect));
CGPoint topRight = CGPointMake(CGRectGetMaxX(rect), CGRectGetMinY(rect));
CGPoint midBottom = CGPointMake(CGRectGetMidX(rect), CGRectGetMaxY(rect));
CGFloat height = bounds.size.height;
CGFloat width = bounds.size.width;
//draw semi circle
CGContextBeginPath(ctx);
CGContextAddArc(ctx, width/2, height/2, width/2, 0 ,M_PI, YES);
//draw bottom cone
CGContextAddQuadCurveToPoint(ctx, topLeft.x, height * 2 / 3, midBottom.x, midBottom.y);
CGContextAddQuadCurveToPoint(ctx, topRight.x, height * 2 / 3, topRight.x, topRight.y + height/2);
// CGContextAddLineToPoint(ctx, midBottom.x, midBottom.y);
// CGContextAddLineToPoint(ctx, topRight.x, topRight.y + height/2); // mid right
CGContextClosePath(ctx);
CGContextSetFillColorWithColor(ctx, [UIColor redColor].CGColor);
CGContextFillPath(ctx);
UIGraphicsPopContext();
}
This employs a quadratic bezier curve, which is a good curve when you don't want inflection points. You can achieve a similar curve with the cubic bezier, but if you're not careful with your control points, you can get undesired inflection points. The quadratic curve is just a little easier with only one control point per curve.
By choosing control points with x values the same as the start and end of the semicircle, it ensures a smooth transition from the circle to the curve leading down to the point. By choosing y values for those control points which are relatively close to the start and end of the semicircle, it ensures that the curve will transition quickly from the semicircle to the point. You can adjust these control points, but hopefully this illustrates the idea.
For illustration of the difference between these two types of curves, see the Curves section of the Paths chapter of the Quartz 2D Programming Guide.
I'm trying to create a "mini-map." It's a circle filled with markers, and I want the border of the circle to glow in places to indicate to the user to indicate that more markers are beyond the minimap in a given direction.
I can give the circle a 'glowing' blue border by drawing, below it, a blue circle with a slightly larger radius. I think that I can make this blue border brighter in some places than others by giving its CALayer a mask. (I've tried giving it a gradient mask, and it works.)
Assuming that I can make the proper calculations to determine how bright a given pixel should be (given the position of markers beyond the minimap's viewport), how do I set the individual pixels of a CALayer? Or is there an easier way to accomplish what I'm looking for besides making a complicated alpha value calculation for each pixel in the circle?
Thanks.
Here's my solution. I drew a series of 1-pixel arcs, each with a different stroke color.
void AddGlowArc(CGContextRef context, CGFloat x, CGFloat y, CGFloat radius, CGFloat peakAngle, CGFloat sideAngle, CGColorRef colorRef){
CGFloat increment = .05;
for (CGFloat angle = peakAngle - sideAngle; angle < peakAngle + sideAngle; angle+=increment){
CGFloat alpha = (sideAngle - fabs(angle - peakAngle)) / sideAngle;
CGColorRef newColor = CGColorCreateCopyWithAlpha(colorRef, alpha);
CGContextSetStrokeColorWithColor(context, newColor);
CGContextAddArc(context, x, y, radius, angle, angle + increment, 0);
CGContextStrokePath(context);
}
}
And then, in DrawRect,
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 2.0);
AddGlowArc(context, 160, 160, 160, angle, .2, [UIColor colorWithRed:0 green:.76 blue:.87 alpha:1].CGColor);
This was my [much longer] solution which would allow each glow-point layer to be added and animated individually if needed. The small circle is added on the circumference of a larger one and that larger circle is rotated. Enjoyed getting my head around this even though you answered the question
- (void)viewDidLoad
{
[super viewDidLoad];
CGPoint circleCenter = CGPointMake(300.f, 300.f);
CALayer *outerCircle = [self outerCircleToRotateWithCenter:circleCenter andRadius:100.f];
[self.view.layer addSublayer:outerCircle];
CGFloat rotationAngleInDeg = 270.f;
CGFloat rotationAngle = (M_PI * -rotationAngleInDeg)/180.f;
CATransform3D transform = CATransform3DIdentity;
transform = CATransform3DRotate(transform, rotationAngle, 0.f, 0.f, -1.f);
[outerCircle setTransform:transform];
}
Using method:
- (CALayer *)outerCircleToRotateWithCenter:(CGPoint)circleCenter andRadius:(CGFloat )radius {
// outer circle
CAShapeLayer *container = [CAShapeLayer layer];
UIBezierPath *containerCircle = [UIBezierPath
bezierPathWithOvalInRect:CGRectMake(0.f, 0.f, radius, radius)];
[container setPath:containerCircle.CGPath];
[container setBounds:CGPathGetBoundingBox(containerCircle.CGPath)];
[container setPosition:circleCenter];
[container setAnchorPoint:CGPointMake(0.5f, 0.5f)];
[container setStrokeColor:[UIColor blackColor].CGColor]; // REMOVE
[container setFillColor:nil];
// smaller circle
CAShapeLayer *circle = [[CAShapeLayer alloc] init];
[container addSublayer:circle];
UIBezierPath *circlePath = [UIBezierPath
bezierPathWithOvalInRect:CGRectMake(0.f, 0.f, 25.f, 25.f)];
[circle setPath:circlePath.CGPath];
[circle setBounds:CGPathGetBoundingBox(circlePath.CGPath)];
[circle setFillColor:[UIColor orangeColor].CGColor];
[circle setAnchorPoint:CGPointMake(0.5f, 0.5f)];
[circle setPosition:CGPointMake(radius/2.f, 0.f)];
[circle setOpacity:0.4f];
// shadow
[circle setShadowOpacity:0.8f];
[circle setShadowRadius:4.f];
[circle setShadowOffset:CGSizeMake(0.f, 0.f)];
[circle setShadowColor:[UIColor orangeColor].CGColor];
[circle setShadowPath:circlePath.CGPath];
return container;
}
I'm guessing the smaller circle could be added and rotated in a single transform rather then being a sublayer.
I want to draw a very thin hairline width of a line in my UIView's drawRect method. The line I see that has a value 0.5 for CGContextSetLineWidth doesn't match the same 1.0 width value that is used to draw a border CALayer.
You can see the difference between the two - the red line (width = 1) is a lot thinner than the purple/blue line (width = 0.5).
Here's how I am drawing my pseudo 1.0 width horizontal line:
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(ctx, [UIColor blueColor].CGColor);
CGContextSetLineWidth(ctx, 0.5); // I expected a very thin line
CGContextMoveToPoint(ctx, 0, y);
CGContextAddLineToPoint(ctx, self.bounds.size.width, y);
CGContextStrokePath(ctx);
Here's a border for the same view, this time using 1.0 border width:
UIView *myView = (UIView *)self;
CALayer *layer = myView.layer;
layer.borderColor = [UIColor redColor].CGColor;
layer.borderWidth = 1.0;
What do I need to do differently to draw my own custom line that's the same width as the CALayer version?
When you stroke a path, the stroke straddles the path. To say it another way, the path lies along the center of the stroke.
If the path runs along the edge between two pixels, then the stroke will (partially) cover the pixels on both sides of that edge. With a line width of 0.5, a horizontal stroke will extend 0.25 points into the pixel above the path, and 0.25 points into the pixel below the path.
You need to move your path so it doesn't run along the edge of the pixels:
CGFloat lineWidth = 0.5f;
CGContextSetLineWidth(ctx, lineWidth);
// Move the path down by half of the line width so it doesn't straddle pixels.
CGContextMoveToPoint(ctx, 0, y + lineWidth * 0.5f);
CGContextAddLineToPoint(ctx, self.bounds.size.width, y + lineWidth * 0.5f);
But since you're just drawing a horizontal line, it's simpler to use CGContextFillRect:
CGContextSetFillColorWithColor(ctx, [UIColor blueColor].CGColor);
CGContextFillRect(ctx, CGRectMake(0, y, self.bounds.size.width, 0.5f));
You need to turn off antialiasing to get a thin line when not drawn on an integral.
CGContextSetShouldAntialias(ctx, NO)