UIScrollView pinch zoom into CGContextStrokePath without changing line width - iphone

I have a UIScrollView that contains a custom UIView. In my UIView I am overriding drawRect to draw a path using CGContextStrokePath. I would like to slightly alter the way the zoom works. Pinch zooming out will show more of the paths on the screen. This is what I want but i want the line width to stay the same not shrink as you zoom out so that they are still clear to the user.
I thought I would just do this (myUIView zoom target is called _lineView)
-(void)scrollViewDidZoom:(UIScrollView *)pScrollView
{
_lineView.zoomScale = _scrollView.zoomScale;
[_lineView setNeedsDisplay];
}
and then just calculate an appropriate line stroke size in my _lineView drawRect method to give the effect of constant line width as you zoom out.
This is really slow and I have read that this is expected as drawrect is not optimised to be called many times a second.
I then started looking at using a GLKView instead and just rendering the whole thing in opengl directly. The problem with this is I will have to implement all of the zooming and panning myself (with all the lovely zoom and pan bounce effects you get for free in UiScrollView). I will also have to implement all the controls I want to use in opengl, buttons etc.
Is there a way to do this whilst still using Quartz2d? I feel like opengl will give me lots of power but it will take me much longer to get the rest of my app done if I go down that route.

I figured this out. I found a simple way to do this that does not slow down the zoom / bouncing animations at all and is very fast and fluid.
Quartz has a class called CAShapeLayer that lets you do some pretty cool stuff. Among these is being able to set a CGPath property and specify a linewidth. Changes are reflected in the view.
So i basically call shapeLayer.lineWidth from my scrollViewDidZoom method and it does exactly what i need.

What I would do if I were you is to use default zooming behavior while zooming (which does not redraw, but instead just applies a transform to the zoomed view, which can be done by the GPU very, very quickly, but as you have noticed can lead to inferior quality).
Then, when the user finished zooming, redraw the whole view as you do now. The appropriate delegate method is scrollViewDidEndZooming:withView:atScale:.
This way, you have fast (but slightly ugly) zooming, and nice (but slightly, probably unnoticeably, slower) display after the zoom is finished.

Related

iOS zoom algorithm

I am creating a graphing calculator application in OpenGL on iOS, and I would like to implement pinch zooming that works like the google maps application or uiscrollview zooming. I'm pretty sure I can't use a uiscrollview because the content of the graph is being generated dynamically.
Implementing zooming where the center of the screen is assumed to be the center of the zoom is easy, but in other cases it is not obvious to me how its being implemented. Can anyone point me in the right direction?
The view that does your custom drawing should be capable of drawing at a certain scale. When your user zooms in or out of the scroll view, send the appropriate zoom factor and let the view redraw itself. The changing bounds is not a problem. The scroll view adjusts its bounds as you pan inside, so its subviews shouldn't have any concern over the bounds of the scroll view.
Of course, you'll probably have to consider performance. For example, it may be too slow to redraw the graph at every frame as a user zooms. Instead, you may only want to redraw when the user stops zooming, which is what Maps.app does.
There are many other considerations for this problem, but that would be where I would start.
EDIT: Ah, I overlooked the OpenGL aspect. Still, with a GL layer-backed view, you still should be able to make the appropriate translations based on both the current zoom factor as well as the scroll view's bounds.

draw outside of UIView's bounds from drawRect

My question is very similar to this one Not drawing outside bounds when clipToBounds=NO which received no clear answer.
Basically I have a UIView, and I want to draw a line from the center of it, to the edge of the screen. Calculating where these points are is easy, using [self convertPoint:(CGPoint){0,0} fromView:[self superview]]; (which finds the origin with respect to my view's superview. But when I draw a line from my view's drawRect: it gets clipped at my view's bounds.
Is there a way to draw outside of my view's bounds? I've tried changing the clipsToBounds property, but it doesn't seem to have any effect.
I can't draw my lines from the superview because I need to do this with multiple views and some will be in front of others... figuring out the layer from the superview's drawRect seems like a bad idea.
Similarly, I don't think I can just resize my view's bounds to include the entire screen, because my views need to be dynamically re-sizable... the bounds would have to be HUGE (>20,000 points square) for this to work.
I wouldn't recommend ever drawing outside of a view's bounds. Either your view needs to resize automatically to include your drawing or you need to have transparent overlapping views. Or both. I can't think of a situation that either of these cases wouldn't cover, but I may lack imagination. =)
Likely what is happening currently is that when the super view gets redrawn it tells the super view that it needs redrawn, resulting in erasing the drawing you are doing outside. It's been a while, anyone more knowledgeable can (should!) correct me here if I'm wrong.
I don't know if "Quartz Debug" (from the standard apple developer tools install, /Developer/Applications/Performance Tools/Quartz Debug) works in the simulator, but it's worth a try. It has a mode that will show you when and how often redrawing takes place, with a border and optional delay on the refreshes.
You can do what you are asking, but you need to force redraw your sub-views every time you go outside the sub-view's bounds, meaning that your super-view needs to manually draw it's children inside of it's draw function. Essentially you would be throwing out apple's drawing paradigm and simply causing your sub-views to act like a drawing extension of your main view anyway.
Additionally, if your ranges are so dynamic you may want to consider drawing in percentages of the screen or super-view rather than in points, it may make more sense to code.

Zooming out for the first time in UIScrollView is not smooth

