Нежелательная прокрутка при анимации zoomScale в UIScrollView

Резюме: иногда UIScrollView вносит нежелательные изменения в значение contentOffset, в результате чего приложение отображает неправильное место в просматриваемом документе. Нежелательное изменение происходит в сочетании с анимированным изменением zoomScale представления прокрутки.

Подробности. У меня возникают проблемы при уменьшении масштаба с помощью CATiledLayer в UIScrollView. CATiledLayer содержит PDF-файл, и когда contentOffset находится в определенном диапазоне, когда я уменьшаю масштаб, contentOffset изменяется (это ошибка) до того, как происходит масштабирование. Кажется, что contentOffset изменено в коде Apple.

Чтобы проиллюстрировать проблему, я изменил пример приложения Apple, ZoomingPDFViewer. Код находится на github: https://github.com/DirkMaas/ZoomingPDFViewer-bug

Нажатие приведет к изменению zoomScale на 0,5 с помощью animateWithDuration, что приведет к уменьшению масштаба. Если contentOffset.y UIScrollView меньше 2700 или больше 5900, анимация zoomScale работает нормально. Если касание происходит, когда contentOffset.y находится между этими двумя значениями, contentOffset.y перепрыгнет (не анимируется) примерно до 2700, а затем произойдет zoomScale анимация, но в то же время будет происходить прокрутка, так что, когда анимация будет выполнена, contentOffset.y там, где должно быть. Но откуда скачок?

Например, скажем, contentOffset.y равно 2000 при касании экрана: анимация zoomScale работает просто отлично; contentOffset.y не изменяется.

Но если contentOffset.y равно 4000 при нажатии на экран: contentOffset.y подскочит без анимации примерно до 2700, а затем масштабирование и прокрутка начнутся с этой точки и будут происходить одновременно. Когда анимация завершена, кажется, что мы увеличили масштаб прямо с 4000, поэтому мы оказываемся в нужном месте, но поведение неправильное.

Примечание по пользовательскому интерфейсу:

  • текст можно прокручивать по вертикали обычным способом
  • текст можно увеличивать и уменьшать, сжимая обычным способом
  • одно касание приведет к тому, что zoomScale будет установлено на 0,5; изменение анимировано

Я заметил, что если zoomScale больше 0,5, скачок не такой большой. Кроме того, если я использую setZoomScale:animated: вместо animateWithDuration, ошибка исчезает, но я не могу использовать ее, потому что мне нужно цепочку анимаций.

Вот краткое изложение того, что я сделал (код в github включает эти изменения):

  • Загружен ZoomingPDFViewer с http://developer.apple.com/library/ios/#samplecode/ZoomingPDFViewer/Introduction/Intro.html и открыл его в XCode.
  • Изменены настройки сборки | Архитектуры | Базовый SDK для последней версии iOS (iOS 4.3) изменил настройки сборки | GCC 4.2 - Язык | Скомпилируйте исходники в соответствии с Objective-C++
  • удален TestPage.pdf из проекта
  • добавил в проект "whoiam 5 24 cropped 3-2.pdf" на свое место
  • добавлен PDFScrollView *scrollView; в ZoomingPDFViewerViewController класс
  • изменил loadView в ZoomingPDFViewerViewController для инициализации scrollView вместо sv
  • добавлены viewDidLoad, handleTapFrom:recognizer и zoomOut к ZoomingPDFViewerViewController в PDFScrollview.m
  • закомментированы scrollViewDidEndZooming:withView:atScale и scrollViewWillBeginZooming:withView:, потому что они отвлекают внимание от обсуждаемой проблемы на фоне изображения.

Большое спасибо за то, что терпите меня, и за любую помощь!


