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;
29 // The unit of all the values in this delegate are physical pixels.
30 public interface Delegate {
31 // Call View#overScrollBy on the containerView.
32 void overScrollContainerViewBy(int deltaX, int deltaY, int scrollX, int scrollY,
33 int scrollRangeX, int scrollRangeY, boolean isTouchEvent);
34 // Call View#scrollTo on the containerView.
35 void scrollContainerViewTo(int x, int y);
36 // Store the scroll offset in the native side. This should really be a simple store
37 // operation, the native side shouldn't synchronously alter the scroll offset from within
39 void scrollNativeTo(int x, int y);
41 int getContainerViewScrollX();
42 int getContainerViewScrollY();
47 private final Delegate mDelegate;
49 // Scroll offset as seen by the native side.
50 private int mNativeScrollX;
51 private int mNativeScrollY;
53 // How many pixels can we scroll in a given direction.
54 private int mMaxHorizontalScrollOffset;
55 private int mMaxVerticalScrollOffset;
57 // Size of the container view.
58 private int mContainerViewWidth;
59 private int mContainerViewHeight;
61 // Whether we're in the middle of processing a touch event.
62 private boolean mProcessingTouchEvent;
64 private boolean mFlinging;
66 // Whether (and to what value) to update the native side scroll offset after we've finished
67 // processing a touch event.
68 private boolean mApplyDeferredNativeScroll;
69 private int mDeferredNativeScrollX;
70 private int mDeferredNativeScrollY;
72 // The velocity of the last recorded fling,
73 private int mLastFlingVelocityX;
74 private int mLastFlingVelocityY;
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 at the beginning of every fling gesture.
253 public void onFlingStartGesture(int velocityX, int velocityY) {
254 mLastFlingVelocityX = velocityX;
255 mLastFlingVelocityY = velocityY;
258 // Called whenever some other touch interaction requires the fling gesture to be canceled.
259 public void onFlingCancelGesture() {
260 // TODO(mkosiba): Support speeding up a fling by flinging again.
261 // http://crbug.com/265841
262 mScroller.forceFinished(true);
265 // Called when a fling gesture is not handled by the renderer.
266 // We explicitly ask the renderer not to handle fling gestures targeted at the root
268 public void onUnhandledFlingStartEvent() {
269 flingScroll(-mLastFlingVelocityX, -mLastFlingVelocityY);
272 // Starts the fling animation. Called both as a response to a fling gesture and as via the
273 // public WebView#flingScroll(int, int) API.
274 public void flingScroll(int velocityX, int velocityY) {
275 final int scrollX = mDelegate.getContainerViewScrollX();
276 final int scrollY = mDelegate.getContainerViewScrollY();
277 final int scrollRangeX = computeMaximumHorizontalScrollOffset();
278 final int scrollRangeY = computeMaximumVerticalScrollOffset();
280 mScroller.fling(scrollX, scrollY, velocityX, velocityY,
281 0, scrollRangeX, 0, scrollRangeY);
283 mDelegate.invalidate();
286 // Called immediately before the draw to update the scroll offset.
287 public void computeScrollAndAbsorbGlow(OverScrollGlow overScrollGlow) {
288 final boolean stillAnimating = mScroller.computeScrollOffset();
289 if (!stillAnimating) {
294 final int oldX = mDelegate.getContainerViewScrollX();
295 final int oldY = mDelegate.getContainerViewScrollY();
296 int x = mScroller.getCurrX();
297 int y = mScroller.getCurrY();
299 final int scrollRangeX = computeMaximumHorizontalScrollOffset();
300 final int scrollRangeY = computeMaximumVerticalScrollOffset();
302 if (overScrollGlow != null) {
303 overScrollGlow.absorbGlow(x, y, oldX, oldY, scrollRangeX, scrollRangeY,
304 mScroller.getCurrVelocity());
307 // The mScroller is configured not to go outside of the scrollable range, so this call
308 // should never result in attempting to scroll outside of the scrollable region.
309 scrollBy(x - oldX, y - oldY);
311 mDelegate.invalidate();
314 private static int computeDurationInMilliSec(int dx, int dy) {
315 int distance = Math.max(Math.abs(dx), Math.abs(dy));
316 int duration = distance * 1000 / STD_SCROLL_ANIMATION_SPEED_PIX_PER_SEC;
317 return Math.min(duration, MAX_SCROLL_ANIMATION_DURATION_MILLISEC);
320 private boolean animateScrollTo(int x, int y) {
321 final int scrollX = mDelegate.getContainerViewScrollX();
322 final int scrollY = mDelegate.getContainerViewScrollY();
324 x = clampHorizontalScroll(x);
325 y = clampVerticalScroll(y);
327 int dx = x - scrollX;
328 int dy = y - scrollY;
330 if (dx == 0 && dy == 0)
333 mScroller.startScroll(scrollX, scrollY, dx, dy, computeDurationInMilliSec(dx, dy));
334 mDelegate.invalidate();
340 * See {@link android.webkit.WebView#pageUp(boolean)}
342 public boolean pageUp(boolean top) {
343 final int scrollX = mDelegate.getContainerViewScrollX();
344 final int scrollY = mDelegate.getContainerViewScrollY();
347 // go to the top of the document
348 return animateScrollTo(scrollX, 0);
350 int dy = -mContainerViewHeight / 2;
351 if (mContainerViewHeight > 2 * PAGE_SCROLL_OVERLAP) {
352 dy = -mContainerViewHeight + PAGE_SCROLL_OVERLAP;
354 // animateScrollTo clamps the argument to the scrollable range so using (scrollY + dy) is
356 return animateScrollTo(scrollX, scrollY + dy);
360 * See {@link android.webkit.WebView#pageDown(boolean)}
362 public boolean pageDown(boolean bottom) {
363 final int scrollX = mDelegate.getContainerViewScrollX();
364 final int scrollY = mDelegate.getContainerViewScrollY();
367 return animateScrollTo(scrollX, computeVerticalScrollRange());
369 int dy = mContainerViewHeight / 2;
370 if (mContainerViewHeight > 2 * PAGE_SCROLL_OVERLAP) {
371 dy = mContainerViewHeight - PAGE_SCROLL_OVERLAP;
373 // animateScrollTo clamps the argument to the scrollable range so using (scrollY + dy) is
375 return animateScrollTo(scrollX, scrollY + dy);
379 * See {@link android.webkit.WebView#requestChildRectangleOnScreen(View, Rect, boolean)}
381 public boolean requestChildRectangleOnScreen(int childOffsetX, int childOffsetY, Rect rect,
383 // TODO(mkosiba): WebViewClassic immediately returns false if a zoom animation is
384 // in progress. We currently can't tell if one is happening.. should we instead cancel any
385 // scroll animation when the size/pageScaleFactor changes?
387 // TODO(mkosiba): Take scrollbar width into account in the screenRight/screenBotton
388 // calculations. http://crbug.com/269032
390 final int scrollX = mDelegate.getContainerViewScrollX();
391 final int scrollY = mDelegate.getContainerViewScrollY();
393 rect.offset(childOffsetX, childOffsetY);
395 int screenTop = scrollY;
396 int screenBottom = scrollY + mContainerViewHeight;
397 int scrollYDelta = 0;
399 if (rect.bottom > screenBottom) {
400 int oneThirdOfScreenHeight = mContainerViewHeight / 3;
401 if (rect.width() > 2 * oneThirdOfScreenHeight) {
402 // If the rectangle is too tall to fit in the bottom two thirds
403 // of the screen, place it at the top.
404 scrollYDelta = rect.top - screenTop;
406 // If the rectangle will still fit on screen, we want its
407 // top to be in the top third of the screen.
408 scrollYDelta = rect.top - (screenTop + oneThirdOfScreenHeight);
410 } else if (rect.top < screenTop) {
411 scrollYDelta = rect.top - screenTop;
414 int screenLeft = scrollX;
415 int screenRight = scrollX + mContainerViewWidth;
416 int scrollXDelta = 0;
418 if (rect.right > screenRight && rect.left > screenLeft) {
419 if (rect.width() > mContainerViewWidth) {
420 scrollXDelta += (rect.left - screenLeft);
422 scrollXDelta += (rect.right - screenRight);
424 } else if (rect.left < screenLeft) {
425 scrollXDelta -= (screenLeft - rect.left);
428 if (scrollYDelta == 0 && scrollXDelta == 0) {
433 scrollBy(scrollXDelta, scrollYDelta);
436 return animateScrollTo(scrollX + scrollXDelta, scrollY + scrollYDelta);