I created UIScrollView and added UIView with lots of tiled UIButtons in the UIView. My problem is, when every time I zoom out the content using zoomToRect method of UIScrollView to the minimum scale I set, the zooming out is not smooth. But zoom-in and zoom-out for the second time is smooth. How can I make the zooming out for the first time to smooth zooming?
Thanks.
The slow initial zoom is obviously due to the phone allocating all the UIButtons the first time it has to draw them. They should be allocated incrementally or before the user starts to interact with them.
What are you doing that requires so much loading and drawing? It doesn't sound like the user would be able to interact with the million or so buttons they might be viewing.
I would suggest adding a pile of code to a UIScrollView Sub Class that makes it aware of it's content size, and it can then init the required UIButtons before the user starts to interact with your UIScrollview, or incrementally as I said.
There is demo code called 'Tiling' that sheds some light on using UIScrollViews to manage large content. It's quite complex, but a very complete demo that I'm sure most projects implement if they handle UIScrollViews with tiled content.
Before zoom out set your UIScrollView* scrollView like:
scrollView.contentInset = UIEdgeInsetsMake(0,0,0,0);

How can I stop the iPhone from displaying a transform change until after the screen is redrawn

I have found the UIScrollView's zooming mechanism to be clunk and essentially unusable. So instead, I'm rolling my own. I have a UIView that resizes itself with the pinch-zoom, and that's working fine. When the zoom is complete, the view needs to reset its transform and redraw the images.
The zoom works essentially in the same way the UIScrollView does. It sets the transform property of the UIView until complete. Then, when the zoom finishes, I want to reset the transform to CGAffineTransformIdentity, resize the frame to be the size it was before, and tell the view to redraw itself at the new size.
It all works pretty well, except when I change the transform to identity then redraw the image, there is a slight flicker before the image completely redraws. This is due to the fact that I'm using a subclass of CATiledLayer, since the view can be of arbitrary size.
I've overridden the fadeDuration to be zero, but there is still a flicker while the transform is reset before the redraw is finished. Is there any simple way to overcome this without creating another view to draw with then replacing it?
Problem solved using a lower-level approach to layers.

CATiledLayer blanking tiles before drawing contents

All,
I'm having trouble getting behavior that I want from CATiledLayer. Is there a way that I can trigger the tiles to redraw without having the side-effect that their areas are cleared to white first? I've already subclassed CATiledLayer to set fadeDuration to return 0.
To be more specific, here are the details of what I'm seeing and what I'm trying to achieve:
I have a UIScrollView with a big content size...~12000x800. Its content view is a UIView backed by a CATiledLayer.
The UIView is rendered with a lot of custom-drawn lines
Everything works fine, but the contents of the UIView sometimes change. When that happens, I'd like to redraw the tiles as seamlessly as possible. When I use setNeedsDisplay on the view, the tiles redraw but they are first cleared to white and there's a fraction-of-a-second delay before the new content is drawn. I've already subclassed CATiledLayer so that fadeDuration is set to 0.
The behavior that I want seems like it should be possible...when you zoom in on the scrollview and the content gets redrawn at a higher resolution, there's no blanking before the redraw; the new content is drawn right on top of the old one. That's what I'm looking for.
Thanks; I appreciate your ideas.
Update:
Just to follow up - I realized that the tiles weren't being cleared to white before the redraw, they're being taken out entirely; the white that I was seeing is the color of the view that's beneath my CATiledLayer-backed view.
As a quick hack/fix, I put a UIImageView beneath the UIScrollView, and before triggering a redraw of the CATiledLayer-backed view I render its visible section into the UIImageView and let it show. This smooths out the redraw significantly.
If anyone has a better solution, like keeping the redraw-targeted tiles from going away before being redrawn in the first place, I'd still love to hear it.
I've found that if you set levelsOfDetailBias and levelsOfDetail both to the same value (2 in my case), then it only redraws the tiles that are touched by my setNeedsDisplayInRect: call, as you'd hope.
However if the levelsOfDetail is different to LODB, then any calls to setNeedsDisplayInRect: redraw all the tiles.
You could add another layer (possibly a CATiledLayer) behind the existing tiled layer. (Sort of a double-buffered solution.) You would call setNeedsDisplay: on the second layer from a timer that fires after a few seconds to ensure that that layer doesn't redraw at the same time as the front layer.
Another potential option is to use the same delegate to draw content to a bitmap context and swap the bitmap into the backing store once the content is refreshed. This should produce a flicker-free result. That being said, I can't tell you how this might be done, and one nice thing about CATiledLayers is they automatically generate tiles when you zoom and pregenerate tiles when you pan once zoomed in.
I would like to see how you implement your application. I have been looking for weeks to find an example that uses a combination of UIScrollView and a CATiledLayer-back view with a lot of custom drawn lines. Apple has some great sample code - but it all involves images rather than line art, so no help for me.
Having read through these answers without a solution, I discovered that tiling a page was the dominant background task.
Preparing my lo-res placeholder image on a high priority queue solved this issue - the images now appear while the tiling is occurring. Caching the placeholder images further improves their appearance - they appear before the tiling begins.
With newer devices, the tiling it so fast, these tricks might not matter. A sample PDF consisting of large scanned images (e.g. a scanned book) tiles the slowest in my experience, and makes for good test data.
I had the same problem with iPad.
The solution was more simple than I thought and far more simple than using UIImageView to render display before redrawing... :
Just don't set any background color for the Layer!
I had CATiledLayer set in a similar way:
layer = [[CATiledLayer alloc] init];
layer.masksToBounds = YES;
layer.contentsGravity = kCAGravityLeft;
//layer.backgroundColor = [[UIColor whiteColor] CGColor];
layer.tileSize = CGSizeMake(1004.0, 1004.0);
layer.levelsOfDetail = 16;
layer.levelsOfDetailBias = 8;
Note that I have commented out the line setting layer's background color to white.
After that the white blank before redraw problem disappeared!
Let me know if anyone has tried that.