在 UIScrollView 中设置 ZoomScale 动画时出现不需要的滚动

发布于 2024-12-03 01:47:49 字数 2690 浏览 1 评论 0 原文

执行摘要: 有时,UIScrollViewcontentOffset 的值进行不必要的更改,从而导致应用在正在查看的文档中显示错误的位置。这种不需要的更改与滚动视图的 zoomScale 的动画更改同时发生。

详细信息: 我在 UIScrollView 中使用 CATiledLayer 缩小时遇到问题。 CATiledLayer 保存一个 pdf,当 contentOffset 在一定范围内时,当我缩小时,contentOffset 会发生变化(这就是错误)在缩放发生之前。 Apple 代码中的 contentOffset 似乎已更改。

为了说明这个问题,我修改了 Apple 的示例应用程序 ZoomingPDFViewer。代码位于 github 上: https://github.com/DirkMaas/ZoomingPDFViewer-bug

点击使用 animateWithDuration 会导致 zoomScale 更改为 0.5,从而缩小。如果 UIScrollViewcontentOffset.y 小于约 2700 或大于 5900,则 zoomScale 动画效果良好。如果当 contentOffset.y 位于这两个值之间时发生点击,contentOffset.y 将跳转(非动画)到大约 2700,然后 zoomScale< /code> 动画会发生,但滚动会同时发生,因此当动画完成时,contentOffset.y 就在它应该在的位置。但跳跃从哪里来呢?

例如,假设点击屏幕时 contentOffset.y 为 2000:zoomScale 动画效果很好; contentOffset.y 未更改。

但是,如果点击屏幕时 contentOffset.y 为 4000:contentOffset.y 将在没有动画的情况下跳转到大约 2700,然后将从该点同时发生。动画完成后,看起来好像我们从 4000 直接缩放回来,所以我们最终到达了正确的位置,但行为是错误的。

UI 上的注释:

  • 文本可以以正常方式垂直滚动
  • 文本可以通过以正常方式捏合来放大和缩小,
  • 单击将导致 zoomScale 设置为 0.5;更改是动画的

我注意到,如果 zoomScale 大于 0.5,则跳跃不会那么大。另外,如果我使用 setZoomScale:animated: 而不是 animateWithDuration,错误就会消失,但我无法使用它,因为我需要链接动画。

以下是我所做的总结(github 中的代码包括这些更改):

  • 从 Downloaded ZoomingPDFViewer http://developer.apple.com/library/ios/ #samplecode/ZoomingPDFViewer/Introduction/Intro.html 并在 XCode 中打开它
  • 更改了构建设置 |建筑 |最新 iOS (iOS 4.3) 的基础 SDK 更改了构建设置 | GCC 4.2 - 语言 |编译源 对于 Objective-C++,从
  • 项目中删除了 TestPage.pdf,在
  • 项目中添加了“whoiam 5 24cropped 3-2.pdf”,在其位置
  • 添加了 PDFScrollView *scrollView;ZoomingPDFViewerViewController< /code> 类
  • 更改了 ZoomingPDFViewerViewController 中的 loadView 以初始化 scrollView 而不是sv
  • 在 PDFScrollview.m 中的 ZoomingPDFViewerViewController 中添加了 viewDidLoadhandleTapFrom:recognizerzoomOut
  • 注释掉 scrollViewDidEndZooming:withView:atScalescrollViewWillBeginZooming:withView: 因为它们在分散注意力的图像背景

非常感谢您对我的耐心以及所有帮助!

Executive Summary:
At times UIScrollView makes an unwanted change to the value of contentOffset, thus causing the app to display the wrong location in the document being viewed. The unwanted change happens in conjunction to an animated change to the scroll view's zoomScale.

The Details:
I'm having trouble when zooming out with CATiledLayer in a UIScrollView. The CATiledLayer holds a pdf, and when contentOffset is within a certain range, when I zoom out, the contentOffset is changed (that's the bug) before the zooming occurs. The contentOffset seems to be changed in Apple's code.

