如何在缩放后重绘自定义视图时防止“反弹”效果?

时间:2013-03-12 19:51:56

标签: ios uiscrollview zoom

对随意读者的注意事项:尽管有标题,但此问题与UIScrollView属性bounces(滚动相关)或bouncesZoom无关。 / p>

我正在使用UIScrollView将缩放添加到自定义视图。自定义视图使用子图层来绘制其内容。每个子图层都是CALayer个实例,使用[CALayer addSublayer:]添加到视图的主图层。子图层使用CoreGraphics来呈现其内容。

每次缩放完成后,自定义视图需要以新的缩放比例重绘其内容,以便内容再次显得清晰锐利。我目前正试图获得 this SO question中显示的工作方法,即我在每次缩放操作后将滚动视图的zoomScale属性重置为1.0,然后调整minimumZoomScalemaximumZoomScale属性,以便用户无法放大/缩小超出预期的范围。

内容重绘已经正常工作(!),但我缺少的是一个流畅的GUI更新,以便缩放内容重新绘制到位而不会移动。使用我当前的解决方案(代码示例在此问题的底部),我观察到一种“反弹”效果:一旦缩放操作结束,缩放的内容会短暂移动到不同的位置,然后立即移回其原始状态位置。

我不完全确定“反弹”效果的原因是:有两个GUI更新周期(一个用于将zoomScale重置为1.0,另一个用于setNeedsDisplay),或者一些正在发生一种动画,使得两种变化一个接一个地可见。

我的问题是:如何防止上述“反弹”效应?

更新:以下是一个最小但完整代码示例,您只需复制和粘贴即可观察我正在谈论的效果。

  1. 使用“空应用程序”模板创建一个新的Xcode项目。
  2. 分别将以下代码添加到AppDelegate.hAppDelegate.m
  3. 在项目的链接构建阶段,添加对QuartzCore.framework
  4. 的引用

    进入AppDelegate.h的东西:

    #import <UIKit/UIKit.h>
    
    @class LayerView;
    
    @interface AppDelegate : UIResponder <UIApplicationDelegate, UIScrollViewDelegate>
    @property (nonatomic, retain) UIWindow* window;
    @property (nonatomic, retain) LayerView* layerView;
    @end
    

    进入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
    

2 个答案:

答案 0 :(得分:2)

根据您的示例项目,关键是您正在直接操作CALayer。默认情况下,设置CALayer属性(例如帧)会导致动画。使用[UIView setAnimationsEnabled:NO]的建议是在正确的轨道上,但只影响基于UIView的动画。如果你使用CALayer等效,请在你的setNeedsDisplay:方法中说:

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

它可以防止隐式的帧变换动画,并且看起来对我而言。您还可以通过LayerDelegate类中的CALayerDelegate方法禁用这些隐式动画:

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

原始建议:

也许你在不知情的情况下进入动画块?或者,您可能正在调用的方法之一是设置动画块?如果您在代码之前[UIView setAnimationsEnabled:NO]并在之后重新启用它们会怎样?

如果它不是动画,那么它可能就像你怀疑的那样;两种视图更新。 (也许一个来自滚动视图,一个来自你的代码?)在这种情况下,一些可运行的示例代码会很棒。

(出于好奇,您是否尝试过使用CALayer的shouldRasterize和rasterizationScale而不是伪造缩放级别?)

答案 1 :(得分:-2)

在X Code用户界面构建器中,有Bounce设置(位于Scroll View下)。