1

Note to casual readers: Despite the title, this question has nothing to do with the UIScrollView properties bounces (scrolling related) or bouncesZoom.

I am using UIScrollView to add zooming to a custom view. The custom view uses sublayers to draw its content. Each sublayer is a CALayer instance that is added to the view's main layer with [CALayer addSublayer:]. Sublayers use CoreGraphics to render their content.

After each zoom completes, the custom view needs to redraw its content at the new zoom scale so that the content appears crisp and sharp again. I am currently trying to get the approach to work that is shown in this SO question, i.e. I reset the scroll view's zoomScale property to 1.0 after each zoom operation, then I adjust the minimumZoomScale and maximumZoomScale properties so that the user cannot zoom in/out more than originally intended.

The content redrawing already works correctly (!), but what I am missing is a smooth GUI update so that the zoomed content is redrawn in place without appearing to move. With my current solution (code example follows at bottom of this question), I observe a kind of "bounce" effect: As soon as the zoom operation ends, the zoomed content briefly moves to a different location, then immediately moves back to its original location.

I am not entirely sure what the reason for the "bounce" effect is: Either there are two GUI update cycles (one for resetting zoomScale to 1.0, and another for setNeedsDisplay), or some sort of animation is taking place that makes both changes visible, one after the other.

My question is: How can I prevent the "bounce" effect described above?

UPDATE: The following is a minimal but complete code example that you can simply copy&paste to observe the effect that I am talking about.

  1. Create a new Xcode project using the "Empty application" template.
  2. Add the code below to AppDelegate.h and AppDelegate.m, respectively.
  3. In the project's Link build phase, add a reference to QuartzCore.framework.

Stuff that goes into AppDelegate.h:

#import <UIKit/UIKit.h>

@class LayerView;

@interface AppDelegate : UIResponder <UIApplicationDelegate, UIScrollViewDelegate>
@property (nonatomic, retain) UIWindow* window;
@property (nonatomic, retain) LayerView* layerView;
@end

Stuff that goes into AppDelegate.m:

#import "AppDelegate.h"
#import <QuartzCore/QuartzCore.h>

@class LayerDelegate;

@interface LayerView : UIView
@property (nonatomic, retain) LayerDelegate* layerDelegate;
@end

@interface LayerDelegate : NSObject
@property(nonatomic, retain) CALayer* layer;
@property (nonatomic, assign) CGFloat zoomScale;
@end

static CGFloat kMinimumZoomScale = 1.0;
static CGFloat kMaximumZoomScale = 5.0;

@implementation AppDelegate

- (void) dealloc
{
  self.window = nil;
  self.layerView = nil;
  [super dealloc];
}

- (BOOL) application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{
  [UIApplication sharedApplication].statusBarHidden = YES;
  self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];
  self.window.backgroundColor = [UIColor whiteColor];

  UIScrollView* scrollView = [[[UIScrollView alloc] initWithFrame:self.window.bounds] autorelease];
  [self.window addSubview:scrollView];
  scrollView.contentSize = scrollView.bounds.size;
  scrollView.delegate = self;
  scrollView.minimumZoomScale = kMinimumZoomScale;
  scrollView.maximumZoomScale = kMaximumZoomScale;
  scrollView.zoomScale = 1.0f;
  scrollView.bouncesZoom = NO;

  self.layerView = [[[LayerView alloc] initWithFrame:scrollView.bounds] autorelease];
  [scrollView addSubview:self.layerView];

  [self.window makeKeyAndVisible];
  return YES;
}

- (UIView*) viewForZoomingInScrollView:(UIScrollView*)scrollView
{
  return self.layerView;
}

- (void) scrollViewDidEndZooming:(UIScrollView*)scrollView withView:(UIView*)view atScale:(float)scale
{
  CGPoint contentOffset = scrollView.contentOffset;
  CGSize contentSize = scrollView.contentSize;

  scrollView.maximumZoomScale = scrollView.maximumZoomScale / scale;
  scrollView.minimumZoomScale = scrollView.minimumZoomScale / scale;
  // Big change here: This resets the scroll view's contentSize and
  // contentOffset, and also the LayerView's frame, bounds and transform
  // properties
  scrollView.zoomScale = 1.0f;

  CGFloat newZoomScale = self.layerView.layerDelegate.zoomScale * scale;
  self.layerView.layerDelegate.zoomScale = newZoomScale;

  self.layerView.frame = CGRectMake(0, 0, contentSize.width, contentSize.height);
  scrollView.contentSize = contentSize;
  [scrollView setContentOffset:contentOffset animated:NO];

  [self.layerView setNeedsDisplay];
}

@end

@implementation LayerView

- (id) initWithFrame:(CGRect)frame
{
  self = [super initWithFrame:frame];
  if (self)
  {
    self.layerDelegate = [[[LayerDelegate alloc] init] autorelease];
    [self.layer addSublayer:self.layerDelegate.layer];
    // super's initWithFrame already invoked setNeedsDisplay, but we need to
    // repeat because at that time our layerDelegate property was still empty
    [self setNeedsDisplay];

  }
  return self;
}

- (void) dealloc
{
  self.layerDelegate = nil;
  [super dealloc];
}

- (void) setNeedsDisplay
{
  [super setNeedsDisplay];
  // Zooming changes the view's frame, but not the frame of the layer
  self.layerDelegate.layer.frame = self.bounds;
  [self.layerDelegate.layer setNeedsDisplay];
}

