[webkit-changes] [WebKit/WebKit] f4b233: [iOS] Mainframe scroll snap jumps to unpredictable...
Wenson Hsieh
noreply at github.com
Mon Apr 24 14:35:54 PDT 2023
Branch: refs/heads/main
Home: https://github.com/WebKit/WebKit
Commit: f4b2339d9e5187c28766b443ac6c29c27f59821c
https://github.com/WebKit/WebKit/commit/f4b2339d9e5187c28766b443ac6c29c27f59821c
Author: Wenson Hsieh <wenson_hsieh at apple.com>
Date: 2023-04-24 (Mon, 24 Apr 2023)
Changed paths:
A LayoutTests/css3/scroll-snap/ios/scroll-snap-mainframe-pinch-out-expected.txt
A LayoutTests/css3/scroll-snap/ios/scroll-snap-mainframe-pinch-out.html
M LayoutTests/css3/scroll-snap/ios/scroll-snap-mainframe-scroll-deceleration-with-obscured-inset.html
M LayoutTests/resources/ui-helper.js
M Source/WebCore/page/ChromeClient.h
M Source/WebCore/page/LocalFrameView.cpp
M Source/WebCore/page/LocalFrameView.h
M Source/WebCore/platform/ScrollableArea.cpp
M Source/WebCore/platform/ScrollableArea.h
M Source/WebKit/Platform/spi/ios/UIKitSPI.h
M Source/WebKit/UIProcess/API/ios/WKWebViewIOS.mm
M Source/WebKit/UIProcess/RemoteLayerTree/ios/RemoteScrollingCoordinatorProxyIOS.mm
M Source/WebKit/UIProcess/ios/WKScrollView.mm
M Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.cpp
M Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.h
M Source/WebKit/WebProcess/WebPage/WebPage.h
Log Message:
-----------
[iOS] Mainframe scroll snap jumps to unpredictable offsets when pinch zooming
https://bugs.webkit.org/show_bug.cgi?id=255848
rdar://108008629
Reviewed by Brent Fulgham and Simon Fraser.
When pinch zooming out to less than the initial scale on book.stevejobsarchive.com (when scroll
snapping on the root element is enabled), the scroll position frequently jumps to (seemingly) random
offsets in the page, sometimes after attempting to scroll (with animation) to another wrong scroll
snap point. There are multiple root causes that trigger these symptoms, described below (along with
the changes in this patch that address each one):
(1) We occasionally use the wrong scale to compute the appropriate snap offset in
`RemoteScrollingCoordinatorProxyIOS`. Currently, `WebPageProxy::displayedContentScale` only
yields the latest zoom scale we sent via `VisibleContentRectUpdate`, but we scale (and
counterscale) geometry directly on the `WKScrollView` using this scale. There's no guarantee
that `-[UIScrollView zoomScale]` is in sync with this last sent scale, so we can end up
computing the wrong nearest snap position in both `closestSnapOffsetForMainFrameScrolling` (when
ending the pinch) and `nearestActiveContentInsetAdjustedSnapOffset` (when we receive the first
stable-state commit after the bounce animation).
To fix this, we simply use `UIScrollView`'s `-zoomScale` instead so that the scale is consistent
with the current value of `-contentOffset`, as well as the retargeted content offset.
(2) Even ignoring (1), the scroll view sometimes fails to retarget to any offset at all after
pinching out, and instead settles back down to the current offset (as if snapping were
disabled), before instantly jumping to a nearby snap point. Whether or not this bug happens is
dependent on the order in which `WKScrollView`'s pan gesture action fires in Ended state —
relative to the pinch gesture — when ending the pinch. If the scroll view observes the pan
gesture ending (`-handlePan:`) before the pinch gesture ends (`-handlePinch:`), we'll run our
retargeting logic for scroll snapping, but then `UIScrollView` immediately stomps over the
retargeted position due to the `-zoomScale` clamping back to `-minimumZoomScale` when ending the
pinch. This ordering is currently non-deterministic, as it depends on hashing order of the
scroll view's pinch and pan gesture recognizers, when sending actions to gestures after ending
the touch.
To work around this, we take advantage of the fact that `-handlePinch:` is idempotent when the
pinch gesture has ended (since we'll clear the `zooming` flag and transition to `zoomBouncing`
state only once), and directly invoke `-handlePinch:` from within `-handlePan:` here to ensure
that we're able to use the retargeting delegate hook.
There are other workarounds that I considered, such as detecting that this bug is about to
happen and falling back to an animated scroll instead of retargeting; however, the above is the
only approach I could find that results in a smooth "glide" animation to the correct snap
destination (as opposed to zooming back to minimum scale and then scrolling to the final
location).
(3) There's logic in `-_updateVisibleContentRects` that's responsible for scrolling to the active
snap position when we're about to send a stable visible content rect update; however, this fires
way too early in some cases (i.e. right after the pinch ends, in the middle of zooming), causing
us to jump to the final offset. This is because we send a visible content rect update after the
`-isZooming` flag is unset (i.e. after the pinch gesture recognizer transitions to Ended state),
but before the zooming actually begins. Correct this gap in view stability state by additionally
consulting the `-isZoomBouncing` flag, which is set right after zooming ends when pinching out
and unset after the zoom animation completes.
(4) After fixing all the above, the behavior is considerably improved, but scrolling still
occasionally jumps to the final offset in the middle of the zoom bounce animation. This is
because `resnapAfterLayout()` triggers before the zoom animation is complete, causing a stutter
in the scroll position as we attempt an instant scroll to the final offset while still zooming.
Address this by avoiding `resnapAfterLayout()` in the case where the viewport is not in stable
state; to achieve this, we add some client plumbing through `LocalFrameView` ->
`WebChromeClient` -> `WebPage` to return whether or not we're in stable state. Note that we also
rely on (3) here, otherwise the web process will enter stable state too early.
With the 4 issues above fixed, pinch zooming on book.stevejobsarchive.com no longer causes the
scroll position to visually stutter or jump, and we instead transition in one smooth animation from
the offset in the pinched-out view to the final scroll snap destination, in all cases.
Test: css3/scroll-snap/ios/scroll-snap-mainframe-pinch-out.html
* LayoutTests/css3/scroll-snap/ios/scroll-snap-mainframe-pinch-out-expected.txt: Added.
* LayoutTests/css3/scroll-snap/ios/scroll-snap-mainframe-pinch-out.html: Added.
Add a new test case to exercise this scenario; this test uses scroll snapping on the root element,
and additionally continuously triggers layout on the page by changing the widths of each of the
scroll snap children every frame. The test then programmatically scrolls to a snap point in the
middle of the page, zooms out by pinching, and verifies that after the bounce has ended (i.e. we get
a stable state presentation update), the final scroll offset matches what we started with.
* LayoutTests/css3/scroll-snap/ios/scroll-snap-mainframe-scroll-deceleration-with-obscured-inset.html:
Drive-by fix: clean up this test page by moving the `script` elements in this test under the `head`.
* LayoutTests/resources/ui-helper.js:
(window.UIHelper.async pinch):
Add a `UIHelper` method to make it easier to synthesize a pinch gesture; this helper method takes
4 points (8 x/y values total) that represent the two starting touch locations, and two ending touch
locations. It then creates and synthesizes a 2-finger gesture stream using these points.
* Source/WebCore/page/ChromeClient.h:
(WebCore::ChromeClient::isInStableState const):
* Source/WebCore/page/LocalFrameView.cpp:
(WebCore::LocalFrameView::isInStableState const):
* Source/WebCore/page/LocalFrameView.h:
* Source/WebCore/platform/ScrollableArea.cpp:
(WebCore::ScrollableArea::resnapAfterLayout):
See (4) in the above diagnosis.
* Source/WebCore/platform/ScrollableArea.h:
(WebCore::ScrollableArea::isInStableState const):
* Source/WebKit/Platform/spi/ios/UIKitSPI.h:
Add more forward declarations on `UIScrollView`.
* Source/WebKit/UIProcess/API/ios/WKWebViewIOS.mm:
(-[WKWebView _viewStabilityState:]):
See (3) in the above diagnosis.
* Source/WebKit/UIProcess/RemoteLayerTree/ios/RemoteScrollingCoordinatorProxyIOS.mm:
(WebKit::RemoteScrollingCoordinatorProxyIOS::closestSnapOffsetForMainFrameScrolling const):
(WebKit::RemoteScrollingCoordinatorProxyIOS::nearestActiveContentInsetAdjustedSnapOffset const):
See (1) in the above diagnosis.
* Source/WebKit/UIProcess/ios/WKScrollView.mm:
(-[WKScrollView _sendPinchGestureActionEarlyIfNeeded]):
(-[WKScrollView handlePan:]):
See (2) in the above diagnosis.
* Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.cpp:
(WebKit::WebChromeClient::isInStableState const):
* Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.h:
* Source/WebKit/WebProcess/WebPage/WebPage.h:
(WebKit::WebPage::isInStableState const):
Canonical link: https://commits.webkit.org/263336@main
More information about the webkit-changes
mailing list