1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 package org.chromium.android_webview;
7 import android.graphics.Rect;
8 import android.widget.OverScroller;
10 import com.google.common.annotations.VisibleForTesting;
13 * Takes care of syncing the scroll offset between the Android View system and the
14 * InProcessViewRenderer.
16 * Unless otherwise values (sizes, scroll offsets) are in physical pixels.
19 public class AwScrollOffsetManager {
20 // Values taken from WebViewClassic.
22 // The amount of content to overlap between two screens when using pageUp/pageDown methiods.
23 private static final int PAGE_SCROLL_OVERLAP = 24;
24 // Standard animated scroll speed.
25 private static final int STD_SCROLL_ANIMATION_SPEED_PIX_PER_SEC = 480;
26 // Time for the longest scroll animation.
27 private static final int MAX_SCROLL_ANIMATION_DURATION_MILLISEC = 750;
30 * The interface that all users of AwScrollOffsetManager should implement.
32 * The unit of all the values in this delegate are physical pixels.
34 public interface Delegate {
35 // Call View#overScrollBy on the containerView.
36 void overScrollContainerViewBy(int deltaX, int deltaY, int scrollX, int scrollY,
37 int scrollRangeX, int scrollRangeY, boolean isTouchEvent);
38 // Call View#scrollTo on the containerView.
39 void scrollContainerViewTo(int x, int y);
40 // Store the scroll offset in the native side. This should really be a simple store
41 // operation, the native side shouldn't synchronously alter the scroll offset from within
43 void scrollNativeTo(int x, int y);
45 int getContainerViewScrollX();
46 int getContainerViewScrollY();
51 private final Delegate mDelegate;
53 // Scroll offset as seen by the native side.
54 private int mNativeScrollX;
55 private int mNativeScrollY;
57 // How many pixels can we scroll in a given direction.
58 private int mMaxHorizontalScrollOffset;
59 private int mMaxVerticalScrollOffset;
61 // Size of the container view.
62 private int mContainerViewWidth;
63 private int mContainerViewHeight;
65 // Whether we're in the middle of processing a touch event.
66 private boolean mProcessingTouchEvent;
68 private boolean mFlinging;
70 // Whether (and to what value) to update the native side scroll offset after we've finished
71 // processing a touch event.
72 private boolean mApplyDeferredNativeScroll;
73 private int mDeferredNativeScrollX;
74 private int mDeferredNativeScrollY;
76 private OverScroller mScroller;
78 public AwScrollOffsetManager(Delegate delegate, OverScroller overScroller) {
80 mScroller = overScroller;
83 //----- Scroll range and extent calculation methods -------------------------------------------
85 public int computeHorizontalScrollRange() {
86 return mContainerViewWidth + mMaxHorizontalScrollOffset;
89 public int computeMaximumHorizontalScrollOffset() {
90 return mMaxHorizontalScrollOffset;
93 public int computeHorizontalScrollOffset() {
94 return mDelegate.getContainerViewScrollX();
97 public int computeVerticalScrollRange() {
98 return mContainerViewHeight + mMaxVerticalScrollOffset;
101 public int computeMaximumVerticalScrollOffset() {
102 return mMaxVerticalScrollOffset;
105 public int computeVerticalScrollOffset() {
106 return mDelegate.getContainerViewScrollY();
109 public int computeVerticalScrollExtent() {
110 return mContainerViewHeight;
113 //---------------------------------------------------------------------------------------------
115 * Called when the scroll range changes. This needs to be the size of the on-screen content.
117 public void setMaxScrollOffset(int width, int height) {
118 mMaxHorizontalScrollOffset = width;
119 mMaxVerticalScrollOffset = height;
123 * Called when the physical size of the view changes.
125 public void setContainerViewSize(int width, int height) {
126 mContainerViewWidth = width;
127 mContainerViewHeight = height;
130 public void syncScrollOffsetFromOnDraw() {
131 // Unfortunately apps override onScrollChanged without calling super which is why we need
132 // to sync the scroll offset on every onDraw.
133 onContainerViewScrollChanged(mDelegate.getContainerViewScrollX(),
134 mDelegate.getContainerViewScrollY());
137 public void setProcessingTouchEvent(boolean processingTouchEvent) {
138 assert mProcessingTouchEvent != processingTouchEvent;
139 mProcessingTouchEvent = processingTouchEvent;
141 if (!mProcessingTouchEvent && mApplyDeferredNativeScroll) {
142 mApplyDeferredNativeScroll = false;
143 scrollNativeTo(mDeferredNativeScrollX, mDeferredNativeScrollY);
147 // Called by the native side to scroll the container view.
148 public void scrollContainerViewTo(int x, int y) {
152 final int scrollX = mDelegate.getContainerViewScrollX();
153 final int scrollY = mDelegate.getContainerViewScrollY();
154 final int deltaX = x - scrollX;
155 final int deltaY = y - scrollY;
156 final int scrollRangeX = computeMaximumHorizontalScrollOffset();
157 final int scrollRangeY = computeMaximumVerticalScrollOffset();
159 // We use overScrollContainerViewBy to be compatible with WebViewClassic which used this
160 // method for handling both over-scroll as well as in-bounds scroll.
161 mDelegate.overScrollContainerViewBy(deltaX, deltaY, scrollX, scrollY,
162 scrollRangeX, scrollRangeY, mProcessingTouchEvent);
165 public boolean isFlingActive() {
169 // Called by the native side to over-scroll the container view.
170 public void overScrollBy(int deltaX, int deltaY) {
171 // TODO(mkosiba): Once http://crbug.com/260663 and http://crbug.com/261239 are fixed it
172 // should be possible to uncomment the following asserts:
173 // if (deltaX < 0) assert mDelegate.getContainerViewScrollX() == 0;
174 // if (deltaX > 0) assert mDelegate.getContainerViewScrollX() ==
175 // computeMaximumHorizontalScrollOffset();
176 scrollBy(deltaX, deltaY);
179 private void scrollBy(int deltaX, int deltaY) {
180 if (deltaX == 0 && deltaY == 0) return;
182 final int scrollX = mDelegate.getContainerViewScrollX();
183 final int scrollY = mDelegate.getContainerViewScrollY();
184 final int scrollRangeX = computeMaximumHorizontalScrollOffset();
185 final int scrollRangeY = computeMaximumVerticalScrollOffset();
187 // The android.view.View.overScrollBy method is used for both scrolling and over-scrolling
188 // which is why we use it here.
189 mDelegate.overScrollContainerViewBy(deltaX, deltaY, scrollX, scrollY,
190 scrollRangeX, scrollRangeY, mProcessingTouchEvent);
193 private int clampHorizontalScroll(int scrollX) {
194 scrollX = Math.max(0, scrollX);
195 scrollX = Math.min(computeMaximumHorizontalScrollOffset(), scrollX);
199 private int clampVerticalScroll(int scrollY) {
200 scrollY = Math.max(0, scrollY);
201 scrollY = Math.min(computeMaximumVerticalScrollOffset(), scrollY);
205 // Called by the View system as a response to the mDelegate.overScrollContainerViewBy call.
206 public void onContainerViewOverScrolled(int scrollX, int scrollY, boolean clampedX,
208 // Clamp the scroll offset at (0, max).
209 scrollX = clampHorizontalScroll(scrollX);
210 scrollY = clampVerticalScroll(scrollY);
212 mDelegate.scrollContainerViewTo(scrollX, scrollY);
214 // This is only necessary if the containerView scroll offset ends up being different
215 // than the one set from native in which case we want the value stored on the native side
216 // to reflect the value stored in the containerView (and not the other way around).
217 scrollNativeTo(mDelegate.getContainerViewScrollX(), mDelegate.getContainerViewScrollY());
220 // Called by the View system when the scroll offset had changed. This might not get called if
221 // the embedder overrides WebView#onScrollChanged without calling super.onScrollChanged. If
222 // this method does get called it is called both as a response to the embedder scrolling the
223 // view as well as a response to mDelegate.scrollContainerViewTo.
224 public void onContainerViewScrollChanged(int x, int y) {
225 scrollNativeTo(x, y);
228 private void scrollNativeTo(int x, int y) {
229 x = clampHorizontalScroll(x);
230 y = clampVerticalScroll(y);
232 // We shouldn't do the store to native while processing a touch event since that confuses
233 // the gesture processing logic.
234 if (mProcessingTouchEvent) {
235 mDeferredNativeScrollX = x;
236 mDeferredNativeScrollY = y;
237 mApplyDeferredNativeScroll = true;
241 if (x == mNativeScrollX && y == mNativeScrollY)
244 // The scrollNativeTo call should be a simple store, so it's OK to assume it always
249 mDelegate.scrollNativeTo(x, y);
252 // Called whenever some other touch interaction requires the fling gesture to be canceled.
253 public void onFlingCancelGesture() {
254 // TODO(mkosiba): Support speeding up a fling by flinging again.
255 // http://crbug.com/265841
256 mScroller.forceFinished(true);
259 // Called when a fling gesture is not handled by the renderer.
260 // We explicitly ask the renderer not to handle fling gestures targeted at the root
262 public void onUnhandledFlingStartEvent(int velocityX, int velocityY) {
263 flingScroll(-velocityX, -velocityY);
266 // Starts the fling animation. Called both as a response to a fling gesture and as via the
267 // public WebView#flingScroll(int, int) API.
268 public void flingScroll(int velocityX, int velocityY) {
269 final int scrollX = mDelegate.getContainerViewScrollX();
270 final int scrollY = mDelegate.getContainerViewScrollY();
271 final int scrollRangeX = computeMaximumHorizontalScrollOffset();
272 final int scrollRangeY = computeMaximumVerticalScrollOffset();
274 mScroller.fling(scrollX, scrollY, velocityX, velocityY,
275 0, scrollRangeX, 0, scrollRangeY);
276 mDelegate.invalidate();
279 // Called immediately before the draw to update the scroll offset.
280 public void computeScrollAndAbsorbGlow(OverScrollGlow overScrollGlow) {
281 mFlinging = mScroller.computeScrollOffset();
286 final int oldX = mDelegate.getContainerViewScrollX();
287 final int oldY = mDelegate.getContainerViewScrollY();
288 int x = mScroller.getCurrX();
289 int y = mScroller.getCurrY();
291 final int scrollRangeX = computeMaximumHorizontalScrollOffset();
292 final int scrollRangeY = computeMaximumVerticalScrollOffset();
294 if (overScrollGlow != null) {
295 overScrollGlow.absorbGlow(x, y, oldX, oldY, scrollRangeX, scrollRangeY,
296 mScroller.getCurrVelocity());
299 // The mScroller is configured not to go outside of the scrollable range, so this call
300 // should never result in attempting to scroll outside of the scrollable region.
301 scrollBy(x - oldX, y - oldY);
303 mDelegate.invalidate();
306 private static int computeDurationInMilliSec(int dx, int dy) {
307 int distance = Math.max(Math.abs(dx), Math.abs(dy));
308 int duration = distance * 1000 / STD_SCROLL_ANIMATION_SPEED_PIX_PER_SEC;
309 return Math.min(duration, MAX_SCROLL_ANIMATION_DURATION_MILLISEC);
312 private boolean animateScrollTo(int x, int y) {
313 final int scrollX = mDelegate.getContainerViewScrollX();
314 final int scrollY = mDelegate.getContainerViewScrollY();
316 x = clampHorizontalScroll(x);
317 y = clampVerticalScroll(y);
319 int dx = x - scrollX;
320 int dy = y - scrollY;
322 if (dx == 0 && dy == 0)
325 mScroller.startScroll(scrollX, scrollY, dx, dy, computeDurationInMilliSec(dx, dy));
326 mDelegate.invalidate();
332 * See {@link android.webkit.WebView#pageUp(boolean)}
334 public boolean pageUp(boolean top) {
335 final int scrollX = mDelegate.getContainerViewScrollX();
336 final int scrollY = mDelegate.getContainerViewScrollY();
339 // go to the top of the document
340 return animateScrollTo(scrollX, 0);
342 int dy = -mContainerViewHeight / 2;
343 if (mContainerViewHeight > 2 * PAGE_SCROLL_OVERLAP) {
344 dy = -mContainerViewHeight + PAGE_SCROLL_OVERLAP;
346 // animateScrollTo clamps the argument to the scrollable range so using (scrollY + dy) is
348 return animateScrollTo(scrollX, scrollY + dy);
352 * See {@link android.webkit.WebView#pageDown(boolean)}
354 public boolean pageDown(boolean bottom) {
355 final int scrollX = mDelegate.getContainerViewScrollX();
356 final int scrollY = mDelegate.getContainerViewScrollY();
359 return animateScrollTo(scrollX, computeVerticalScrollRange());
361 int dy = mContainerViewHeight / 2;
362 if (mContainerViewHeight > 2 * PAGE_SCROLL_OVERLAP) {
363 dy = mContainerViewHeight - PAGE_SCROLL_OVERLAP;
365 // animateScrollTo clamps the argument to the scrollable range so using (scrollY + dy) is
367 return animateScrollTo(scrollX, scrollY + dy);
371 * See {@link android.webkit.WebView#requestChildRectangleOnScreen(View, Rect, boolean)}
373 public boolean requestChildRectangleOnScreen(int childOffsetX, int childOffsetY, Rect rect,
375 // TODO(mkosiba): WebViewClassic immediately returns false if a zoom animation is
376 // in progress. We currently can't tell if one is happening.. should we instead cancel any
377 // scroll animation when the size/pageScaleFactor changes?
379 // TODO(mkosiba): Take scrollbar width into account in the screenRight/screenBotton
380 // calculations. http://crbug.com/269032
382 final int scrollX = mDelegate.getContainerViewScrollX();
383 final int scrollY = mDelegate.getContainerViewScrollY();
385 rect.offset(childOffsetX, childOffsetY);
387 int screenTop = scrollY;
388 int screenBottom = scrollY + mContainerViewHeight;
389 int scrollYDelta = 0;
391 if (rect.bottom > screenBottom) {
392 int oneThirdOfScreenHeight = mContainerViewHeight / 3;
393 if (rect.width() > 2 * oneThirdOfScreenHeight) {
394 // If the rectangle is too tall to fit in the bottom two thirds
395 // of the screen, place it at the top.
396 scrollYDelta = rect.top - screenTop;
398 // If the rectangle will still fit on screen, we want its
399 // top to be in the top third of the screen.
400 scrollYDelta = rect.top - (screenTop + oneThirdOfScreenHeight);
402 } else if (rect.top < screenTop) {
403 scrollYDelta = rect.top - screenTop;
406 int screenLeft = scrollX;
407 int screenRight = scrollX + mContainerViewWidth;
408 int scrollXDelta = 0;
410 if (rect.right > screenRight && rect.left > screenLeft) {
411 if (rect.width() > mContainerViewWidth) {
412 scrollXDelta += (rect.left - screenLeft);
414 scrollXDelta += (rect.right - screenRight);
416 } else if (rect.left < screenLeft) {
417 scrollXDelta -= (screenLeft - rect.left);
420 if (scrollYDelta == 0 && scrollXDelta == 0) {
425 scrollBy(scrollXDelta, scrollYDelta);
428 return animateScrollTo(scrollX + scrollXDelta, scrollY + scrollYDelta);