Animation when drawing with CGContext - ios

My question is how to animate the drawing process when drawing with CGContextRef. Is it possible? Assuming it is, how?
I have two code snippets that I would like to animate. First one draws a progress bar and the second one draws a simple line chart. The drawing is done inside a subclass of UIView.
Progress bar is nice and easy. But I want it to sort of draw itself out from the left. I am pretty sure that this will require using something other than UIRectFill but I dont know how to accomplish it.
- (void)drawProgressLine
{
[bgColor set];
UIRectFill(self.bounds);
[graphColor set];
UIRectFill(CGRectMake(0, 0, self.frame.size.width / 100 * [[items objectAtIndex:0] floatValue], self.frame.size.height));
}
The line chart is a bit more complex. I would really really like it to start drawing itself from the left line by line slowly completing itself towards the right but if that is too much how can I just slowly fade it in? The code:
- (void)drawLineChart
{
[bgColor set];
UIRectFill(self.bounds);
[graphColor set];
if (items.count < 2) return;
CGRect bounds = CGRectMake(0, 50, self.bounds.size.width, self.bounds.size.height - 100);
float max = -1;
for (GraphItem *item in items)
if (item.value > max)
max = item.value;
float xStep = (self.frame.size.width) / (items.count - 1);
for (int i = 0; i < items.count; i++)
{
if (i == items.count - 1) break;
float itemHeight = bounds.origin.y + bounds.size.height - ((GraphItem*)[items objectAtIndex:i]).value / max * bounds.size.height;
float nextItemHeight = bounds.origin.y + bounds.size.height - ((GraphItem*)[items objectAtIndex:i + 1]).value / max * bounds.size.height;
CGPoint start = CGPointMake(xStep * i, itemHeight);
CGPoint stop = CGPointMake(xStep * (i + 1), nextItemHeight);
[self drawLineFromPoint:start toPoint:stop lineWidth:1 color:graphColor shadow:YES];
}
}
Pretty simple I guess. If important the drawLineFromPoint..... is implemented like:
- (void)drawLineFromPoint:(CGPoint)startPoint toPoint:(CGPoint)endPoint lineWidth:(CGFloat)width color:(UIColor *)color shadow:(BOOL)shadow
{
if (shadow)
{
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGFloat components[4] = {0.0, 0.0, 0.0, 1.0};
CGColorRef shadowColor = CGColorCreate(colorSpace, components);
CGContextSetShadowWithColor(UIGraphicsGetCurrentContext(), CGSizeMake(1,1), 2.0, shadowColor);
}
CGContextBeginPath(context);
CGContextSetLineWidth(context, width);
CGContextMoveToPoint(context, startPoint.x, startPoint.y);
CGContextAddLineToPoint(context, endPoint.x, endPoint.y);
CGContextClosePath(context);
[color setStroke];
CGContextStrokePath(context);
CGContextSetShadowWithColor(context, CGSizeZero, 0, NULL);
}
I hope I made myself clear cause its 1 am in my country and this post is the last thing that stands between me and my bed. Cheers, Jan.

It sounds like you don't understand the UIKit view drawing cycle. Do you understand that each time you want to change the appearance of your custom-drawn view, you need to send it setNeedsDisplay? And then you need to redraw it entirely in your drawRect: method? Your drawing doesn't appear on screen until drawRect: returns, and after that you cannot draw more in that view until it receives another drawRect: message. If you want the contents of the view to be animated, you will need to send setNeedsDisplay to the view periodically (say, every 1/30th or 1/60th of a second, using either an NSTimer or a CADisplayLink).

It seems like you got the progress bar handled, so here is what I suggest for the graph drawing. Just create and debug your code once to draw the entire graph. Then, use a clip rect that you animate the width of, so that the clip rect starts out skinny and then extends in width until the whole graph becomes visible (from left to right). That will give the user the idea that whatever lines you have are "drawing" from the left to the right, but the actual code is very simple as the animation steps just modify the clip rect to make it wider for each "step". See this question for more info on the CoreGraphics calls: How to set up a clipping rectangle or area

Related

CGContextMoveToPoint moves to wrong position

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.

Performance issue while trying to use gradient for realtime plot

