Pan works fine for me, but pinch with recognizer code like this does not:
- (void)pinchDetected:(UIPinchGestureRecognizer *)pinchRecognizer
{
CGFloat scale = pinchRecognizer.scale;
self.imageView.transform = CGAffineTransformScale(self.imageView.transform, scale, scale);
pinchRecognizer.scale = 1.0;
}
What happens is that the image view is continuously resetting the image according to its "mode", whether it's center, aspect fit, etc.
I solved my problem: I'm making my first image viewer, and to learn how to pinch and zoom, I naively googled for how to support gestures, which are not enabled by simply adding an image view to a view controller.
Unfortunately, there are many "tutorials" on this, showing how to program with the gesture recognizers, etc. And I spent a few hours going down this route unnecessarily. I kept going because I felt tantalizingly close to getting things working: The pan gesture was flawless and was "just" zoom that was broken.
(Side question: is there some awesome source for current, iOS 6 "best practices"?)
It turns out, this is the wrong path and needlessly complex for basic gesture recognition. All that's needed is to place the image view in a scroll view. 99% of the programming is taken care of. (I was convinced this had to be the case — I couldn't believe that such core functionality wouldn't be provided by cocoa touch.)
Related
I have been working on an iPad app that performs animations on very large images (full screen images that can be zoomed at 2x and still be retina quality). I have spent a lot of time getting smooth transitions when zooming and panning. When running the app on iOS7 however, the animations become really jerky (slow frame rate).
Further testing shows that it is the zoom animation that causes the problem (panning does not cause a problem). Interestingly, I have been able to fix it by setting the alpha of the image being scaled to 0.995 (instead of 1.0).
I have two questions
What has changed in iOS7 to make this happen?
Why does changing the opacity of the view make a difference?
Further information for the above questions:
Animation Setup
The animations are all pre-defined and are played upon user interaction. The animations are all a mix of pan and zoom. The animations are really simple:
[UIView animateWithDuration:animationDuration delay:animationDelay options:UIViewAnimationOptionCurveEaseInOut animations:^{
self.frame = nextFrame;
//...
} completion:^(BOOL finished) {
//...
}];
To fix the jerky animation, I set the alpha before the animation
self.alpha = 0.99;
Some interesting points:
Setting the alpha inside of the animation works as well
Setting the alpha back to 1.0 after the animation and then doing the reverse animation with a 1.0 alpha does not give a smooth reverse animation.
Opacity fix
I have previously used the opacity fix to make animations smooth when scaling and panning multiple images. For example, I had two large images panning and scaling at different speeds with one on top of the other. When a previously un-rendered part of the lower image (the image on the bottom) became visible, the animation would become jerky (panning as well as scaling). My thought for why alpha helps in this case is, if the top image has a bit of transparency, the bottom image must always be rendered, which means it can be cached before the animation takes place. This thought is backed by doing the reverse animation and not seeing the jerky animation. (I guess I would be interested to know if anyone has different thoughts on this as well).
Having said the above, I don't know how this would have an affect when there is just one image (as in the situation I am describing in my question). Particularly when after getting the jerky animation, the reverse animation is still jerky. Another point of difference between the two situations is that it is only scaling that causes the problem in the current issue, while in the double image issue it was panning as well as scaling.
I hope the above is clear - any insights appreciated.
Look at Group Opacity. iOS 7 has that turned ON by default and this changes the way views/layers are composited:
When the UIViewGroupOpacity key is not present, the default value is
now YES. The default was previously NO.
This means that subviews of a transparent view will first be
composited onto that transparent view, then the precomposited subtree
will be drawn as a whole onto the background. A NO setting results in
less expensive, but also less accurate, compositing: each view in the
transparent subtree is composited onto what’s underneath it, according
to the parent’s opacity, in the normal painter’s algorithm order.
(source: iOS7 Release Notes)
With this setting on, compositing - also during animations - is way more expensive.
Also, have a look at the CoreGraphics Instruments tool to check if you have lots of off-screen images compositing going on.
Are you having any sort of changes going on in the view being animated? That would trigger more discarding of the rendered layer image from the backing store.
I've been trying to animate an image view to slide upwards on the screen. The views height is also increased whilst the position is moved upwards.
On my iPhone 4S (5.x) the image view behaves as expected, the view only moves upwards as its height is increased, however on my iPhone 3G (4.1), the view moves down a little bit during this animation.
Such a level of accuracy is needed as the image view is used to create a non expensive shadow effect. Its alignment is important for the effect. The image is a resizable graphic.
This is how I change the position and size of the view
CGRect oldShadow = self.shaddowView.frame;
oldShadow.size.height = oldShadow.size.height+200;
oldShadow.origin.y = oldShadow.origin.y - 200;
self.shaddowView.frame =oldShadow;
This is how the image for the view is set up as resizable:
UIImage* shadow = [[UIImage imageNamed:#"shadow.png"] stretchableImageWithLeftCapWidth:20 topCapHeight:20];
self.shaddowView.image = shadow;
Thanks.
I used the following animation, with a border around not only the starting position but also the intended final position, so that I could confirm whether any undesired shifting of the view (other than the obvious upward expansion) took place, but it worked fine on iOS 4.2.1:
[UIView animateWithDuration:2.0
animations:^{
CGRect newImageFrame = imageView.frame;
newImageFrame.origin.y -= stretchBy;
newImageFrame.size.height += stretchBy;
imageView.frame = newImageFrame;
}];
I don't have iOS 4.1 device sitting around (my old 3G test phone is running iOS 4.2.1, the latest supported iOS version for that device), so I can't speak to that, but it's fine with iOS 4.2.1.
I have to confess, though, that I find it very unlikely that when you animate the changing of a frame, that the final frame would not be precisely what you specified it to be. If you NSLog the frame when you're done, it is not the correct value? Or are you saying that it momentarily moves during the animation but then ends up in the correct location? Or that it shifts down during animation and ends up in the wrong position even when the animation is done?
I wonder if there's something else going on (e.g. is your animated view a subview of a scroll view, which itself might be shifting? or is there some code not shown here that is accidentally further adjusting the frame after the animation? etc.). Seems like a little debugging should confirm whether the frame is actually not what you intended, or whether there is some other issue going on.
I originally answered suggesting shouldRasterize option, but if you're trying to support old iPhone 3G devices, then maybe that's not good enough. Definitely stutters a little on these old phones. Anyway, this was my original answer:
I assume you're doing this because the layer shadow feature is a little CPU intensive. But I've heard (but can't speak to it) that if you use the shouldRasterize option, it's a little better:
viewThatNeedsShadow.layer.shadowColor = [UIColor blackColor].CGColor;
viewThatNeedsShadow.layer.shadowOffset = CGSizeMake(3.0, 3.0);
viewThatNeedsShadow.layer.shadowOpacity = 0.5;
viewThatNeedsShadow.layer.shouldRasterize = YES;
viewThatNeedsShadow.clipsToBounds = NO;
So I know there's a lot of questions regarding this, but so far as I can tell this is a unique situation so I figured I would post it. Hopefully, this will add some info that may finally give us an answer as to why this is happening to us. I'm getting the error: wait_fences: failed to receive reply: 10004003, when my device rotates. The animation of my views are initiated from:
- (void) willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
I'm only getting the error on the new iPad 3. I've used the exact same program on an original iPad and iPhones as low as a 3GS. They all don't get the wait_fences error and they all rotate faster than the iPad 3 does.
I use Core Graphics almost exclusively to draw the views. I also ensure they're redrawn on a resize so I don't get pixilated views. If I disable the redraw on resize, I don't get this error (but I get stretched views). If I disable core graphics drawing altogether I don't get the error (but, of course, I get black views).
I used the Time Profiler and found out that the hangup was primarily in drawing gradients:
I have altered the code to fill rather than draw gradients and that does alleviate the problem. I would say that the gradients are the problem except I do these animations in other situations (other than in response to rotation) and it works just fine.
I would also like to note that I've paid special attention to making sure I only animate views that are actually on the screen. I know that animating views off screen can sometimes cause this error to occur.
I have not included the animation code
Any ideas as to why this is happening? Especially since it's only happening on the iPad 3?
For those who will ask, this is the code that performs the animation. It will normally be wrapped in a UIView Animation Block.
- (void) setFramesForFocusView:(CustomControl *)focusView atX:(CGFloat)x showInput:(BOOL)showInput{
CGSize bSize = self.bounds.size;
CGRect fRect = focusView.frame;
fRect.size.width = bSize.width;
CGRect iRect;
if (focusView.inputViewIsSystemKeyboard){
if (_keyboardRect.origin.y < 0 || _keyboardRect.origin.y >= CGRectGetMaxY(self.bounds) || CGRectIsEmpty(_keyboardRect) || CGRectGetMaxY(_keyboardRect) > CGRectGetMaxY(self.bounds)) return;
iRect = _keyboardRect;
} else {
iRect = (focusView.inputUIView && showInput) ? CGRectMake(0, bSize.height / 2, bSize.width, bSize.height / 2) : CGRectZero;
}
CGRect iaRect = focusView.inputAccessoryUIView.frame;
CGFloat availableFieldHeight = iRect.origin.y - iaRect.size.height;
iRect.size.width = bSize.width;
iaRect.size.width = bSize.width;
if (!showInput){
iRect.origin.y = bSize.height;
}
iaRect.origin.y = iRect.origin.y - iaRect.size.height;
iRect.origin.x = x;
iaRect.origin.x = x;
focusView.inputUIView.frame = iRect;
focusView.inputAccessoryUIView.frame = iaRect;
if (focusView.expandInput){
fRect.origin.y = 0;
fRect.size.height = availableFieldHeight;
} else {
if (focusView.labelPlacement != LabelPlacementTop && focusView.labelPlacement != LabelPlacementBottom){
fRect.size.height = _currentView.storedFrame.size.height + [focusView.label.text sizeWithFont:focusView.label.font].height;
}
fRect.origin.y = availableFieldHeight - fRect.size.height;
}
if (fRect.size.height > availableFieldHeight){
fRect.origin.y = 0;
fRect.size.height = availableFieldHeight;
}
fRect.origin.x = x;
[focusView setLabelPlacement:LabelPlacementTop toFrame:fRect];
}
Well that was quick. #RobNapier was correct in that it was a timing issue. I commented out my animations and wow there were a lot of other views animating behind there! Even though I was explicitly animating only on-screen views, there was another ViewController receiving the rotation events behind my views without my... uh... knowledge? I mean, I should know right? I wrote the code. I didn't realize at first because my set of views was covering the entire screen. Unfortunately this will require a lot of rewriting. I use Custom Container Controllers and now I see I need to reconsider my implementation. A lot of stuff is getting needlessly rotated/animated. But wow...that answered a lot of performance questions....
Update
So I had thought that the problem I was facing had to do with the extra views being animated by other view controllers. However, while this is technically true, it's not as true as I thought or in the way I thought. I made absolutely sure that no other views were animated by removing the entire root view hierarchy from the window and replacing it with only the view controller I'm wanting to have rotated. This definitely helped, but not completely. Really, it just 'lowered the bar' so that it was less likely for me to get the 'wait_fences' error. I still discovered I was getting the error though in certain situations.
I believe the problem I'm having is my use of a UIScrollView. My implementation has a variable number of of subviews that it manages. The specific view is my own custom implementation of a UIPickerView, so as you can imagine, the number of views it manages can become quite large. I've discovered that if those subviews become too numerous, I start getting the 'wait_fences' error.
So it appears that: If UIScollView is animated, it will animate all of it's subviews, even if those subviews aren't on screen. This is important. I rather suspect that a lot of people who are struggling with this error may not realize this. Each one of those off-screen subviews are pushing you ever closer to hitting the 'wait_fences' error. The solution in my case is "simple": I'm going to convert my UIScrollView to a UITableView. That'll mean rewriting a lot of code, but at least I know that off-screen subviews will be removed from the screen and thus not animated.
I also noted something else: Core-Graphic Gradients hit you hard. I can animate a lot more off-screen views if they don't use gradients. Of course, I love gradients and I'm not willing to give them up (which is why I'm rewriting my PickerView) but it is interesting and important to note.
Update 2
Finished rewriting my UIScrollView as a tableView and that seems to have done the trick. I'm getting no lag and no wait_fences error when I rotate the screen.
Update 2
So yeah, it's a lot easier to hit the wait_fences error on the iPad 3 than any other iPad/iPhone. I've gone through all my code making sure I never animate anything that's not on screen, so that issue's resolved. I still get the wait_fences error on the iPad 3 when I use 'heavy' drawing routines. Stuff I've found that makes me hit it:
Gradients: gradients really make the CPU work on the retina screen.
Transparency: if your view isn't opaque, the CPU works hard to figure out the transparent areas of the view.
Transparent Colors: not the same as view transparency. This is layering transparent colors/gradients over the top of each other to gain an 'effect', like gloss, highlights whatever.
Textures: I find using textures makes it a little more likely to hit the wait_fences error, but nothing near like what gradients/transparency does.
I have several UIButtons on a UIScrollView. I want the buttons to have rounded corners, so I call maskToBounds: on each of them. When I do this and run on the device, the scrolling framerate is pretty bad (it works fine on the simulator). Any ideas on a workaround for this problem?
You're causing the view to be composited offscreen with that call to masksToBounds:, which slows things down quite a bit. Are you rendering custom button images? If so use UIImage -stretchableImageWithLeftCapWidth:topCapHeight: with an image which is the minimum width to encompass it's rounded edges. This allows the GPU to handle stretching the image in the most efficient way possible, while still giving you a button made out of an image. There is a session in the WWDC 2011 videos on Drawing in UIKit - watch that, as it addresses exactly this problem, and a few others you're likely to have.
A few alternative methods:
Tweeties implementation of fast scrolling, by drawing everything manually
Matt Gallaghers implementation of custom drawing. This is the method I use, as it's easy to maintain
I'm trying to recreate the behaviour of the photos app, where you can pan, pinch and rotate simultaneously. I have the basics working, but I'm stuck on something.
For the pan, I offset the centrepoint of the view by the translation amount. This is working well.
For the pinch and rotate I'm applying an affine transform to the view. This is also working well.
The problem is when I pan (ie. move the subview), and then pinch or rotate - the affine transform seems to get applied using the old centre point of the view. I though that it should use the current centre point as the transform origin - as I'm updating the centrepoint when I pan I though that this should work. Instead of a rotation about the centrepoint of the subview, I get a rotational movement about the original centrepoint.
How do I correct this ? It's clearly possible to combine these three gestures intuitively, as the photos app does it successfully.
I tried using an affine translation for the pan, but the effect was the same.
Apple have confirmed this appears to be a bug with the way that gesture recognisers are working in iPhone OS 3.2. I have filed a bug report.