person DirkMaas    schedule 04.09.2011    source источник
comment
Что вы имеете в виду, когда говорите, что вам нужно связать анимацию? Почему вы не можете использовать scrollViewDidEndZooming:withView:atScale для уведомления о завершении масштабирования?   -  person Michael Frederick    schedule 14.09.2011
comment
@Майкл Фредерик - хорошая мысль. Это обходной путь, но мне нравится использовать animateWithDuration: вместо setZoomScale:Animated:, потому что, если я что-то не упустил, последний не позволяет контролировать продолжительность, и потому что блочные анимации, на мой взгляд, лучше справляются с сохранением связанного кода. вместе. Кроме того, Apple, похоже, поощряет использование блочной анимации. Так что, возможно, мне следовало сказать, что я не хочу использовать setZoomScale:Animated:. Спасибо за комментарий!   -  person DirkMaas    schedule 14.09.2011


Ответы (5)


Одна из самых сложных вещей, которые нужно понять о масштабировании, заключается в том, что оно всегда происходит вокруг точки, называемой опорной точкой. Я думаю, что лучший способ понять это — представить одну систему координат, наложенную поверх другой. Скажем, 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.

Фактическое положение представления содержимого прокрутки на экране определяется ОБА смещением и точкой привязки. Таким образом, если вы просто измените точку привязки слоев внизу, не внося соответствующих изменений в смещение, вы увидите, что вид перескакивает в другое место, даже если смещение остается тем же.

Я предполагаю, что это основная проблема здесь. Когда вы анимируете масштабирование, Core Animation должна выбирать новую опорную точку, чтобы масштабирование «выглядело» правильно. Это также изменит смещение, чтобы фактическая видимая область просмотра на экране не прыгала. Попробуйте зарегистрировать местоположение точки привязки в разное время на протяжении всего этого процесса (оно определяется в базовом 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;
}
person devdavid    schedule 14.09.2011
comment
Спасибо, но я не думаю, что это все. Код не пытается изменить значение anchorpoint прямо или косвенно, и я записал его до и после масштабирования, и оба раза это было значение по умолчанию (0,5, 0,5). Кроме того, по своей природе anchorPoint всегда находится на экране, видимо, что означает, что любой прыжок будет на расстоянии менее 1 экрана, но прыжок, который я вижу, заставляет contentOffset.y изменяться на тысячи - расстояние в несколько экранов. Для меня это доказывает, что якорь не является проблемой, если только я что-то не понимаю. - person DirkMaas; 16.09.2011

Я играл с кодом, который вы опубликовали, и я думаю, что нашел, что происходит. Когда вы делаете блочную анимацию, как в вашем коде:

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

Блок анимации фактически вызывается в начале анимации. Core Animation внутренне обрабатывает слои, которые создают видимость движения. Существует список анимируемых свойств в Центре разработки, и zoomScale среди них нет.

Когда zoomScale изменяется, scrollView автоматически обновляет свой contentOffset. Итак, когда начинается анимация, скачок, который вы видите, — это изменение contentOffset для нового масштабирования масштаба. Затем слой анимируется до нужного масштаба масштабирования, но целевое значение contentOffset (то есть то, каким должно быть значение contentOffset в конце масштабирования) уже установлено в начале масштабирования.

Вот почему масштабирование выглядит так, как будто оно сосредоточено вокруг точки за пределами экрана. Вам либо придется использовать setZoomScale:animated: либо, возможно, анимировать слой и смещение самостоятельно...

person devdavid    schedule 22.09.2011

Вы должны использовать scrollViewDidEndZooming:withView:atScale, чтобы уведомить вас о завершении масштабирования. В противном случае я считаю, что вам придется удалить использование свойства zoomScale UIScrollView и вместо этого полностью реализовать масштабирование вручную, например. http://jonathanwatmough.com/2008/12/implementing-tap-to-zoom-in-uiscrollview-on-an-iphone/.

person Michael Frederick    schedule 15.09.2011

У меня было много ошибок с масштабированием, когда моя функция 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: намного надежнее, чем изменение zoomScale в scrollView. Это тоже может помочь, даже если у вас будет больше вычислений...

person CedricSoubrie    schedule 19.09.2011

Я думаю, вам нужно самостоятельно манипулировать contentInset и/или contentOffset при каждом событии прокрутки. Манипуляции с опорными точками не помогут. Посмотрите на этот вопрос и мой ответ.

person winitzki    schedule 25.10.2013