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 //---------------------------------------------------------------------------------------------
114 // Called when the scroll range changes. This needs to be the size of the on-screen content.
115 public void setMaxScrollOffset(int width, int height) {
116 mMaxHorizontalScrollOffset = width;
117 mMaxVerticalScrollOffset = height;
120 // Called when the physical size of the view changes.
121 public void setContainerViewSize(int width, int height) {
122 mContainerViewWidth = width;
123 mContainerViewHeight = height;
126 public void syncScrollOffsetFromOnDraw() {
127 // Unfortunately apps override onScrollChanged without calling super which is why we need
128 // to sync the scroll offset on every onDraw.
129 onContainerViewScrollChanged(mDelegate.getContainerViewScrollX(),
130 mDelegate.getContainerViewScrollY());
133 public void setProcessingTouchEvent(boolean processingTouchEvent) {
134 assert mProcessingTouchEvent != processingTouchEvent;
135 mProcessingTouchEvent = processingTouchEvent;
137 if (!mProcessingTouchEvent && mApplyDeferredNativeScroll) {
138 mApplyDeferredNativeScroll = false;
139 scrollNativeTo(mDeferredNativeScrollX, mDeferredNativeScrollY);
143 // Called by the native side to scroll the container view.
144 public void scrollContainerViewTo(int x, int y) {
148 final int scrollX = mDelegate.getContainerViewScrollX();
149 final int scrollY = mDelegate.getContainerViewScrollY();
150 final int deltaX = x - scrollX;
151 final int deltaY = y - scrollY;
152 final int scrollRangeX = computeMaximumHorizontalScrollOffset();
153 final int scrollRangeY = computeMaximumVerticalScrollOffset();
155 // We use overScrollContainerViewBy to be compatible with WebViewClassic which used this
156 // method for handling both over-scroll as well as in-bounds scroll.
157 mDelegate.overScrollContainerViewBy(deltaX, deltaY, scrollX, scrollY,
158 scrollRangeX, scrollRangeY, mProcessingTouchEvent);
161 public boolean isFlingActive() {
165 // Called by the native side to over-scroll the container view.
166 public void overScrollBy(int deltaX, int deltaY) {
167 // TODO(mkosiba): Once http://crbug.com/260663 and http://crbug.com/261239 are fixed it
168 // should be possible to uncomment the following asserts:
169 // if (deltaX < 0) assert mDelegate.getContainerViewScrollX() == 0;
170 // if (deltaX > 0) assert mDelegate.getContainerViewScrollX() ==
171 // computeMaximumHorizontalScrollOffset();
172 scrollBy(deltaX, deltaY);
175 private void scrollBy(int deltaX, int deltaY) {
176 if (deltaX == 0 && deltaY == 0) return;
178 final int scrollX = mDelegate.getContainerViewScrollX();
179 final int scrollY = mDelegate.getContainerViewScrollY();
180 final int scrollRangeX = computeMaximumHorizontalScrollOffset();
181 final int scrollRangeY = computeMaximumVerticalScrollOffset();
183 // The android.view.View.overScrollBy method is used for both scrolling and over-scrolling
184 // which is why we use it here.
185 mDelegate.overScrollContainerViewBy(deltaX, deltaY, scrollX, scrollY,
186 scrollRangeX, scrollRangeY, mProcessingTouchEvent);
189 private int clampHorizontalScroll(int scrollX) {
190 scrollX = Math.max(0, scrollX);
191 scrollX = Math.min(computeMaximumHorizontalScrollOffset(), scrollX);
195 private int clampVerticalScroll(int scrollY) {
196 scrollY = Math.max(0, scrollY);
197 scrollY = Math.min(computeMaximumVerticalScrollOffset(), scrollY);
201 // Called by the View system as a response to the mDelegate.overScrollContainerViewBy call.
202 public void onContainerViewOverScrolled(int scrollX, int scrollY, boolean clampedX,
204 // Clamp the scroll offset at (0, max).
205 scrollX = clampHorizontalScroll(scrollX);
206 scrollY = clampVerticalScroll(scrollY);
208 mDelegate.scrollContainerViewTo(scrollX, scrollY);
210 // This is only necessary if the containerView scroll offset ends up being different
211 // than the one set from native in which case we want the value stored on the native side
212 // to reflect the value stored in the containerView (and not the other way around).
213 scrollNativeTo(mDelegate.getContainerViewScrollX(), mDelegate.getContainerViewScrollY());
216 // Called by the View system when the scroll offset had changed. This might not get called if
217 // the embedder overrides WebView#onScrollChanged without calling super.onScrollChanged. If
218 // this method does get called it is called both as a response to the embedder scrolling the
219 // view as well as a response to mDelegate.scrollContainerViewTo.
220 public void onContainerViewScrollChanged(int x, int y) {
221 scrollNativeTo(x, y);
224 private void scrollNativeTo(int x, int y) {
225 x = clampHorizontalScroll(x);
226 y = clampVerticalScroll(y);
228 // We shouldn't do the store to native while processing a touch event since that confuses
229 // the gesture processing logic.
230 if (mProcessingTouchEvent) {
231 mDeferredNativeScrollX = x;
232 mDeferredNativeScrollY = y;
233 mApplyDeferredNativeScroll = true;
237 if (x == mNativeScrollX && y == mNativeScrollY)
240 // The scrollNativeTo call should be a simple store, so it's OK to assume it always
245 mDelegate.scrollNativeTo(x, y);
248 // Called at the beginning of every fling gesture.
249 public void onFlingStartGesture(int velocityX, int velocityY) {
250 mLastFlingVelocityX = velocityX;
251 mLastFlingVelocityY = velocityY;
254 // Called whenever some other touch interaction requires the fling gesture to be canceled.
255 public void onFlingCancelGesture() {
256 // TODO(mkosiba): Support speeding up a fling by flinging again.
257 // http://crbug.com/265841
258 mScroller.forceFinished(true);
261 // Called when a fling gesture is not handled by the renderer.
262 // We explicitly ask the renderer not to handle fling gestures targeted at the root
264 public void onUnhandledFlingStartEvent() {
265 flingScroll(-mLastFlingVelocityX, -mLastFlingVelocityY);
268 // Starts the fling animation. Called both as a response to a fling gesture and as via the
269 // public WebView#flingScroll(int, int) API.
270 public void flingScroll(int velocityX, int velocityY) {
271 final int scrollX = mDelegate.getContainerViewScrollX();
272 final int scrollY = mDelegate.getContainerViewScrollY();
273 final int scrollRangeX = computeMaximumHorizontalScrollOffset();
274 final int scrollRangeY = computeMaximumVerticalScrollOffset();
276 mScroller.fling(scrollX, scrollY, velocityX, velocityY,
277 0, scrollRangeX, 0, scrollRangeY);
279 mDelegate.invalidate();
282 // Called immediately before the draw to update the scroll offset.
283 public void computeScrollAndAbsorbGlow(OverScrollGlow overScrollGlow) {
284 final boolean stillAnimating = mScroller.computeScrollOffset();
285 if (!stillAnimating) {
290 final int oldX = mDelegate.getContainerViewScrollX();
291 final int oldY = mDelegate.getContainerViewScrollY();
292 int x = mScroller.getCurrX();
293 int y = mScroller.getCurrY();
295 final int scrollRangeX = computeMaximumHorizontalScrollOffset();
296 final int scrollRangeY = computeMaximumVerticalScrollOffset();
298 if (overScrollGlow != null) {
299 overScrollGlow.absorbGlow(x, y, oldX, oldY, scrollRangeX, scrollRangeY,
300 mScroller.getCurrVelocity());
303 // The mScroller is configured not to go outside of the scrollable range, so this call
304 // should never result in attempting to scroll outside of the scrollable region.
305 scrollBy(x - oldX, y - oldY);
307 mDelegate.invalidate();
310 private static int computeDurationInMilliSec(int dx, int dy) {
311 int distance = Math.max(Math.abs(dx), Math.abs(dy));
312 int duration = distance * 1000 / STD_SCROLL_ANIMATION_SPEED_PIX_PER_SEC;
313 return Math.min(duration, MAX_SCROLL_ANIMATION_DURATION_MILLISEC);
316 private boolean animateScrollTo(int x, int y) {
317 final int scrollX = mDelegate.getContainerViewScrollX();
318 final int scrollY = mDelegate.getContainerViewScrollY();
320 x = clampHorizontalScroll(x);
321 y = clampVerticalScroll(y);
323 int dx = x - scrollX;
324 int dy = y - scrollY;
326 if (dx == 0 && dy == 0)
329 mScroller.startScroll(scrollX, scrollY, dx, dy, computeDurationInMilliSec(dx, dy));
330 mDelegate.invalidate();
336 * See {@link WebView#pageUp(boolean)}
338 public boolean pageUp(boolean top) {
339 final int scrollX = mDelegate.getContainerViewScrollX();
340 final int scrollY = mDelegate.getContainerViewScrollY();
343 // go to the top of the document
344 return animateScrollTo(scrollX, 0);
346 int dy = -mContainerViewHeight / 2;
347 if (mContainerViewHeight > 2 * PAGE_SCROLL_OVERLAP) {
348 dy = -mContainerViewHeight + PAGE_SCROLL_OVERLAP;
350 // animateScrollTo clamps the argument to the scrollable range so using (scrollY + dy) is
352 return animateScrollTo(scrollX, scrollY + dy);
356 * See {@link WebView#pageDown(boolean)}
358 public boolean pageDown(boolean bottom) {
359 final int scrollX = mDelegate.getContainerViewScrollX();
360 final int scrollY = mDelegate.getContainerViewScrollY();
363 return animateScrollTo(scrollX, computeVerticalScrollRange());
365 int dy = mContainerViewHeight / 2;
366 if (mContainerViewHeight > 2 * PAGE_SCROLL_OVERLAP) {
367 dy = mContainerViewHeight - PAGE_SCROLL_OVERLAP;
369 // animateScrollTo clamps the argument to the scrollable range so using (scrollY + dy) is
371 return animateScrollTo(scrollX, scrollY + dy);
375 * See {@link WebView#requestChildRectangleOnScreen(View, Rect, boolean)}
377 public boolean requestChildRectangleOnScreen(int childOffsetX, int childOffsetY, Rect rect,
379 // TODO(mkosiba): WebViewClassic immediately returns false if a zoom animation is
380 // in progress. We currently can't tell if one is happening.. should we instead cancel any
381 // scroll animation when the size/pageScaleFactor changes?
383 // TODO(mkosiba): Take scrollbar width into account in the screenRight/screenBotton
384 // calculations. http://crbug.com/269032
386 final int scrollX = mDelegate.getContainerViewScrollX();
387 final int scrollY = mDelegate.getContainerViewScrollY();
389 rect.offset(childOffsetX, childOffsetY);
391 int screenTop = scrollY;
392 int screenBottom = scrollY + mContainerViewHeight;
393 int scrollYDelta = 0;
395 if (rect.bottom > screenBottom) {
396 int oneThirdOfScreenHeight = mContainerViewHeight / 3;
397 if (rect.width() > 2 * oneThirdOfScreenHeight) {
398 // If the rectangle is too tall to fit in the bottom two thirds
399 // of the screen, place it at the top.
400 scrollYDelta = rect.top - screenTop;
402 // If the rectangle will still fit on screen, we want its
403 // top to be in the top third of the screen.
404 scrollYDelta = rect.top - (screenTop + oneThirdOfScreenHeight);
406 } else if (rect.top < screenTop) {
407 scrollYDelta = rect.top - screenTop;
410 int screenLeft = scrollX;
411 int screenRight = scrollX + mContainerViewWidth;
412 int scrollXDelta = 0;
414 if (rect.right > screenRight && rect.left > screenLeft) {
415 if (rect.width() > mContainerViewWidth) {
416 scrollXDelta += (rect.left - screenLeft);
418 scrollXDelta += (rect.right - screenRight);
420 } else if (rect.left < screenLeft) {
421 scrollXDelta -= (screenLeft - rect.left);
424 if (scrollYDelta == 0 && scrollXDelta == 0) {
429 scrollBy(scrollXDelta, scrollYDelta);
432 return animateScrollTo(scrollX + scrollXDelta, scrollY + scrollYDelta);