I need to build a realtime plot sin(x)
So I update it 25 times per second. I try to use gradient for the plot line to make it transparent in the end:
Everything looks good - but the performance is critically low.. When I not creating gradient - everything works good.
The needed plot looks like this:
Is it possible to improve my code to solve the performance issue?
CGContextSaveGState(context);
CGFloat value;
UIBezierPath *graph = [UIBezierPath new];
for (NSUInteger counter = 0; counter < historyArray.count; counter++) {
value = [historyArray[counter] floatValue];
if (counter == 0) {
[graph moveToPoint:CGPointMake((CGFloat) (bounds.origin.x + bounds.size.width * 0.75 / 40), (CGFloat) (bounds.origin.y + bounds.size.height / 2 - value * bounds.size.height / 2.1))];
} else {
[graph addLineToPoint:CGPointMake((CGFloat) (bounds.origin.x + (float) counter / (float) (40 - 1) * bounds.size.width * 0.75), (CGFloat) (bounds.origin.y + bounds.size.height / 2 - value * bounds.size.height / 2.1))];
}
}
if (historyArray.count > 0) {
CGFloat colors[] = {
0.0, 1.0, 1.0, 0.0,
0.0, 1.0, 1.0, 1.0,
0.0, 1.0, 1.0, 1.0
};
CGColorSpaceRef baseSpace = CGColorSpaceCreateDeviceRGB();
CGGradientRef gradient = CGGradientCreateWithColorComponents(baseSpace, colors, NULL, 3);
CGColorSpaceRelease(baseSpace), baseSpace = NULL;
CGContextSetLineWidth(context, 2.f);
CGContextSetLineJoin(context, kCGLineJoinRound);
CGContextSetLineCap(context, kCGLineCapRound);
CGContextAddPath(context, graph.CGPath);
CGContextReplacePathWithStrokedPath(context);
CGContextClip(context);
// Define the start and end points for the gradient
// This determines the direction in which the gradient is drawn
CGPoint startPoint = bounds.origin;
CGPoint endPoint = CGPointMake(bounds.origin.x + bounds.size.width, bounds.origin.y);
CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0);
CGGradientRelease(gradient), gradient = NULL;
CGContextRestoreGState(context);
}
Creating the gradient isn't likely to be the performance problem. It's more likely clipping it to a complex path. It's not totally clear from your code how many line segments your path has, but I suspect it's in the hundreds, right?
As for more-performant ways to do this:
Reduce the number of points in the path - if you're plotting points every X pixels horizontally, then you're plotting more than you need when the curve isn't changing much (near the peaks and troughs).
Do away with the gradient altogether, and just draw your path with multiple differently-colored line segments. This might actually end up looking better, anyway, since you can adjust the transparency by distance traveled, rather than by horizontal coordinate.
If you really do want the horizontal gradient, try applying it to subsets of the path, which might be faster than doing it all at once.

Memory Management of multiple simple UIViews

