UIScrollView 缩小具有 -ve 原点的视图

Posted

技术标签:

【中文标题】UIScrollView 缩小具有 -ve 原点的视图【英文标题】:UIScrollView zooming out of a view with a -ve origin 【发布时间】:2013-05-03 14:52:26 【问题描述】:

我有一个 UIScrollView。在这个我有一个 UIView 有一个负原点的框架 - 我需要限制滚动视图,这样你就不能滚动整个视图..

我已经在这个滚动视图中实现了缩放。

缩放时,滚动视图将根据比例调整可缩放视图的大小。但它不会调整原点。

所以如果我有一个框架为 0, -500, 1000, 1000 的视图

我缩小到 0.5 的比例,这将给我一个 0, -500, 500, 500

的新框架

显然这不好,整个视图被缩小到滚动视图之外。我希望框架为 0, -250, 500, 500

我可以通过正确调整原点在 scrollViewDidZoom 方法中修复一些问题。这确实有效,但缩放不平滑。在此处更改原点会导致它跳转。

我在 UIView 的文档中注意到它说(关于 frame 属性):

警告:如果变换属性不是恒等变换,则 此属性的值未定义,因此应忽略。

不太清楚为什么会这样。

我处理这个问题的方法有误吗?修复它的最佳方法是什么?

谢谢


以下是我正在使用的测试应用的一些源代码:

在 ViewController..

- (void)viewDidLoad

    [super viewDidLoad];
    self.bigView = [[BigView alloc] initWithFrame: CGRectMake(0, -400, 1000, 1000)];

    [self.bigScroll addSubview: bigView];
    self.bigScroll.delegate = self;
    self.bigScroll.minimumZoomScale = 0.2;
    self.bigScroll.maximumZoomScale = 5;
    self.bigScroll.contentSize = bigView.bounds.size;