@end

@implementation LayerDelegate

- (id) init
{
  self = [super init];
  if (self)
  {
    self.layer = [CALayer layer];
    self.layer.delegate = self;
    self.zoomScale = 1.0f;
  }
  return self;
}

- (void) dealloc
{
  self.layer = nil;
  [super dealloc];
}

- (void) drawLayer:(CALayer*)layer inContext:(CGContextRef)context
{
  CGRect layerRect = self.layer.bounds;
  CGFloat radius = 25 * self.zoomScale;
  CGFloat centerDistanceFromEdge = 5 * self.zoomScale + radius;

  CGPoint topLeftCenter = CGPointMake(CGRectGetMinX(layerRect) + centerDistanceFromEdge,
                                      CGRectGetMinY(layerRect) + centerDistanceFromEdge);
  [self drawCircleWithCenter:topLeftCenter radius:radius fillColor:[UIColor redColor] inContext:context];

  CGPoint layerCenter = CGPointMake(CGRectGetMidX(layerRect), CGRectGetMidY(layerRect));
  [self drawCircleWithCenter:layerCenter radius:radius fillColor:[UIColor greenColor] inContext:context];

  CGPoint bottomRightCenter = CGPointMake(CGRectGetMaxX(layerRect) - centerDistanceFromEdge,
                                          CGRectGetMaxY(layerRect) - centerDistanceFromEdge);
  [self drawCircleWithCenter:bottomRightCenter radius:radius fillColor:[UIColor blueColor] inContext:context];
}

- (void) drawCircleWithCenter:(CGPoint)center
                       radius:(CGFloat)radius
                    fillColor:(UIColor*)color
                    inContext:(CGContextRef)context
{
  const int startRadius = [self radians:0];
  const int endRadius = [self radians:360];
  const int clockwise = 0;
  CGContextAddArc(context, center.x, center.y, radius,
                  startRadius, endRadius, clockwise);
  CGContextSetFillColorWithColor(context, color.CGColor);
  CGContextFillPath(context);
}

- (double) radians:(double)degrees
{
  return degrees * M_PI / 180;
}

@end
Community
  • 1
  • 1
herzbube
  • 13,158
  • 9
  • 45
  • 87

2 Answers2

2

Based on your sample project, the key is that you're manipulating a CALayer directly. By default, setting CALayer properties, such as frame, cause animations. The suggestion to use [UIView setAnimationsEnabled:NO] was on the right track, but only affects UIView-based animations. If you do the CALayer equivalent, say in your setNeedsDisplay: method:

[CATransaction begin];
[CATransaction setDisableActions:YES];
self.layerDelegate.layer.frame = self.bounds;
[CATransaction commit];

It prevents the implicit frame-changing animation and looks right to me. You can also disable these implicit animations via a CALayerDelegate method in your LayerDelegate class:

- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
    return (id)[NSNull null]; // NSNull means "don't do any implicit animations"
}

Original Suggestions:

Perhaps you are in an animation block without knowing it? Or, perhaps one of the methods you're calling is setting up an animation block? What if you [UIView setAnimationsEnabled:NO] before your code and re-enable them after?

If it's not an animation, then it's probably as you suspect; two view updates of some kind. (Perhaps one from the scroll view, and one from your code somehow?) Some runnable sample code would be great in that case.

(Out of curiosity, have you tried using CALayer's shouldRasterize and rasterizationScale rather than faking out the zoom level?)

Jesse Rusak
  • 56,530
  • 12
  • 101
  • 102
  • I've tried invoking `[UIView setAnimationsEnabled:NO]` once in the scroll view delegate's `scrollViewDidEndZooming`, and for good measure also in the app delegate's `didFinishLaunchingWithOptions`. Alas, without success. Regarding rasterization: No, I haven't tried those properties you mention, I was not aware of them. After reading over their description, I must admit that I am at a loss how to use them for redrawing after a zoom - but I will try to find out, I am not at all fixated on the reset-zoom-scale approach. Finally, I will fix up a minimal but complete code example ASAP. – herzbube Mar 12 '13 at 20:55
  • If you find the time to have another look, I have now added the full code example to the question. – herzbube Mar 12 '13 at 23:06
  • @herzbube Awesome, looking now. – Jesse Rusak Mar 12 '13 at 23:52
  • @herzbube Sorry, should have poked you. I updated my answer based on your new code. – Jesse Rusak Mar 13 '13 at 11:22
  • Thank you so much, a pity I can upvote only once! Do you mind if I mention your name in my app's credits screen? – herzbube Mar 13 '13 at 19:05
  • @herzbube Glad it worked for you. I'd be flattered if you included my name in the credits, but you certainly shouldn't feel like you need to! – Jesse Rusak Mar 13 '13 at 19:54
  • With those 3 lines of code - simple as they might look to someone with iOS drawing/animation experience - you have ended 4 days of misery. I was already close to giving up, so I think it is only fair that the users of my app should know whom they can thank that they get pinch gesture zooming! – herzbube Mar 13 '13 at 22:18
-2

In the X Code user interface builder there's a Bounce setting (it's under Scroll View).

Andrew
  • 11,894
  • 12
  • 69
  • 85
  • Nope, that's not it, the question has nothing to do with the `UIScrollView` property `bounces` (scrolling related). And to forestall other questions: It also has nothing to do with `bouncesZoom`. – herzbube Mar 12 '13 at 20:06