Background:
I'm making an app that is grid-based using ARC. Basically there is a 4x4-8x8 grid in the center of the screen (that takes up most of the screen). This grid is constructed using a single UIView that is tinted some color and lines drawn with drawRect: (I'll be posting all of the relevant code below for reference).
Each of the cells is contained inside an NSMutableArray for each row that is contained inside another NSMutableArray of the rows:
Array (Rows)
Array (Cols)
Cell Contents
In each of these cells, I either have an actor object or a placeholder object. The placeholder object is essentially just a blank NSObject while the actor object has 8 primitive properties and 1 object property.
For instance, one of the actors is a source, which essentially recursively draws a plain UIView from the source across the grid until it hits another actor or a wall of the grid.
The blue and red lines show different UIViews as they are currently running. With a grid this small, memory doesn't seem to be an issue often; however, when the full game runs with an 8x8 grid, there can feasibly be 50+ drawn UIViews on the screen in addition to the UIImageViews that function as the sources, movables, etc. as well as the other UILabels and buttons that are not included in the grid. There can easily be over 100 UIViews on the screen at once, which, even on the latest devices with the best hardware, causes some pretty bad lag.
I have a feeling that this has to do with the fact that I am rendering 100+ views to the screen at once.
Question:
Can I incorporate all of these dynamically drawn lines into one view, or is there a better solution entirely?
drawRect:
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
CGContextRef context = UIGraphicsGetCurrentContext();
CGColorRef color = [[self backgroundColor] CGColor];
int numComponents = CGColorGetNumberOfComponents(color);
CGFloat red = 0, green = 0, blue = 0;
if (numComponents == 4)
{
const CGFloat *components = CGColorGetComponents(color);
red = components[0];
green = components[1];
blue = components[2];
}
CGContextSetRGBFillColor(context, red, green, blue, 0.5);
float top;
float cell = [self cellWidth];
float grid = [self gridWidth];
//Draw
for(int i = 0; i < [self size]+1; i++)
{
top = i*(cell+lineWidth);
CGContextFillRect(context, CGRectMake(0, top, grid, lineWidth));
CGContextFillRect(context, CGRectMake(top, 0, lineWidth, grid));
}
}
addSources:
- (void)addSources:(NSArray*)sources
{
for(int i = 0; i < [sources count]; i++)
{
NSArray* src = [sources objectAtIndex:i];
int row = [[src objectAtIndex:1] intValue];
int column = [[src objectAtIndex:0] intValue];
int direction = [[src objectAtIndex:2] intValue];
int color = [UIColor colorKeyForString:[src objectAtIndex:3]];
float width = [self cellWidth]*scaleActors;
float x = lineWidth + (([self cellWidth]+lineWidth) * (column-1)) + (([self cellWidth]-width)/2.0);
float y = lineWidth + (([self cellWidth]+lineWidth) * (row-1)) + (([self cellWidth]-width)/2.0);
ActorView* actor = [[ActorView alloc] initWithFrame:CGRectMake(x, y, width, width)];
[actor setType:4];
[actor setDirection:direction];
[actor setColorKey:color];
[actor setIsGlowing:YES];
[actor setPicture];
if([self isCreatingLevel])
[actor setCanRotate:YES];
[self addSubview:actor];
[[[self rows] objectAtIndex:(row-1)] replaceObjectAtIndex:(column-1) withObject:actor];
}
}
Edit: Time Profiler Results
By this point, I have roughly 48 drawn views on the screen, (about 70 views total).
I'd suggest WWDC 2012 video iOS App Performance: Responsiveness as a good primer in using Instruments to track down these sorts of issues. Lots of good techniques and tips in that video.
But I agree that this number of views doesn't seem outlandish (though I might be tempted to render this all in CoreGraphics). I'm not using your same model, but here is a pure Core Graphics rendering of that graphic with a single UIView subclass:
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
// configure the gridlines
CGContextSetStrokeColorWithColor(context, [[UIColor blackColor] CGColor]);
CGContextSetLineWidth(context, 8.0);
CGContextSetLineCap(context, kCGLineCapSquare);
// add the horizontal gridlines
for (NSInteger row = 0; row <= self.rows; row++)
{
CGPoint from = [self coordinateForX:0 Y:row];
CGPoint to = [self coordinateForX:_cols Y:row];
CGContextMoveToPoint(context, from.x, from.y);
CGContextAddLineToPoint(context, to.x, to.y);
}
// add the vertical gridlines
for (NSInteger col = 0; col <= self.cols; col++)
{
CGPoint from = [self coordinateForX:col Y:0 ];
CGPoint to = [self coordinateForX:col Y:_rows];
CGContextMoveToPoint(context, from.x, from.y);
CGContextAddLineToPoint(context, to.x, to.y);
}
// stroke the gridlines
CGContextStrokePath(context);
// now configure the red/blue line segments
CGContextSetLineWidth(context, self.bounds.size.width / _cols / 2.0);
CGContextSetLineCap(context, kCGLineCapRound);
// iterate through our array of points
CGPoint lastPoint = [self.points[0] CGPointValue];
for (NSInteger i = 1; i < [self.points count]; i++)
{
// set the color
if (i % 2)
CGContextSetStrokeColorWithColor(context, [[UIColor redColor] CGColor]);
else
CGContextSetStrokeColorWithColor(context, [[UIColor blueColor] CGColor]);
CGPoint nextPoint = [self.points[i] CGPointValue];
// create path
CGPoint from = [self coordinateForCenterX:lastPoint.x Y:lastPoint.y];
CGPoint to = [self coordinateForCenterX:nextPoint.x Y:nextPoint.y];
CGContextMoveToPoint(context, from.x, from.y);
CGContextAddLineToPoint(context, to.x, to.y);
// stroke it
CGContextStrokePath(context);
// save the last point
lastPoint = nextPoint;
}
}
Now, maybe you're doing something else that requires more sophisticated treatment, in which case that WWDC video (or, perhaps iOS App Performance: Graphics and Animations) should point you in the right direction.

Design Horizontal Line on UiView

