Unwanted scrolling when animating zoomScale in UIScrollView

此生再无相见时 提交于 2019-12-18 15:55:25

问题


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!


回答1:


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



回答2:


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...




回答3:


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/.




回答4:


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...




回答5:


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.



来源:https://stackoverflow.com/questions/7301744/unwanted-scrolling-when-animating-zoomscale-in-uiscrollview

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!