I need help with drawing something like this:
I have been told that the gray background bar and the purple bar should be drawn on separate layers. And then the dots there that signifies the chapters of a book (which this slider is about) will be on a layer on top of those two.
I have accomplished the task of creating the gradient on the active bar and drawing it like so:
- (void)drawRect:(CGRect)rect{
self.opaque=NO;
CGRect viewRect = self.bounds;
//NSLog(#"innerRect width is: %f", innerRect.size.width);
CGFloat perPageWidth = viewRect.size.width/[self.model.book.totalPages floatValue];
NSLog(#"perpage width is: %f", perPageWidth);
CGContextRef context = UIGraphicsGetCurrentContext();
UIBezierPath *beizerPathForSegment= [UIBezierPath bezierPath];
NSArray *arrayFromReadingSessionsSet =[self.model.readingSessions allObjects];
NSArray *arrayFromAssesmentSet = [self.model.studentAssessments allObjects];
NSLog(#"array is : %#", self.model.readingSessions);
CGGradientRef gradient = [self gradient];
for (int i=0;i<[arrayFromReadingSessionsSet count]; i++) {
ReadingSession *tempRSObj= [arrayFromReadingSessionsSet objectAtIndex:i];
CGFloat pageDifference = [tempRSObj.endPage floatValue]-[tempRSObj.startPage floatValue];
NSLog(#"startpage is: %#, end page is: %#, total pages are: %#", tempRSObj.startPage, tempRSObj.endPage, self.model.book.totalPages) ;
CGRect ProgressIndicator = CGRectMake(perPageWidth*[tempRSObj.startPage floatValue], viewRect.origin.y, perPageWidth*pageDifference, viewRect.size.height);
[beizerPathForSegment appendPath:[UIBezierPath bezierPathWithRoundedRect:ProgressIndicator cornerRadius:13.0]];
}
[beizerPathForSegment addClip];
CGContextDrawLinearGradient(context, gradient, CGPointMake(CGRectGetMidX([beizerPathForSegment bounds]), CGRectGetMaxY([beizerPathForSegment bounds])),CGPointMake(CGRectGetMidX([beizerPathForSegment bounds]), 0), (CGGradientDrawingOptions)NULL);
}
How do I shift it onto a layer and then create another layer and another layer and then put them over one another?
TIA
I’m guessing the person you spoke with was referring to CALayer. In iOS, every view has a CALayer backing it. Instead of implementing -drawRect: in your view, do this:
link with QuartzCore
#import <QuartzCore/QuartzCore.h> anywhere you want to use this.
Use to your view’s layer property.
Layers behave a lot like views, in that you can have sublayers and superlayers, and layers have properties for things like background color, and they can be animated. A couple of subclasses that will probably be useful for your purposes are CAGradientLayer and CAShapeLayer. For more on how to use layers, refer to the Core Animation Programming Guide.
Related
I have looked at the libraries like gaugekit but they does not solve my problem.
Are there any other libraries for making gauge view as in the image?
If not, then how can I go around about it?
As #DonMag pointed out.
I have tried to make the changes in gaugekit by adding a view on top the gauge view....but it does not turns out to be good.
So I am stuck out at making the spaces in between the actual gauge.
https://imgur.com/Qk1EpcV
I suggest you create your own custom view, it's not so difficult. Here is how I would do it. I have left out some details for clarity, but you can see in the comments my suggested solutions for that.
First, create a sub-class of UIVew. We will need one property to keep track of the gauge position. This goes into your .h file.
#interface GaugeView : UIView
#property (nonatomic) CGFloat knobPosition;
#end
Next, add the implementation. The GaugeView is a view in itself, so it will be used as the container for the other parts we want. I have used awakeFromNib function to do the initialization, so that you can use the class for a UIView in Storyboard. If you prefer, you can do the initialization from an init function also.
I have not provided code for the knob in the center, but I would suggest you simply create one view with a white disc (or two to make the gray circle) and the labels to hold the texts parts, and beneath that you add an image view with the gray pointer. The pointer can be moved by applying a rotational transform it.
- (void)awakeFromNib {
[super awakeFromNib];
// Initialization part could also be placed in init
[self createSegmentLayers];
// Add knob views to self
// :
// Start somewhere
self.knobPosition = 0.7;
}
Next, create the segments. The actual shapes are not added here, since they will require the size of the view. It is better to defer that to layoutSubviews.
- (void)createSegmentLayers {
for (NSInteger segment = 0; segment < 10; ++segment) {
// Create the shape layer and set fixed properties
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
// Color can be set differently for each segment
shapeLayer.strokeColor = [UIColor blueColor].CGColor;
shapeLayer.lineWidth = 1.0;
[self.layer addSublayer:shapeLayer];
}
}
Next, we need to respond to size changes to the view. This is where we create the actual shapes too.
- (void)layoutSubviews {
[super layoutSubviews];
// Dynamically create the segment paths and scale them to the current view width
NSInteger segment = 0;
for (CAShapeLayer *layer in self.layer.sublayers) {
layer.frame = self.layer.bounds;
layer.path = [self createSegmentPath:segment radius:self.bounds.size.width / 2.0].CGPath;
// If we should fill or not depends on the knob position
// Since the knobPosition's range is 0.0..1.0 we can just multiply by 10
// and compare to the segment number
layer.fillColor = segment < (_knobPosition * 10) ? layer.strokeColor : nil;
// Assume we added the segment layers first
if (++segment >= 10)
break;
}
// Move and size knob images
// :
}
Then we need the shapes.
- (UIBezierPath *)createSegmentPath:(NSInteger)segment radius:(CGFloat)radius {
UIBezierPath *path = [UIBezierPath bezierPath];
// We could also use a table with start and end angles for different segment sizes
CGFloat startAngle = segment * 21.0 + 180.0 - 12.0;
CGFloat endAngle = startAngle + 15.0;
// Draw the path, two arcs and two implicit lines
[path addArcWithCenter:CGPointMake(radius, radius) radius:0.9 * radius startAngle:DEG2RAD(startAngle) endAngle:DEG2RAD(endAngle) clockwise:YES];
[path addArcWithCenter:CGPointMake(radius, radius) radius:0.75 * radius startAngle:DEG2RAD(endAngle) endAngle:DEG2RAD(startAngle) clockwise:NO];
[path closePath];
return path;
}
Finally, we want to respond to changes to the knobPosition property. Calling setNeedsLayout will trigger a call to layoutSubviews.
// Position is 0.0 .. 1.0
- (void)setKnobPosition:(CGFloat)knobPosition {
// Rotate the knob image to point at the right segment
// self.knobPointerImageView.transform = CGAffineTransformMakeRotation(DEG2RAD(knobPosition * 207.0 + 180.0));
_knobPosition = knobPosition;
[self setNeedsLayout];
}
This is what it will look like now. Add the knob, some colors and possibly different sized segments and you are done!
Based on the image I saw the easiest solution might be to create 12 images and then programmatically swap the images as the value it represents grows or shrinks.
I am drawing image on a custom UIView. On resizing the view, the drawing performance goes down and it starts lagging.
My image drawing code is below:
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
UIBezierPath *bpath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, width, height)];
CGContextAddPath(context, bpath.CGPath);
CGContextClip(context);
CGContextDrawImage(context, [self bounds], image.CGImage);
}
Is this approach correct?
You would be better using Instruments to find where the bottleneck is than asking on here.
However, what you will probably find is that every time the frame changes slightly the entire view will be redrawn.
If you're just using the drawRect to clip the view into an oval (I guess there's an image behind it or something) then you would be better off using a CAShapeLayer.
Create a CAShapeLayer and give it a CGPath then add it as a clipping layer to the view.layer.
Then you can change the path on the CAShapeLayer and it will update. You'll find (I think) that it performs much better too.
If your height and width are the same, you could just use a UIImageView instead of needing a custom view, and get the circular clipping by setting properties on the image view's layer. That approach draws nice and quickly.
Just set up a UIImageView (called "image" in my example) and then have your view controller do this once:
image.layer.cornerRadius = image.size.width / 2.0;
image.layer.masksToBounds = YES;
Anyone know how to/if it's a good idea to animate CGPaths in a UIView's drawRect method?
For example, draw a black line from one end of the UIView to the other and then as a timer ticks over, change each individual pixel to a different colour variation to imitate a colour 'flow' of sorts (think Mexican wave, but with colour shades).
Is this doable/efficient?
Try this.
-(void)drawRect:(CGRect)rect{
[UIView animateWithDuration:0.5 animations:^{
CAShapeLayer *shapeLayer=[CAShapeLayer layer];
shapeLayer.path=[self maskPath].CGPath;
[yourview.layer setMask:shapeLayer];
}
-(UIBezierPath *)maskPath{
UIBezierPath *path=[[UIBezierPath alloc] init];
[path moveToPoint:[self normalizedCGPointInView:yourview ForX:1 Y:1]];
[path addLineToPoint:[self normalizedCGPointInView:yourview ForX:9 Y:1]];
[path addLineToPoint:[self normalizedCGPointInView:yourview ForX:9 Y:9]];
[path addLineToPoint:[self normalizedCGPointInView:yourview ForX:1 Y:9]];
return path;
}
-(CGPoint)normalizedCGPointInView:(UIView *)view ForX:(CGFloat)x Y:(CGFloat)y{
CGFloat normalizedXUnit=view.frame.size.width/10;
CGFloat normalizedYUnit=view.frame.size.height/10;
return CGPointMake(normalizedXUnit*x, normalizedYUnit*y);
}
By using a normalised co-ordinate scheme it's a lot easier to calculate the BezierPath for the mask. Additionally it means that the mask is always relative to the size of your view and not fixed to a specific size.
Hope this helps!
The question
How do you create a separate context for your layer and incorporate that context into super layer?
Why I want to do this
I want abstract the drawing of a view layers into separate objects/files. I want to construct a view out of layers, then position then on top of one another and have other possibilities as that.
The problem is that I'm not aware of you you're supposed to draw a part of your view into a layer without drawing straight into the context of the main views sublayer.
Here's an example, I have subclassed CALayer with HFFoundation:
#implementation HFFoundation
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
UIBezierPath *foundation = [UIBezierPath bezierPath];
// some drawing goes on here in the form of messages to [foundation]
}
#end
Now when I instantiate my custom layer inside a views (void)drawRect:(CGRect)rect I this way:
HFFoundation *foundation = [HFFoundation new];
foundation.frame = CGRectMake(40, viewHeight - 100, 300, 100);
foundation.backgroundColor = [[UIColor colorWithRed:1 green:.4 blue:1 alpha:0.2] CGColor];
[self.window.rootViewController.view.layer addSublayer:foundation];
I get this result (no drawings appear, just the bg color inside the layer frame):
If I [foundation drawLayer:foundation inContext:context]; afterwards, the drawing appears, but it appears inside the top layers context, not in foundation layer. Since foundation layer is also lower in the hierarchy, it hides the drawing (unless I reduce it's alpha, which I've dont in the picture):
How do you draw into the layer itself, I.e. foundation in this case?
To insert a sublayer and set its index you want this piece of code:
[view.layer insertSublayer:foundation atIndex:0];
I'm creating an app that allows users to cut out part of an image. In order to do this, they'll create a bunch of UIBezierPaths to form the clipping path. My current setup is as follows:
A UIImageView displays the image they're cutting.
Above that UIImageView is a custom subclass of UIImageView that
performs the custom drawRect: methods for showing/updating the
UIBezierPaths that the user is adding.
When the user clicks the "Done" button, a new UIBezierPath object is created that incorporates all the individual paths created by the user by looping through the array they're stored in and calling appendPath: on itself. This new UIBezierPath then closes its path.
That's as far as I've gotten. I know UIBezierPath has an addClip method, but I can't figure out from the documentation how to use it.
In general, all examples I've seen for clipping directly use Core Graphics rather than the UIBezierPath wrapper. I realize that UIBezierPath has a CGPath property. So should I be using this at the time of clipping rather than the full UIBezierPath object?
Apple say not to subclass UIImageView, according to the UIImageView class reference. Thank you to #rob mayoff for pointing this out.
However, if you're implementing your own drawRect, start with your own UIView subclass. And, it's within drawRect that you use addClip. You can do this with a UIBezierPath without converting it to a CGPath.
- (void)drawRect:(CGRect)rect
{
// This assumes the clippingPath and image may be drawn in the current coordinate space.
[[self clippingPath] addClip];
[[self image] drawAtPoint:CGPointZero];
}
If you want to scale up or down to fill the bounds, you need to scale the graphics context. (You could also apply a CGAffineTransform to the clippingPath, but that is permanent, so you'd need to copy the clippingPath first.)
- (void)drawRect:(CGRect)rect
{
// This assumes the clippingPath and image are in the same coordinate space, and scales both to fill the view bounds.
if ([self image])
{
CGSize imageSize = [[self image] size];
CGRect bounds = [self bounds];
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextScaleCTM(context, bounds.size.width/imageSize.width, bounds.size.height/imageSize.height);
[[self clippingPath] addClip];
[[self image] drawAtPoint:CGPointZero];
}
}
This will scale the image separately on each axis. If you want to preserve its aspect ratio, you'll need to work out the overall scaling, and possibly translate it so it's centered or otherwise aligned.
Finally, all of this is relatively slow if your path gets drawn a lot. You will probably find it's faster to store the image in a CALayer, and mask that with a CAShapeLayer containing the path. Do not use the following methods except for testing. You will need to separately scale the image layer and the mask to make them line up. The advantage is that you can change the mask without the underlying image being rendered.
- (void) setImage:(UIImage *)image;
{
// This method should also store the image for later retrieval.
// Putting an image directly into a CALayer will stretch the image to fill the layer.
[[self layer] setContents:(id) [image CGImage]];
}
- (void) setClippingPath:(UIBezierPath *)clippingPath;
{
// This method should also store the clippingPath for later retrieval.
if (![[self layer] mask])
[[self layer] setMask:[CAShapeLayer layer]];
[(CAShapeLayer*) [[self layer] mask] setPath:[clippingPath CGPath]];
}
If you do make image clipping with layer masks work, you no longer need a drawRect method. Remove it for efficiency.