To illustrate the problem, I modified Apple's sample app, ZoomingPDFViewer. The code is on github: https://github.com/DirkMaas/ZoomingPDFViewer-bug

A tap will cause zoomScale to be changed to 0.5, using animateWithDuration, thus zooming out. If the UIScrollView's contentOffset.y is less than about 2700 or greater than 5900, the zoomScale animation works fine. If the tap happens when contentOffset.y is between those two values, the contentOffset.y will jump (not animated) to about 2700, and then the zoomScale animation will occur, but scrolling will occur at the same time, so that when the animation is done, the contentOffset.y is where it should be. But where does the jump come from?

For example, say the contentOffset.y is 2000 when the screen is tapped: the zoomScale animation works just fine; contentOffset.y is not changed.

But if the contentOffset.y is 4000 when the screen is tapped: the contentOffset.y will jump, without animation, to about 2700, and then zooming and scrolling will begin from that point and occur at the same time. When the animation is done, it looks as if we zoomed straight back from 4000, so we end up in the right place, but the behavior is wrong.

A note on the UI:

  • the text can be scrolled vertically in the normal way
  • the text can be zoomed in and out by pinching in the normal way
  • a single tap will cause the zoomScale to be set to 0.5; the change is animated

I've noticed that if zoomScale is greater than 0.5, the jump is not so big. Also, if I use setZoomScale:animated: instead of animateWithDuration, the bug disappears, but I can't use it because I need to chain animations.

Here is a summary of what I did (the code in github includes these changes):

  • Downloaded ZoomingPDFViewer from
    http://developer.apple.com/library/ios/#samplecode/ZoomingPDFViewer/Introduction/Intro.html and opened it in XCode
  • Changed Build Settings | Architectures | Base SDK to Latest iOS (iOS 4.3) changed Build Settings | GCC 4.2 - Language | Compile Sources As to Objective-C++
  • removed TestPage.pdf from the project
  • added "whoiam 5 24 cropped 3-2.pdf" to the project in its place
  • added PDFScrollView *scrollView; to ZoomingPDFViewerViewController class
  • changed loadView in ZoomingPDFViewerViewController to initialize scrollView instead of sv
  • added viewDidLoad, handleTapFrom:recognizer and zoomOut to ZoomingPDFViewerViewController in PDFScrollview.m
  • commented out scrollViewDidEndZooming:withView:atScale and scrollViewWillBeginZooming:withView: because they do stuff in the image background that distracts from the issue at hand

Thanks so much for bearing with me, and any and all help!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

心房的律动 2024-12-10 01:47:49

关于缩放最难理解的事情之一是它总是发生在称为锚点的点周围。我认为理解它的最好方法是想象一个坐标系叠加在另一个坐标系之上。假设 A 是外部坐标系,B 是内部坐标系(B 将是滚动视图)。当B的偏移量为(0,0)且比例尺为1.0时,则点B(0,0)对应A(0,0),一般情况下B(x,y) = A(x,y) )。

此外,如果 B 的偏移量为 (xOff, yOff),则 B(x,y) = A(x - xOff, y - yOff)。同样,这仍然假设缩放比例为 1.0。

现在,再次让偏移量为 (0,0) 并想象一下当您尝试缩放时会发生什么。屏幕上必须有一个点在缩放时不会移动,而其他所有点都会从该点向外移动。这就是锚点定义的内容。如果您的锚点为 (0,0),则左下角点将保持固定,而所有其他点向上和向右移动。在这种情况下,偏移量保持不变。

如果您的锚点是 (0.5, 0.5)(锚点已标准化,即 0 到 1,因此 0.5 是跨度的一半),则中心点保持固定,而所有其他点向外移动。这意味着偏移量必须改变才能反映这一点。如果在纵向模式下的 iPhone 上并且缩放到比例 2.0,则锚点 x 值将移动屏幕宽度的一半,320/2 = 160。

屏幕上滚动视图内容视图的实际位置由偏移量定义和锚点。因此,如果您只是更改下面的图层锚点而不对偏移进行相应的更改,您将看到视图似乎跳转到不同的位置,即使偏移是相同的。