anyone can 'tell me how you draw a horizontal line on the UIView by code or by other methods? I need this line to create a separator
Thank you all
The easiest way to do this is probably use to create a UIView with the height, width, and color you want your separator to have and make it a subview of the view in question. In other words, make a 1 or 2 point tall UIView and use it as your separator. Position it using constraints or manually setting its frame.
Alternatively, add an UILabel and set the text to ---------- or ______ or ======== or whatever looks good. You can adjust the thickness and style by setting the font or the color by setting the text color. Make sure you set the line breaks to "clip" (NSLineBreakByClipping) or you may end up with ellipsis (...) if you enter more characters than the label can show.
You can override the UIView's drawRect: method to draw a 1 pixel stroke at the bottom of the view to act as a separator.
- (void)drawRect:(CGRrect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGPoint startPoint = CGPointMake(rect.origin.x, rect.origin.y + rect.size.height - 1);
CGPoint endPoint = CGPointMake(rect.origin.x + rect.size.width - 1, rect.origin.y + rect.size.height - 1);
CGContextSaveState(context);
CGContextSetLineCap(context, kCGLineCapSquare);
CGContextSetStrokeColorWithColor(context, UIColor.CGColor);
CGContextSetLineWidth(context, 1.0); // Set the line width here
CGContextMoveToPoint(context, startPoint.x + 0.5, startPoint.y + 0.5);
CGContextAddLineToPoint(context, endPoint.x + 0.5, startPoint.y + 0.5);
CGContextStrokePath(context);
CGContextRestoreState(context);
}

Drawings in UIImage are being distorted

I have implemented a highlight function in my app. This highlight is being drawn in UIImage so that it can be saved as a PNG Representation. Everything is working perfectly but recently I realized somewhat a very confusing issue. Sometimes when I am highlighting, the drawings are being distorted. Here is what it looks like:
Whenever I move my finger to highlight the characters, the drawn highlights are stretching to the left. Another one:
In this one, every time I move my finger to highlight, the drawn highlights are moving upward!
This is all very confusing for me. This happens from time to time, and sometimes to certain pages only. Sometimes it works well just like this:
I am very confused on why is this happening. Can anyone tell me or at least give me an idea on why is this happening? Please help me.
THE CODE:
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
currPoint = [[touches anyObject]locationInView:self];
for (int r = 0; r < [rectangles count]; r++)
{
CGRect rect = [[rectangles objectAtIndex:r]CGRectValue];
//Get the edges of the rectangles
CGFloat xEdge = rect.origin.x + rect.size.width;
CGFloat yEdge = rect.origin.y + rect.size.height;
if ((currPoint.x > rect.origin.x && currPoint.x < xEdge) && (currPoint.y < rect.origin.y && currPoint.y > yEdge))
{
imgView.image = [self drawRectsToImage:imgView.image withRectOriginX:rect.origin.x originY:rect.origin.y rectWidth:rect.size.width rectHeight:rect.size.height];
break;
}
}
}
//The function that draws the highlight
- (UIImage *)drawRectsToImage:(UIImage *)image withRectOriginX:(CGFloat)x originY:(CGFloat)y rectWidth:(CGFloat)width rectHeight:(CGFloat)ht
{
UIGraphicsBeginImageContext(self.bounds.size);
CGContextRef context = UIGraphicsGetCurrentContext();
[image drawInRect:self.bounds];
CGContextSetStrokeColorWithColor(context, [UIColor clearColor].CGColor);
CGRect rect = CGRectMake(x, y, width, ht);
CGContextAddRect(context, rect);
CGContextSetCMYKFillColor(context, 0, 0, 1, 0, 0.5);
CGContextFillRect(context, rect);
UIImage *ret = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return ret;
}
I can't tell you exactly why these artifacts occur, but...
I don't think it's a good idea to render an image in a touch handler. Touch handling should do as little as possible.
You might want to try using CoreAnimation's CALayer:
Set your image as background image like this (assuming your class is a subclass of UIView) or use an actual UIImageView:
self.layer.contents = (id)image.CGImage;
When you detect that another rectangle rect has been touched, add the highlight as a sublayer above your image background:
CALayer *highlightLayer = [CALayer layer];
highlightLayer.frame = rect;
highlightLayer.backgroundColor = [UIColor yellowColor].CGColor;
highlightLayer.opacity = 0.25f;
[self.layer addSublayer:highlightLayer];
The shouldRasterize layer property may help to improve performance if necessary:
self.layer.shouldRasterize = YES;
In order do use all this, use the QuartzCore framework and import <QuartzCore/QuartzCore.h> in your implementation file.
You can create a PNG representation of your layer hierarchy by rendering the self.layer to an image context, then get the image and your PNG representation.

Resources