I have a problem understanding how the parameter passed to the drawInRect method is defined when a rotation transformation is performed on a UIView.
To give an example I have a UIView which I rotated with an angle of 307 degree.
In the drawInRect method I log the following:
self.frame: {{103.932, 273.254}, {64.3007, 84.3374}}
rect (the variable passed as parameter:{{0, 0.102913}, {18, 89}}
The problem is that according to the documentation I should not draw outside of rect, but considering what I should draw, there is no way my images will fit there.
Can anyone explain to me how I am supposed to use drawInRect in the case my UIView is rotated ?
To give more detail about my problem, here is what I do:
I have a scrollview with a contentView inside (subclassed). I add my UIViews in the content view.
The views in question are composed of a handler image (bottom left) and the main image (top right). Users are supposed to grab the view by pressing the handler but that's not the point.
The drawInRect method of the UIView contains the following:
[_image drawInRect:CGRectMake(handlerSize.width, 0, _image.size.width, _image.size.height)];
CGSize size = CGSizeMake(kHandPickerWidth/self.scrollViewScale, kHandPickerHeight/self.scrollViewScale);
[_handPickerImage drawInRect:(0, _image.size.height, size.width, size.height)];
The UIViews objects are added at viewWillAppear in the content view doing the following:
first instanciate,
then addSubview:
then I set the scrollViewScale parameter,
then I set the frame parameter (according to the top right image displayed (which may vary)
then I rotate the UIView.
Starting from line three, the code is executed from the
- (void)scrollViewDidZoom:(UIScrollView *)scrollView
to make sure every variable is set properly when displayed is needed.
The line defining the size variable is to adjust the marker's size no mater the zoomScale value of the scroll view.
You basically just draw as you would normally, and the painting will be rotated by iOS for you. You can get this transformation information if you would want to.
You need to get a reference to the currect graphics context:
CGContextRef ctx = UIGraphicsGetCurrentContext();
Then query the transformation matrix directly:
CGAffineTransform tf = CGContextGetCTM(ctx);
NSLog(#"current ctm: %#",NSStringFromCGAffineTransform(tf));
or better, get the transfrom from your drawing function to the device:
CGAffineTransform tf = CGContextGetUserSpaceToDeviceSpaceTransform(ctx);
NSLog(#"user->device transform: %#",NSStringFromCGAffineTransform(tf));
And in drawRect: you should not rely too much on the passed CGRect, because it serves mostly as a hint to what piece of the view needs updating. (e.g. because you called -setNeedsDisplayInRect: on it). To get the actual bounds where your view lives in, use self.bounds.
Drawing outside of the CGRect is no real problem, but will only hurt performance a little.
edit
ps. to clarify: self.frame is the frame of your view in the parent view coordinate system. It changes if you move, rotate or otherwise transform the view. self.bounds is the frame of your view in its own coordinate system, and (therefore) remains constant under changes of position or transformations.
So I found a solution to my problem:
I was setting the frame parameter multiple times with some CGAffineTransformation defined which is not supposed to be done.
Now each time I need to reset the frame I reset the affine transform, change the frame and set the back the affine transform.
Everything works as supposed to this way.
Related
I have a UIView subclass with the drawRect: method overriden. In there, I have long lines of generated code that draw something.
The generated code has a problem of having all the vertices/coordinates of the paths/lines hard-coded. So, to draw a 100x100 square, it would start at 0,0 and go to 0,100 -> 100,100 -> 100,0. To make this shape scale based on the UIView bounds property was done as follows:
// This is the size of the drawing. I always know this value beforehand.
CGSize contentSize = SHAPE_XMARK_SIZE;
CGFloat scaleX = self.bounds.size.width / contentSize.width;
CGFloat scaleY = self.bounds.size.height / contentSize.height;
CGContextScaleCTM(context, MIN(scaleX, scaleY), MIN(scaleX, scaleY));
All good as far as the scale is concerned. Now, I would like the position of the drawing to be ralative to the bounds, too. I want to somehow make the drawRect: method align the drawing based on the bounds, too.
I am thinking of something like:
push context method
draw code
pop context
position previous context
Is that a sane approach? Or is the push and pop context not able to accomplish such sorcery?
One Simple Approach:
To answer the "what have you tried" question, I tried making the view that I draw isolated with a bounds size equal to the drawing size, then embed that view inside another view... and it works. However, this method is tedious and I would prefer avoiding it, if possible.
Pfft, the answer was super simple. Me 1, SO 0, I guess.
CGContextTranslateCTM(context, xOffset, yOffset);
Yeah, I somehow overlooked the fact that an awesome function should exist to translate CGContextRef, since the scale function existed.
The rest is all simple math to achieve vertical & horizontal alignment.
I can't find an answer for this one.
I would like to know how to have the image size in a calayer's to be lower than calayer's bound's size.
I've got several pawns in an iPad game, each is a CALayer and I have them resize simply with a contentsGravity=kCAGravityResizeAspect. Image is 128x128 inside of a CALayer of 30x30 so the image gets resized automatically to 30x30 and because of both being a box, aspect ratio maintains and works.
Here I set CALayer's bounds proportional relative to superview's size, so the Pawns always present the same relative size to the view. This one is inside my sprite class subclass of calayer:
-(void) setSpriteScaleToDice {
CGFloat newSize = [self superlayer].bounds.size.width * 0.066666667f;
self.bounds=CGRectMake(0.0f, 0.0f, newSize, newSize);
self.contentsGravity = kCAGravityResizeAspect;
}
Note that in my case the CALayer bounds gets a maximum of 30x30 which is small for a touch. That's the problem I'm facing, due to this small size it's difficult to "touch" them, sometimes touch fails...
One of the ideas that I'm thinking is to increase the "bounds" of the calayer, while keeping the image at its original size. The problem is that I've search a lot and tried several options with contentsGravity, contentsCenter, contentsScale, etc... without success.
In particular, as per apple docs looks like the way to go is with contentsCenter (and not using contentsGravity), however I get deformation in the bitmap...
Please, any idea is really welcome, and thanks in advance,
Luis
This is probably a silly question, but why are you using CALayers for this instead of UIViews? UIImageView has a contentMode property that lets you do this easily (not to mention being easier to use for touch event handling).
That said, CALayer has a contentsRect property that appears to let you define a sub-rectangle for contents to be drawn within, so that may let you do what you want.
Another option would be to place your image layer inside a larger layer and use that for the hit test.
CAlayer CGFloat contentsScale
/* Defines the scale factor applied to the contents of the layer. If
* the physical size of the contents is '(w, h)' then the logical size
* (i.e. for contentsGravity calculations) is defined as '(w /
* contentsScale, h / contentsScale)'. Applies to both images provided
* explicitly and content provided via -drawInContext: (i.e. if
* contentsScale is two -drawInContext: will draw into a buffer twice
* as large as the layer bounds). Defaults to one. Animatable. */
If you want your image drawn in the CALayer at a size other than the CALayer you need to create your own drawInContext: method and draw the image rather than setting the CALayer's contents property. Do not set the contents property, create your own to track the image you want to draw.
What's the difference between bounds and frame? In fact, why does 'bounds' even exist? The size of 'bounds' is equal to the frame's size, and the bound's origin should always be 0,0.
From the View and Window Architecture Programming Guide for iOS:
A view object tracks its size and location using its frame, bounds,
and center properties:
The frame property contains the frame rectangle, which specifies the
size and location of the view in its superview’s coordinate system.
The bounds property contains the bounds rectangle, which specifies the
size of the view (and its content origin) in the view’s own local
coordinate system.
The center property contains the known center point of the view in the
superview’s coordinate system.
Here is a good visualization of that explanation:
The bound's origin is not always 0,0. It's easy to understand the difference between frame and bounds, if you watch how change bounds property of UIScrollView during scrolling.
For example, you have UIScrollView with frame (0, 0, 320, 460), bounds (0, 0, 320, 460) and ContentSize (640, 460). Its frame always will be (0, 0, 320, 460), but the X coordinate of bounds will change depending on distance of scrolling.
It can be useful if you want to change something in you UIScrollView (create and remove pages diynamically for example), so you want to know distance of scrolling.
The apple documents in the first answer don't cover what happens to the frame and the bounds after rotating to landscape orientation. So to be more complete, you should know that the frame of the window and the root view does not change after rotation, but the bounds do. See this article for a a little more detail and be careful using frame as a reference for anything other than portrait orientation.
From the article:
If your view controller has the top-level non-window view (i.e., it’s
the bottom-most view controller), then
self.frame
is always in portrait orientation. Wha? Yes, always in portrait –
what changes is the transform of your view. So your
self.bounds
is always accurate (keeping in mind the last point), but
self.frame
may or may not give the aspect ratio that the user is really seeing,
since each view’s frame is reported in terms of the superview’s
coordinates, and takes into account any transforms applied to the
view.
A views frame is the size of a rectangle it can completely fit into. It always seems as if the bounds and the frame are same but that's not the case.
Consider a square which is just rotated about 45 degrees!
Here the frame of the this rotated square will be the rectangle to completely fill it in and so it will differ from the bounds of this object.
P.S mostly in rotated objects frames and bounds tend to differ.
frame is coordinates values in the super view's coordinate system
bounds is used by the drawing system to draw the view's content, when the drawing is done, the system will use a transform operation to assign the content to the view's frame
Cocoa's NSScrollView is horribly under-explained. I hope someone here knows what it's all about and can spare me a few seconds.
So I have a custom NSView. I implement -drawRect: for it to draw something, fill itself with colour, whatever. Then I have an NSScrollView wrapping it (set up through the interface Builder).
Now the inner, custom, view must have a size larger than that which fits in the outer scroll view—for it to scroll. That much I realise. I have incidentally configured it so that the scroll view adjusts to the surrounding window’s size, but that shouldn’t matter.
I override my inner view’s -frame method to return a frame sized at least 1000x1000.
- (NSRect)frame {
CGFloat w = 1000;
CGFloat h = 1000;
if (self.superview.bounds.size.width > w)
w = self.superview.bounds.size.width;
if (self.superview.bounds.size.height > h)
h = self.superview.bounds.size.height;
return NSMakeRect(0, 0, w, h);
}
Here’s the outcome, which I have trouble interpreting:
I can scroll when the scroll view encloses an area smaller than 1000x1000
BUT
The only area filled with colour (i.e. that my -drawRect: method has any effect on) is
as large as the scroll view’s bounds
located at (0,0. I use flipped, so that’s top left, and it ends up being outside the visible area after scrolling.
The visible area that lies outside this irrelevant rectangle is not painted at all.
I don’t know anything beyond this point. It seems like the rect for drawing is clipped to the scroll view’s position in the window, and size, or something—but it does not take the scrolled "location" into account.
It should be noted that I don't really expect anything else to happen. I feel I am missing a piece, but can't find which. Sorry for the wall of text, but I can’t explain better right now. I hope it is easier to answer than it is to ask.
Regards and hope,
Not Rick Astley
It's a very very very bad idea to overwrite -frame. There is so much that depends on the actual instance variable having a correct value. Instead try to set the frame to the one you want using setFrame:, that might fix all your problems if you're lucky...
I agree with Max's warning that you shouldn't override -frame. If you want to constrain the set frame, override its setter ( -setFrame: ) and the designated initializer ( -initWithFrame: ) and adjust the proposed frame as desired.
Regarding your overall problem, I wonder if your problem is conceptual. The argument for -drawRect: (the dirty rectangle you're asked to redraw) is useful if you're drawing something that you can redraw incrementally in parts (like a grid - any grid blocks intersecting dirtyRect can be redrawn and the rest can be ignored). If you're doing something that has to be completely redrawn, you should use [self bounds] and not the dirty rect passed at drawRect.
For example, if you have just a standard gradient background, it's difficult to tell from dirtyRect which part of the gradient to redraw and infinitely easier just to redraw the whole view with the gradient, ignoring dirtyRect altogether.
You're right in assuming that only the area of your view exposed by the scroll view's clip rect will normally be asked to redraw when scrolling. There're also interactions with the scroll view's -copiesOnScroll to consider.
I hope this helps.
Use of the NSScroller really relies on a solid understanding of the MVC paradigm. Apple's docs really focus on showing a photo and a set of text, but not much else. The use of NSScrollView is something that I've struggled with in the past.
First off, do not override frame. Use setFrame to tell the scrollView how large the working area is, and then just simply draw in the area the frame encompasses. As I understand it, a custom NSView and the encompassing NSScrollView takes care of the rest, such as what to draw where when. In other words, ignore the bounds of the rect passed into drawRect and instead draw within the bounds of the frame you sent to scrollView; don't worry about what is visible and what isn't because that is the job of the framework.
Here is where the MVC paradigm comes in: setFrame should be used when your Model is updated. So, if an object falls outside of the current bounds of the frame, then use setFrame to set the newly expanded bounds, and then draw within that area.
EDIT: I originally asked this question in regards to general view resizing, but I realize now that it's something much more specific to tableViews.
Say that a tableView originally has a frame of (0 0, 320 460), i.e., it fills an entire iPhone screen. In order to resize the tableView's height and animate the resize, I map the following code to a button:
[UIView beginAnimations:#"Animation" context:nil];
[UIView setAnimationDuration:3];
CGRect rect = self.table.bounds;
rect.size = CGSizeMake(320, 200);
self.table.bounds = rect;
self.table.center = CGPointMake(160, 100);
[UIView commitAnimations];
This code will animate the table's height down, BUT...before any animation happens, the table will clip off all of the table cells that won't fit in the new bounds. So, given a tableView that originally fits 10 rows, the animation code above will cause this sequence of events:
The tableView removes the bottom 5 rows immediately. Note that it only removes the rows - the tableView's background still extends to the bottom.
The tableView animates the height change for the rest of the table as expected.
This is not the behavior I am looking for. Instead of clipping the bottom 5 rows immediately, the tableView should keep its rows in place while the bottom edge of the table just animates upwards.
Based on the behavior I'm seeing, it seems that the tableView is getting some sort of message to refresh its rows immediately after it finds out that its bounds is going to change. Can someone shed some light on this behavior and how I could avoid it?
The bounds property of a view defines the internal coordinate space of the view. If you are changing the size and/or position of the view, you should only change the view's frame. Theoretically, the bounds property could define a totally arbitrary coordinate space disconnected from the view's enclosing frame and UIKit is seemingly designed to support that distinction. In practice I don't think I've ever seen it work that way in UIKit (the bounds.size is always the same as frame.size and bounds.origin is always {0,0}), but that's the distinction being made there and it's important to remember - especially if this behavior ever changes in the future.
I'm not certain that simply changing the code to modify the frame instead of the bounds will solve the issue, but I recommend starting there. If that doesn't solve it, then the problem is probably that the tableview is immediately removing the rows which will become invisible with the new frame, so if that's the case, it's possible that the only solution would be to do the animation yourself by setting up a simple NSTimer-driven routine that slowly changes the frames size/position over time thus creating the illusion you want in spite of the tableview's optimizations or interactions with Core Animation.
Did you try changing the bounds and center within the animation block.
I don't know about this specific case but ISTR having to do similar before. The relationship between frame, bounds and center can be frustrating at times.