我猜这是这里的根本问题。当您设置缩放动画时,核心动画必须选择一个新的锚点,以便缩放“看起来”正确。这也将改变偏移量,以便屏幕上的视图实际可见区域不会跳跃。尝试在整个过程中的不同时间记录锚点的位置(它是在您使用视图“层”属性访问的任何视图的底层 CALayer 上定义的)。

另外,请参阅此处的文档 对于漂亮的图片,并且可能比我在这里给出的情况更好地描述:)

最后,这是我在应用程序中使用的一段代码,用于更改图层的锚点,而无需在屏幕上移动。

-(CGPoint)setNewAnchorPointWithoutMoving:(CGPoint)newAnchor {
    CGPoint currentAnchor = CGPointMake(self.anchorPoint.x * self.contentSize.width,
                                        self.anchorPoint.y * self.contentSize.height);

    CGPoint offset = CGPointMake((1 - self.scale) * (currentAnchor.x - newAnchor.x),
                                 (1 - self.scale) * (currentAnchor.y - newAnchor.y));


    self.anchorPoint = CGPointMake(newAnchor.x / self.contentSize.width,
                                   newAnchor.y / self.contentSize.height);

    self.position = CGPointMake(self.position.x + offset.x, self.position.y + offset.y);

    return offset;
}

One of the trickiest things to understand about zooming is that it always happens around a point called the Anchor Point. I think the best way to understand it is to imagine one coordinate system layered on top of another. Say A is your outer coordinate system, B is the inner (B will be the scrollview). When the offset of B is (0,0) and the scale is 1.0, then the point B(0,0) corresponds to A(0,0), and in general B(x,y) = A(x,y).

Further, if the offset of B is (xOff, yOff) then B(x,y) = A(x - xOff, y - yOff). Again, this is still assuming zoom scale is 1.0.

Now, let the offset be (0,0) again and imagine what happens when you try to zoom. There must be a point on the screen that doesn't move when you zoom, and every other point moves outward from that point. That is what the anchor point defines. If your anchor is (0,0) then the bottom left point will remain fixed while all other points move up and to the right. In this case the offset remains the same.

If your anchor point is (0.5, 0.5) (the anchor point is normalized, i.e. 0 to 1, so 0.5 is half way across), then the center point stays fixed while all other points move outward. This means the offset has to change to reflect that. If it's on an iPhone in portrait mode and you zoom to scale 2.0, the anchor point x value will move half the screen width, 320/2 = 160.

The actual position of the scroll views content view on screen is defined by BOTH the offset and the anchor point. So, if you simply change the layers anchor point underneath without making a corresponding change to the offset, you will see the view appear to jump to a different location, even though the offset is the same.

I am guessing that this is the underlying problem here. When you are animating the zoom, Core Animation must be picking a new anchor point so the zooming "looks" right. This will also change the offset so that the views actual visible region on screen doesn't jump. Try logging the location of the anchor point at various times throughout this process (it is defined on the underlying CALayer of any View which you access with a Views "layer" property).

Also, please see the documentation here for pretty pictures and likely a far better description of the situation than I've given here :)

Finally, here is a snippet of code I used in an app to change the anchor point of a layer without it moving on screen.

-(CGPoint)setNewAnchorPointWithoutMoving:(CGPoint)newAnchor {
    CGPoint currentAnchor = CGPointMake(self.anchorPoint.x * self.contentSize.width,
                                        self.anchorPoint.y * self.contentSize.height);

    CGPoint offset = CGPointMake((1 - self.scale) * (currentAnchor.x - newAnchor.x),
                                 (1 - self.scale) * (currentAnchor.y - newAnchor.y));


    self.anchorPoint = CGPointMake(newAnchor.x / self.contentSize.width,
                                   newAnchor.y / self.contentSize.height);

    self.position = CGPointMake(self.position.x + offset.x, self.position.y + offset.y);

    return offset;
}
聚集的泪 2024-12-10 01:47:49

