问题 UIScrollView使用-ve origin缩小视图


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

我在这个scrollview中实现了Zoom。

缩放滚动视图时,将根据比例调整Zoomable视图的大小。但它没有调整原点。

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

我缩小到0.5的比例,这将给我一个新的框架 {0,-500},{500,500}

显然这不好,整个视图都缩小了滚动视图。我想要框架 {0,-250},{500,500}

我可以通过正确调整原点来解决scrollViewDidZoom方法中的问题。这确实有效,但缩放不顺畅。在此处更改原点会导致它跳转。

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

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

不太清楚为什么会这样。

我接近这个问题了吗?修复它的最佳方法是什么?

谢谢


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

在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));        
    }
}

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


11694
2018-05-03 14:52


起源

关于变换和框架,文档太模糊了。帧基本上来自边界,中心和应用变换(以及便利设置器)。 frame.size 将会 bounds.size 通过变换矩阵应用的水平和垂直缩放比例缩放(因此对于大小为150x100的视图,矩阵来自 CGAffineTransformMakeScale(3.0, 5.0), frame.size 将是450x500)。对于旋转视图, frame.size 将是可以保持旋转(和缩放)边界的最小直立矩形的大小,以便视图边界的角接触框架的边。 - fzwo
你在模拟器或设备上进行测试吗? - foundry
我设法复制你的生涩缩放。我原来的建议没有解决这个问题。我没有看到它,因为它是相对于偏移的大小而我正在测试一个较小的100磅偏移,这是平滑的。我使用了一个新的解决方案 CAShapeLayer。看到我更新的答案。 - foundry
感谢您的慷慨 - 这就是让我们解决问题的原因; -j - foundry


答案:


鉴于Apple非常坚定的警告,您不必使用框架属性 - 也不应该使用框架属性。在这种情况下,您通常可以使用 bounds 和 center 实现你的结果。

在您的情况下,您可以忽略所有子视图的属性。假设您的子视图是 viewForZoomingInScrollView  你可以使用scrollView的 contentOffset 和 zoomScale 性能

- (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);
        }
    }

从两者中调用它 scrollViewDidScroll 和 scrollViewDidZoom 在您的scrollView委托中。这应该可以顺利运行,但如果您有疑问,也可以通过子类化scrollView并使用它来调用它来实现它 layoutSubviews。在他们的PhotoScroller示例中,Apple通过覆盖来居中scrollView的内容 layoutSubviews  - 尽管他们疯狂地忽略了他们自己的警告并调整了子视图的框架属性来实现这一点。

更新

上面的方法消除了'反弹',因为scrollView达到了它的极限。如果要保留反弹,可以直接更改视图的中心属性:

- (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的bounds / frame origin设置为normal(0,0)。我们将使用图层来替代内容。您需要将QuartzCore框架添加到项目中,然后#import到您的自定义视图中。

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

#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 往左边。这允许您在scrollView的内容视图之外绘制内容,而scrollView不必执行任何额外的工作。

与视图不同,超级图层不会自动剪切位于其边界矩形之外的子图层的内容。相反,超级层允许其子层默认显示为完整
  (Apple Docs,构建层次结构

    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委托方法中删除任何居中代码,不再需要它。


10
2018-05-03 21:45



这不会阻止你通过滚动超出范围获得的“反弹”吗? - Mongus Pong
@MongusPong - 是的。如果你想保持反弹,改为改变视图的中心属性(参见我的更新) - foundry
试着尝试这个。设置中心与设置框架具有完全相同的效果。视图在缩放时上下跳动,不平滑。 - Mongus Pong
@MongusPong - 它对我来说很好,我会看一看,看看可能是什么原因 - 也许你可以更新你的问题以显示你的代码,以防有什么东西让视图跳转? - foundry
在出错的地方添加了一些代码。 - Mongus Pong