-(UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView  
    return bigView;


- (void)scrollViewDidZoom:(UIScrollView *)scrollView     
//    bigView.frame = CGRectMake(0, -400 * scrollView.zoomScale,
//                               bigView.frame.size.width, bigView.frame.size.height);

    bigView.center = CGPointMake(500 * scrollView.zoomScale, 100 * scrollView.zoomScale);

然后在视图中...

- (void)drawRect:(CGRect)rect

    // Drawing code
    CGContextRef ctx = UIGraphicsGetCurrentContext();

    CGContextSetFillColorWithColor(ctx, [UIColor whiteColor].CGColor);
    CGContextSetStrokeColorWithColor(ctx, [UIColor whiteColor].CGColor);
    CGContextFillRect(ctx, CGRectMake(100, 500, 10, 10));

    for (int i = 0; i < 1000; i += 100) 
        CGContextStrokeRect(ctx, CGRectMake(0, i, 1000, 3));        
    

请注意,这里的跳跃性在较大的缩放比例下更加明显。在我的真实应用程序中,有更多的绘图和处理在跳转,在任何时候都更加明显。

【问题讨论】:

关于变换和框架,文档太模糊了。 Frame 本质上是从边界、中心和应用变换(以及便利设置器)派生的。 frame.size 将通过转换矩阵应用的水平和垂直缩放比例缩放bounds.size(因此对于大小为 150x100 的视图,矩阵来自 CGAffineTransformMakeScale(3.0, 5.0)frame.size 将是 450x500)。对于旋转视图,frame.size 将是可以容纳旋转(和缩放)边界的最小直立矩形的大小,以便视图边界的角接触框架的边。 你是在模拟器还是在设备上测试? 我已经成功复制了您的生涩缩放。我最初的建议并没有解决这个问题。我没有看到它,因为它与偏移的大小有关,我正在测试一个较小的 100 点偏移,它是平滑的。我已经使用CAShapeLayer 制定了一个新的解决方案。请参阅我的更新答案。 感谢您的慷慨 - 这就是让我们解谜者继续前进的原因;-j 【参考方案1】:

您不必使用 frame 属性——也不应该使用,因为 Apple 发出了非常坚定的警告。在这种情况下,您通常可以使用boundscenter 来实现您的结果。

在您的情况下,您可以忽略所有子视图的属性。假设您的子视图是viewForZoomingInScrollView,您可以使用滚动视图的contentOffsetzoomScale 属性

- (void) setMinOffsets:(UIScrollView*)scrollView
    
        CGFloat minOffsetX = MIN_OFFSET_X*scrollView.zoomScale;
        CGFloat minOffsetY = MIN_OFFSET_Y*scrollView.zoomScale;

        if ( scrollView.contentOffset.x < minOffsetX
          || scrollView.contentOffset.y < minOffsetY ) 

            CGFloat offsetX = (scrollView.contentOffset.x > minOffsetX)?
                               scrollView.contentOffset.x : minOffsetX;

            CGFloat offsetY = (scrollView.contentOffset.y > minOffsetY)?
                               scrollView.contentOffset.y : minOffsetY;

            scrollView.contentOffset = CGPointMake(offsetX, offsetY);
        
    

从您的 scrollView 委托中的 scrollViewDidScrollscrollViewDidZoom 调用它。这应该可以顺利进行,但如果您有疑问,您也可以通过子类化 scrollView 并使用layoutSubviews 调用它来实现它。在他们的 PhotoScroller 示例中,Apple 通过覆盖 layoutSubviews 将滚动视图的内容居中 - 尽管令人抓狂的是,他们忽略了自己的警告并调整了子视图的 frame 属性来实现这一点。

更新

上述方法消除了滚动视图达到其限制时的“反弹”。如果你想保留反弹,你可以直接改变视图的中心属性:

- (void) setViewCenter:(UIScrollView*)scrollView
    
        UIView* view = [scrollView subviews][0];
        CGFloat centerX = view.bounds.size.width/2-MIN_OFFSET_X;
        CGFloat centerY = view.bounds.size.height/2-MIN_OFFSET_Y;

        centerX *=scrollView.zoomScale;
        centerY *=scrollView.zoomScale;

        view.center = CGPointMake(centerX, centerY);
    

更新 2

从您更新的问题(带有代码)中,我可以看到这些解决方案都不能解决您的问题。似乎正在发生的事情是,您的偏移量越大,变焦运动变得越剧烈。 100点的偏移量还是比较流畅的,但是500点的偏移量就粗糙到无法接受了。这部分与您的 drawRect 例程有关,部分与 scrollView 中进行的(太多)重新计算以显示正确的内容有关。所以我有另一个解决方案……

在您的 viewController 中,将您的 customView 的边界/框架原点设置为正常 (0,0)。我们将使用图层来抵消内容。您需要将 QuartzCore 框架添加到您的项目中,并将其#import 到您的自定义视图中。

在自定义视图中初始化两个 CAShapeLayers - 一个用于盒子,另一个用于线条。如果它们共享相同的填充和描边,则您只需要一个 CAShapeLayer(在此示例中,我更改了填充和描边颜色)。每个 CAShapeLayer 都有自己的 CGContext,您可以使用颜色、线宽等对每个图层进行一次初始化。然后要让 CAShapelayer 进行绘制,您所要做的就是将其设置为带有 CGPath 的 path 属性。

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

@interface CustomView()
@property (nonatomic, strong) CAShapeLayer* shapeLayer1;
@property (nonatomic, strong) CAShapeLayer* shapeLayer2;
@end

@implementation CustomView

    #define MIN_OFFSET_X 100
    #define MIN_OFFSET_Y 500

- (id)initWithFrame:(CGRect)frame

    self = [super initWithFrame:frame];
    if (self) 
        [self initialiseLayers];
    
    return self;



- (void) initialiseLayers

    CGRect layerBounds  = CGRectMake( MIN_OFFSET_X,MIN_OFFSET_Y
                          , self.bounds.size.width + MIN_OFFSET_X
                          , self.bounds.size.height+ MIN_OFFSET_Y);

    self.shapeLayer1 = [[CAShapeLayer alloc] init];
    [self.shapeLayer1 setFillColor:[UIColor clearColor].CGColor];
    [self.shapeLayer1 setStrokeColor:[UIColor yellowColor].CGColor];
    [self.shapeLayer1 setLineWidth:1.0f];
    [self.shapeLayer1 setOpacity:1.0f];

    self.shapeLayer1.anchorPoint = CGPointMake(0, 0);
    self.shapeLayer1.bounds = layerBounds;
    [self.layer addSublayer:self.shapeLayer1];

设置界限是关键。与裁剪其子视图的视图不同,CALayers 将绘制超出其超层的边界。您将开始在视图顶部上方绘制MIN_OFFSET_Y 点,在左侧绘制MIN_OFFSET_X。这允许您在滚动视图的内容视图之外绘制内容,而无需滚动视图做任何额外的工作。

与视图不同,超层不会自动剪切位于其边界矩形之外的子层的内容。相反,默认情况下,超级图层允许其子图层完整显示。 (Apple Docs, Building a Layer Hierarchy)

    self.shapeLayer2 = [[CAShapeLayer alloc] init];

    [self.shapeLayer2 setFillColor:[UIColor blueColor].CGColor];
    [self.shapeLayer2 setStrokeColor:[UIColor clearColor].CGColor];
    [self.shapeLayer2 setLineWidth:0.0f];
    [self.shapeLayer2 setOpacity:1.0f];

    self.shapeLayer2.anchorPoint = CGPointMake(0, 0);
    self.shapeLayer2.bounds = layerBounds;
    [self.layer addSublayer:self.shapeLayer2];

    [self drawIntoLayer1];
    [self drawIntoLayer2];

为每个形状层设置一个贝塞尔路径,然后传入:

- (void) drawIntoLayer1 

    UIBezierPath* path = [[UIBezierPath alloc] init];
    [path moveToPoint:CGPointMake(0,0)];

    for (int i = 0; i < self.bounds.size.height+MIN_OFFSET_Y; i += 100) 
        [path moveToPoint:
                CGPointMake(0,i)];
        [path addLineToPoint:
                CGPointMake(self.bounds.size.width+MIN_OFFSET_X, i)];
        [path addLineToPoint:
                CGPointMake(self.bounds.size.width+MIN_OFFSET_X, i+3)];
        [path addLineToPoint:
                CGPointMake(0, i+3)];
        [path closePath];
    

    [self.shapeLayer1 setPath:path.CGPath];


- (void) drawIntoLayer2 
    UIBezierPath* path = [UIBezierPath bezierPathWithRect:
            CGRectMake(100+MIN_OFFSET_X, MIN_OFFSET_Y, 10, 10)];
    [self.shapeLayer2 setPath:path.CGPath];

这消除了对drawRect 的需要 - 如果您更改路径属性,您只需要重新绘制图层。即使您确实像调用 drawRect 那样频繁地更改路径属性,绘图现在也应该显着提高效率。由于path 是一个动画属性,如果需要,您还可以免费获得动画。

在您的情况下,我们只需要设置一次路径,因此所有工作在初始化时完成一次。

现在您可以从您的 scrollView 委托方法中删除任何居中代码,不再需要它。

【讨论】:

这不会阻止您通过滚动超出范围获得的“反弹”吗? @MongusPong - 是的。如果您想保持反弹,请改为更改视图的中心属性(请参阅我的更新) 试试这个吧。设置中心与设置框架的效果完全相同。缩放时视图会上下跳动,不流畅。 @MongusPong - 它对我来说很好,我会看看可能是什么原因 - 也许你可以更新你的问题以显示你的代码,以防有其他东西正在制作查看跳转? 谢谢,我需要一点时间来消化这个。初步测试看起来很有希望......希望今晚晚些时候能够敲定它。

以上是关于UIScrollView 缩小具有 -ve 原点的视图的主要内容,如果未能解决你的问题,请参考以下文章

UIScrollview 在缩放时同步子视图原点

缩小 UIScrollView

在不使用捏/UIScrollView 的情况下缩小照片

使用 UIScrollView 进行放大和缩小视图

放大时只允许 UIScrollView 缩放弹跳,不能缩小

缩小放置在 UIScrollView 中的 UIImageView