Suppose you wanna implement the same functionality iOS's camera 'Zoom & Crop' has... in which you can scroll and crop an image.
Any section of the picture that exceeds the size of the crop area gets grayed out.
I'm trying to replicate exactly that. Provided that the flag 'clipToBounds' is set to NO, you can get the whole subview to get displayed.
However, i'm finding it a bit hard to gray out the UIScrollView's subviews overflow.
How would you implement that?.
Thanks in advance!
You can do this by creating a subclass of UIView that is semi-transparent in the overflow region and transparent in the "crop" region and placing it over your UIScrollView and extending it out to cover the overflow.
The main methods you need to implement are initWithFrame:
#define kIDZAlphaOverlayDefaultAlpha 0.75
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
mAlpha = kIDZAlphaOverlayDefaultAlpha;
self.backgroundColor = [UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:mAlpha];
self.userInteractionEnabled = NO;
}
return self;
}
Don't miss out the userInteractionEnabled = NO otherwise the scroll view will not sees events.
and drawRect
- (void)drawRect:(CGRect)rect
{
CGRect apertureRect = /* your crop rect */;
CGContextRef context = UIGraphicsGetCurrentContext();
/* draw the transparent rect */
CGContextSetRGBFillColor(context, 0.0, 0.0, 0.0, 0.0);
CGContextSetBlendMode(context, kCGBlendModeCopy);
CGContextFillRect(context, apertureRect);
/* draw a white border */
CGContextSetRGBStrokeColor(context, 1.0, 1.0, 1.0, 1.0);
CGContextStrokeRect(context, apertureRect);
}
The important point here is the kCGBlendModeCopy this allows us to draw (or cut) a transparent rectangle in a semi-transparent background.
If you want to can make the transparent rectangle a rounded rectangle, and include a preview of the cropped image and end up with something like the screen below:
Sorry I can't share all the code for the screen shot. It's from a client project :-(
I've solved this issue by...:
Adding a new helper class, 'ApertureOverlay' (subclass of UIView), with the following code:
(void)drawRect:(CGRect)rect
{
if(CGRectEqualToRect(_apertureRect, CGRectZero) == NO)
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetShouldAntialias(context, false);
CGContextSetRGBFillColor(context, 1.0f, 1.0f, 1.0f, 1.0f);
CGContextFillRect(context, _apertureRect);
CGContextSetShouldAntialias(context, true);
}
}
ApertureOverlay has a background with the alpha byte set to 50%.
[self setBackgroundColor:[UIColor colorWithRed:1.0f green:1.0f blue:1.0f alpha:0.5f]];
So far... al we've got is a view with transparent background, and a white rectangle (drawn with _apertureRect position + size).
After implementing this class, i've set up the 'mask' attribute of the ScrollView, which contains the image inside.
[[self layer] setMask:[_apertureOverlayView layer]];
That's it!. If you update the ApertureOverlay's '_apertureRect' attribute, you'll need to call 'setNeedsDisplay', so it gets redrawn.
One more thing. By setting the antialias to false (ApertureOverlay)... things work pretty smooth.
Related
I want to achieve the effect shown in this github project, but I don't want to use SpriteKit to achieve this.
So I try to draw the big circle in the CircleViiew.m about the CGContextRef in Objective-C.
#import "CircleView.h"
#import <QuartzCore/QuartzCore.h>
#define CIRCLE_RADIUS 80
#implementation CircleView
-(id) initWithFrame:(CGRect)frame andCircleRadius:(int) radius
{
self = [super initWithFrame:frame];
if(self)
{
}
return self;
}
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
// set rect background color
CGRect drawRect = CGRectMake(rect.origin.x, rect.origin.y,rect.size.width, rect.size.height);
CGContextSetRGBFillColor(context, 100.0f/255.0f, 100.0f/255.0f, 100.0f/255.0f, 1.0f);
CGContextFillRect(context, drawRect);
// set line border number
CGContextSetRGBStrokeColor(context,0.6,1,0.6,1.0);
CGContextSetLineWidth(context, 2.0);
CGContextAddArc(context, 82, 82, CIRCLE_RADIUS, 0, 2*M_PI, 0);
CGContextDrawPath(context, kCGPathStroke);
// fill color
UIColor *fillCircleColor = [UIColor colorWithRed:1.000 green:0.800 blue:0.000 alpha:1.000];
CGContextSetFillColorWithColor(context, fillCircleColor.CGColor);
CGContextAddArc(context, 82, 82, CIRCLE_RADIUS, 0, 2*M_PI, 0);
CGContextDrawPath(context, kCGPathFill);
CGContextDrawPath(context, kCGPathFillStroke);
}
#end
Then in the ViewController using the circle.
CircleView *circle = [[CircleView alloc] initWithFrame:CGRectMake(20, 20, 164, 164)];
[self.view addSubview:circle];
But I don't know how to add the small circle overlay the big circle, and the small have to limit on the big circle when we movement the small circle.
Is create small circle object using CircleView on the viewDidload to overlay BigCircle? Or the small circle have to draw in the CircleView class?
I am using auto layout to create the effect. I don't know if I use initwithFrame to make, have generate problem after.
Can anyone give me some direction?
Draw the small circle as a separate view, lying in front of (on top of) the big circle. It can be a UIImageView, for example.
Give the small circle a UIPanGestureRecognizer to make it draggable.
In the gesture recognizer handler, move the view to follow the user's gesture, except that you don't move the view if it would move further than a certain distance from the big circle's center.
I have simple UIView hierarchy:
container View:
→ Map View
→ Custom View
My custom view partially overlaps the Map View. I can see a map, but I can't interact with the map e.g. zoom, scroll etc.
How can I archive partially map overlap and interaction in the same time?
Feel free to ask me if you didn't understand something.
EDIT
I want to disable black areas to interaction, but allow interaction in the circle i.e. in the center of my UIView with black overlay areas.
I'm assuming you have something like this:
You want to be able to touch the red area, and touch the map where the yellow area is, but it is being blocked by the yellow subview?
If so, subclass the yellow subview and override the -pointInside: method, which allows you to specify whether a touched point will collide with the view, or fall back to a view behind it.
- (BOOL) pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
return [[UIBezierPath bezierPathWithOvalInRect:self.bounds] containsPoint:point];
}
if the view only partly covers the view below it, apply a mask to tell IOS that
e.g. from my github fork of XBPageCurl
- (void)applyCornerMaskAsNeeded {
//
//create mask
//
UIImage *cornerImage = [UIImage imageNamed:#"corner_view_mask.png"]; //this is black//white/alpha
CGRect b = self.layer.bounds;
CGRect rtCornerRect=CGRectZero;
UIGraphicsBeginImageContextWithOptions(b.size, NO, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
//white bg
CGContextSetFillColorWithColor(context, [[UIColor whiteColor] CGColor]);
CGContextFillRect(context, b);
//draw corner image mask
CGAffineTransform flipVertical = CGAffineTransformMake(1, 0, 0, -1, 0, cornerImage.size.height);
CGContextConcatCTM(context, flipVertical);
CGContextSetBlendMode(context, kCGBlendModeCopy);
rtCornerRect = [self cornerRectFor:XBPageDragViewCornerTopRight withSize:cornerImage.size.width];
CGContextDrawImage(context, rtCornerRect, cornerImage.CGImage);
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
//
//apply mask
//
CALayer *l = [CALayer layer];
l.frame = b;
l.contents = (id)image.CGImage;
l.name = #"XBPageDragViewCornersMask";
[self.layer setMask:l];
self.layer.masksToBounds = YES;
}
another example is in the apple docs but this should already be pretty clear:
you draw mask. black, alpha
you apple the mask to your view's layer
done
I have a label in my view. Now I want to draw a line above the label, which means you can see the label is under the line.
So far I can draw a line using quartz2D but always under the label. Is there any way to solve my problems?
You can create a CAShapeLayer like this:
CAShapeLayer *lineLayer = [CAShapeLayer layer];
lineLayer.frame = self.label.bounds;
lineLayer.strokeColor = [UIColor redColor].CGColor;
CGRect rect = CGRectMake(0, CGRectGetMidY(lineLayer.bounds), lineLayer.bounds.size.width, 2);
lineLayer.path = [UIBezierPath bezierPathWithRect:rect].CGPath;
And then add it to the UILabel like this:
[self.label.layer addSublayer:lineLayer];
To be honest, the easiest thing to do is to create a 2x2 pixel image called line#2x.png, and have the bottom 2 pixels black, the top 2 transparent, then use it as the background image for an image view. Stretch the image view to whatever width you need by using it as a pattern image. The 1x image should be a 1x1px, all black.
UIView *lineView = [[UIView alloc] initWithFrame:frame]; // Whatever frame the line needs
// Add the line image as a pattern
UIColor *patternColor = [UIColor colorWithPatternImage:[UIImage imageNamed:#"line.png"]];
lineView.backgroundColor = patternColor;
[self.view addSubview:lineView];
If this is a label you will be using a lot, you could make a sub-class of UILabel and override the drawRect function.
- (void) drawRect:(CGRect)r
{
[super drawRect:r];
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
CGContextSetLineWidth(context, 1.0);
CGContextMoveToPoint(context, 1.0, 1.0);
CGContextAddLineToPoint(context, self.bounds.size.width - 1.0, 1.0);
CGContextStrokePath(context);
CGContextRestoreGState(context);
}
The advantage here is that the line will be "baked" into the view and only be draw once. Other methods such as CAShapeLayer or UIView will be re-rendered every frame.
For bonus points, you can make the color and line width properties :)
//inside init
_color = [UIColor orangeColor];
self.backgroundColor = [UIColor clearColor];
self.clearsContextBeforeDrawing = NO;
//inside drawRect
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSaveGState(ctx);
CGContextAddEllipseInRect(ctx, rect);
CGContextSetFillColorWithColor(ctx,
_color.CGColor);
CGContextFillPath(ctx);
CGContextRestoreGState(ctx);
The background keeps showing up as white rather then transparent. The ellipse also does not change color and shows up as black. Thank in advance to anyone who takes a look.
I think you can refer to this answer for your transparency issue :
Setting A CGContext Transparent Background
As for your ellipse color issue, it correctly display as an orange ellipse on my simulator so you may check at possible overlays or other possible side effects from other parts from your code.
I'm trying to modify some simple "create a paint app" code to have a white background colour, rather than the black that it is set to. The example code is located at:
http://blog.effectiveui.com/?p=8105
I've tried setting self.backgroundcolor = [UIColor whiteColor], also [[UIColor whiteColor] setFill] with no effect. I'm missing something very basic due to my inexperience.
Does anyone have any ideas? Many thanks in advance!
I've added a couple of lines to the drawRect that should do it for you. You were on the right track, but when you set the color to white, you actually need to then fill the paintView rectangle with it:
- (void)drawRect:(CGRect)rect {
if(touch != nil){
CGContextRef context = UIGraphicsGetCurrentContext();
//clear background color to white
[[UIColor whiteColor] setFill];
CGContextFillRect(context, rect);
hue += 0.005;
if(hue > 1.0) hue = 0.0;
UIColor *color = [UIColor colorWithHue:hue saturation:0.7 brightness:1.0 alpha:1.0];
CGContextSetStrokeColorWithColor(context, [color CGColor]);
CGContextSetLineCap(context, kCGLineCapRound);
CGContextSetLineWidth(context, 15);
CGPoint lastPoint = [touch previousLocationInView:self];
CGPoint newPoint = [touch locationInView:self];
CGContextMoveToPoint(context, lastPoint.x, lastPoint.y);
CGContextAddLineToPoint(context, newPoint.x, newPoint.y);
CGContextStrokePath(context);
}
}
The reason view.backgroundColor = [UIColor whiteColor]; didn't work is that any view that implements drawRect ignores the backgroundColor property, and the programmer is responsible for drawing the whole view contents, including the background.
I think the part you've missed is this section of PaintView:
- (BOOL) initContext:(CGSize)size {
int bitmapByteCount;
int bitmapBytesPerRow;
// Declare the number of bytes per row. Each pixel in the bitmap in this
// example is represented by 4 bytes; 8 bits each of red, green, blue, and
// alpha.
bitmapBytesPerRow = (size.width * 4);
bitmapByteCount = (bitmapBytesPerRow * size.height);
// Allocate memory for image data. This is the destination in memory
// where any drawing to the bitmap context will be rendered.
cacheBitmap = malloc( bitmapByteCount );
if (cacheBitmap == NULL){
return NO;
}
cacheContext = CGBitmapContextCreate (cacheBitmap, size.width, size.height, 8, bitmapBytesPerRow, CGColorSpaceCreateDeviceRGB(), kCGImageAlphaNoneSkipFirst);
return YES;
}
That creates a single context, which it calls a cache, that all subsequent touches are drawn to. In the view's drawRect: it simply copies the cache to the output.
One of the flags it provides — kCGImageAlphaNoneSkipFirst — specifies that the cached context has no alpha channel. So when it's drawn there's no chance for the background to show through regardless of any other factor; the black comes from the cacheContext just as if you'd painted black with your finger.
So what you really want to do is to fill the cacheContext with white before you begin. You can either do that by memsetting the cacheBitmap array, since you've explicitly told the context where to store its data, or you can use a suitable CGContextFillRect to the cacheContext.
If you want to use the source code but have a clear background to the image you are creating and 'inking' on - when you setup the cachedBitmap, do it like this.
cacheContext = CGBitmapContextCreate ( cacheBitmap
, size.width
, size.height
, 8
, bitmapBytesPerRow
, CGColorSpaceCreateDeviceRGB()
, kCGImageAlphaPremultipliedFirst
);
That way, when you set your stroke color for the 'ink' only the 'ink' will be painted. Ensure that the drawing View also has a background color of clearColor, and opaque set to NO.
This means that the view that the drawing view has been added to will now be visible underneath or through this drawing view. Therefore set the background color of that view to whatever you want or alternatively, put a UIImageView behind the drawing view and voila! You can insert an image for lined paper or graph paper or whatever you want!