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:
zoomScale
to be set to 0.5; the change is animatedI'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):
PDFScrollView *scrollView;
to ZoomingPDFViewerViewController
classloadView
in ZoomingPDFViewerViewController
to initialize scrollView
instead of sv
viewDidLoad
, handleTapFrom:recognizer
and zoomOut
to ZoomingPDFViewerViewController
in PDFScrollview.mscrollViewDidEndZooming:withView:atScale
and scrollViewWillBeginZooming:withView:
because they do stuff in the image background that distracts from the issue at handThanks so much for bearing with me, and any and all help!
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;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With