我一直在玩你发布的代码,我想我已经发现发生了什么。当您像代码中那样执行块动画时:

[UIView animateWithDuration:4.0
                 animations:^ {
                     self.scrollView.zoomScale = 0.5;
                 }
                 completion:nil];

动画块实际上在动画开始时被调用。核心动画在内部处理使其看起来像实际在移动的图层。有一个 ,zoomScale 不在其中。

当zoomScale改变时,scrollView会自动更新它的contentOffset。因此,当动画开始时,您看到的跳跃是针对新的 ZoomScale 调整 contentOffset。然后将图层动画化到正确的缩放比例,但目标 contentOffset(即缩放结束时的 contentOffset 应该是什么)已在缩放开始时设置。

这也是为什么缩放看起来像是以屏幕外的某个点为中心的原因。您要么必须使用 setZoomScale:animated: 要么自己为图层和偏移设置动画......

I've been playing with the code you posted and I think I've found what's going on. When you do a block animation like in your code:

[UIView animateWithDuration:4.0
                 animations:^ {
                     self.scrollView.zoomScale = 0.5;
                 }
                 completion:nil];

The animation block actually gets called at the beginning of the animation. Core Animation internally handles the layers that make it look like it's actually moving. There is a list of Animatable Properties on the Dev Center, and zoomScale is not among them.

When the zoomScale changes, the scrollView automatically updates its contentOffset. So when the animation begins, the jump you are seeing is the contentOffset being adjusted for a new zoomScale. The layer is then animated to the proper zoom scale, but the target contentOffset (i.e. what the contentOffset should be at the end of the zooming) is already set at the beginning of the zooming.

That's also why the zooming looks like it is centered around a point off the screen. You are either going to have to use setZoomScale:animated: or else maybe animate the layer and the offset yourself...

姜生凉生 2024-12-10 01:47:49

您应该使用scrollViewDidEndZooming:withView:atScale 来通知您缩放何时完成。否则,我相信您将不得不删除 UIScrollView 属性 ZoomScale 的使用,而是完全手动实现缩放,例如 http://jonathanwatmough.com/2008/12/implementing-tap-to-zoom-in-uiscrollview-on-an-iphone/

You should use scrollViewDidEndZooming:withView:atScale to notify you of when the zoom is complete. Otherwise I believe you will have to remove the use of the property zoomScale of the UIScrollView and instead implement zooming completely manually, e.g. http://jonathanwatmough.com/2008/12/implementing-tap-to-zoom-in-uiscrollview-on-an-iphone/.

屋檐 2024-12-10 01:47:49

当我的 viewForZoomingInScrollView 函数直接返回滚动视图时,我遇到了许多缩放错误。

当我在包含所有内容的scrollView中设置一个视图并将其返回到viewForZoomingInScrollView中后,许多奇怪的行为都消失了。也许这也应该解决您的问题:

-(void) someIntializationFunction {
    [myScrollView addSubView:self.globalContainer];
    // Now had everything in the container and not in the scrollView directly
    ....
}

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

另一件事,我已经看到zoomToRect:函数比更改scrollView的zoomScale更可靠。即使您需要进行更多计算,这也可能有所帮助......

I had many bugs with zooming when my viewForZoomingInScrollView function was returning the scrollview directly.

Many of those strange behaviors disappeared after I set a view inside the scrollView that was containing everything and returning it in viewForZoomingInScrollView. Maybe this should solve your problem too :

-(void) someIntializationFunction {
    [myScrollView addSubView:self.globalContainer];
    // Now had everything in the container and not in the scrollView directly
    ....
}

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

Another thing, I've seen zoomToRect: function much more reliable than changing the zoomScale of the scrollView. This could help too, even if you'll have more calculation to make...

原谅过去的我 2024-12-10 01:47:49

我认为您必须在每个滚动事件上自己操作 contentInset 和/或 contentOffset 。操纵锚点不会有帮助。看看 这个问题 和我的回答。

I think you have to manipulate the contentInset and/or contentOffset yourself, on each scroll event. Manipulating the anchor points will not help. Look at this question and my answer.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文