• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.
4 
5 package org.chromium.android_webview;
6 
7 import android.graphics.Rect;
8 import android.widget.OverScroller;
9 
10 import com.google.common.annotations.VisibleForTesting;
11 
12 /**
13  * Takes care of syncing the scroll offset between the Android View system and the
14  * InProcessViewRenderer.
15  *
16  * Unless otherwise values (sizes, scroll offsets) are in physical pixels.
17  */
18 @VisibleForTesting
19 public class AwScrollOffsetManager {
20     // Values taken from WebViewClassic.
21 
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;
28 
29     /**
30      * The interface that all users of AwScrollOffsetManager should implement.
31      *
32      * The unit of all the values in this delegate are physical pixels.
33      */
34     public interface Delegate {
35         // Call View#overScrollBy on the containerView.
overScrollContainerViewBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, boolean isTouchEvent)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.
scrollContainerViewTo(int x, int y)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
42         // this call.
scrollNativeTo(int x, int y)43         void scrollNativeTo(int x, int y);
44 
getContainerViewScrollX()45         int getContainerViewScrollX();
getContainerViewScrollY()46         int getContainerViewScrollY();
47 
invalidate()48         void invalidate();
49     }
50 
51     private final Delegate mDelegate;
52 
53     // Scroll offset as seen by the native side.
54     private int mNativeScrollX;
55     private int mNativeScrollY;
56 
57     // How many pixels can we scroll in a given direction.
58     private int mMaxHorizontalScrollOffset;
59     private int mMaxVerticalScrollOffset;
60 
61     // Size of the container view.
62     private int mContainerViewWidth;
63     private int mContainerViewHeight;
64 
65     // Whether we're in the middle of processing a touch event.
66     private boolean mProcessingTouchEvent;
67 
68     // Don't skip computeScrollAndAbsorbGlow just because isFling is called in between.
69     private boolean mWasFlinging;
70 
71     // Whether (and to what value) to update the native side scroll offset after we've finished
72     // processing a touch event.
73     private boolean mApplyDeferredNativeScroll;
74     private int mDeferredNativeScrollX;
75     private int mDeferredNativeScrollY;
76 
77     // The velocity of the last recorded fling,
78     private int mLastFlingVelocityX;
79     private int mLastFlingVelocityY;
80 
81     private OverScroller mScroller;
82 
AwScrollOffsetManager(Delegate delegate, OverScroller overScroller)83     public AwScrollOffsetManager(Delegate delegate, OverScroller overScroller) {
84         mDelegate = delegate;
85         mScroller = overScroller;
86     }
87 
88     //----- Scroll range and extent calculation methods -------------------------------------------
89 
computeHorizontalScrollRange()90     public int computeHorizontalScrollRange() {
91         return mContainerViewWidth + mMaxHorizontalScrollOffset;
92     }
93 
computeMaximumHorizontalScrollOffset()94     public int computeMaximumHorizontalScrollOffset() {
95         return mMaxHorizontalScrollOffset;
96     }
97 
computeHorizontalScrollOffset()98     public int computeHorizontalScrollOffset() {
99         return mDelegate.getContainerViewScrollX();
100     }
101 
computeVerticalScrollRange()102     public int computeVerticalScrollRange() {
103         return mContainerViewHeight + mMaxVerticalScrollOffset;
104     }
105 
computeMaximumVerticalScrollOffset()106     public int computeMaximumVerticalScrollOffset() {
107         return mMaxVerticalScrollOffset;
108     }
109 
computeVerticalScrollOffset()110     public int computeVerticalScrollOffset() {
111         return mDelegate.getContainerViewScrollY();
112     }
113 
computeVerticalScrollExtent()114     public int computeVerticalScrollExtent() {
115         return mContainerViewHeight;
116     }
117 
118     //---------------------------------------------------------------------------------------------
119     /**
120      * Called when the scroll range changes. This needs to be the size of the on-screen content.
121      */
setMaxScrollOffset(int width, int height)122     public void setMaxScrollOffset(int width, int height) {
123         mMaxHorizontalScrollOffset = width;
124         mMaxVerticalScrollOffset = height;
125     }
126 
127     /**
128      * Called when the physical size of the view changes.
129      */
setContainerViewSize(int width, int height)130     public void setContainerViewSize(int width, int height) {
131         mContainerViewWidth = width;
132         mContainerViewHeight = height;
133     }
134 
syncScrollOffsetFromOnDraw()135     public void syncScrollOffsetFromOnDraw() {
136         // Unfortunately apps override onScrollChanged without calling super which is why we need
137         // to sync the scroll offset on every onDraw.
138         onContainerViewScrollChanged(mDelegate.getContainerViewScrollX(),
139                 mDelegate.getContainerViewScrollY());
140     }
141 
setProcessingTouchEvent(boolean processingTouchEvent)142     public void setProcessingTouchEvent(boolean processingTouchEvent) {
143         assert mProcessingTouchEvent != processingTouchEvent;
144         mProcessingTouchEvent = processingTouchEvent;
145 
146         if (!mProcessingTouchEvent && mApplyDeferredNativeScroll) {
147             mApplyDeferredNativeScroll = false;
148             scrollNativeTo(mDeferredNativeScrollX, mDeferredNativeScrollY);
149         }
150     }
151 
152     // Called by the native side to scroll the container view.
scrollContainerViewTo(int x, int y)153     public void scrollContainerViewTo(int x, int y) {
154         mNativeScrollX = x;
155         mNativeScrollY = y;
156 
157         final int scrollX = mDelegate.getContainerViewScrollX();
158         final int scrollY = mDelegate.getContainerViewScrollY();
159         final int deltaX = x - scrollX;
160         final int deltaY = y - scrollY;
161         final int scrollRangeX = computeMaximumHorizontalScrollOffset();
162         final int scrollRangeY = computeMaximumVerticalScrollOffset();
163 
164         // We use overScrollContainerViewBy to be compatible with WebViewClassic which used this
165         // method for handling both over-scroll as well as in-bounds scroll.
166         mDelegate.overScrollContainerViewBy(deltaX, deltaY, scrollX, scrollY,
167                 scrollRangeX, scrollRangeY, mProcessingTouchEvent);
168     }
169 
isFlingActive()170     public boolean isFlingActive() {
171         boolean flinging = mScroller.computeScrollOffset();
172         mWasFlinging |= flinging;
173         return flinging;
174     }
175 
176     // Called by the native side to over-scroll the container view.
overScrollBy(int deltaX, int deltaY)177     public void overScrollBy(int deltaX, int deltaY) {
178         // TODO(mkosiba): Once http://crbug.com/260663 and http://crbug.com/261239 are fixed it
179         // should be possible to uncomment the following asserts:
180         // if (deltaX < 0) assert mDelegate.getContainerViewScrollX() == 0;
181         // if (deltaX > 0) assert mDelegate.getContainerViewScrollX() ==
182         //          computeMaximumHorizontalScrollOffset();
183         scrollBy(deltaX, deltaY);
184     }
185 
scrollBy(int deltaX, int deltaY)186     private void scrollBy(int deltaX, int deltaY) {
187         if (deltaX == 0 && deltaY == 0) return;
188 
189         final int scrollX = mDelegate.getContainerViewScrollX();
190         final int scrollY = mDelegate.getContainerViewScrollY();
191         final int scrollRangeX = computeMaximumHorizontalScrollOffset();
192         final int scrollRangeY = computeMaximumVerticalScrollOffset();
193 
194         // The android.view.View.overScrollBy method is used for both scrolling and over-scrolling
195         // which is why we use it here.
196         mDelegate.overScrollContainerViewBy(deltaX, deltaY, scrollX, scrollY,
197                 scrollRangeX, scrollRangeY, mProcessingTouchEvent);
198     }
199 
clampHorizontalScroll(int scrollX)200     private int clampHorizontalScroll(int scrollX) {
201         scrollX = Math.max(0, scrollX);
202         scrollX = Math.min(computeMaximumHorizontalScrollOffset(), scrollX);
203         return scrollX;
204     }
205 
clampVerticalScroll(int scrollY)206     private int clampVerticalScroll(int scrollY) {
207         scrollY = Math.max(0, scrollY);
208         scrollY = Math.min(computeMaximumVerticalScrollOffset(), scrollY);
209         return scrollY;
210     }
211 
212     // Called by the View system as a response to the mDelegate.overScrollContainerViewBy call.
onContainerViewOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY)213     public void onContainerViewOverScrolled(int scrollX, int scrollY, boolean clampedX,
214             boolean clampedY) {
215         // Clamp the scroll offset at (0, max).
216         scrollX = clampHorizontalScroll(scrollX);
217         scrollY = clampVerticalScroll(scrollY);
218 
219         mDelegate.scrollContainerViewTo(scrollX, scrollY);
220 
221         // This is only necessary if the containerView scroll offset ends up being different
222         // than the one set from native in which case we want the value stored on the native side
223         // to reflect the value stored in the containerView (and not the other way around).
224         scrollNativeTo(mDelegate.getContainerViewScrollX(), mDelegate.getContainerViewScrollY());
225     }
226 
227     // Called by the View system when the scroll offset had changed. This might not get called if
228     // the embedder overrides WebView#onScrollChanged without calling super.onScrollChanged. If
229     // this method does get called it is called both as a response to the embedder scrolling the
230     // view as well as a response to mDelegate.scrollContainerViewTo.
onContainerViewScrollChanged(int x, int y)231     public void onContainerViewScrollChanged(int x, int y) {
232         scrollNativeTo(x, y);
233     }
234 
scrollNativeTo(int x, int y)235     private void scrollNativeTo(int x, int y) {
236         x = clampHorizontalScroll(x);
237         y = clampVerticalScroll(y);
238 
239         // We shouldn't do the store to native while processing a touch event since that confuses
240         // the gesture processing logic.
241         if (mProcessingTouchEvent) {
242             mDeferredNativeScrollX = x;
243             mDeferredNativeScrollY = y;
244             mApplyDeferredNativeScroll = true;
245             return;
246         }
247 
248         if (x == mNativeScrollX && y == mNativeScrollY)
249             return;
250 
251         // The scrollNativeTo call should be a simple store, so it's OK to assume it always
252         // succeeds.
253         mNativeScrollX = x;
254         mNativeScrollY = y;
255 
256         mDelegate.scrollNativeTo(x, y);
257     }
258 
259     // Called at the beginning of every fling gesture.
onFlingStartGesture(int velocityX, int velocityY)260     public void onFlingStartGesture(int velocityX, int velocityY) {
261         mLastFlingVelocityX = velocityX;
262         mLastFlingVelocityY = velocityY;
263     }
264 
265     // Called whenever some other touch interaction requires the fling gesture to be canceled.
onFlingCancelGesture()266     public void onFlingCancelGesture() {
267         // TODO(mkosiba): Support speeding up a fling by flinging again.
268         // http://crbug.com/265841
269         mScroller.forceFinished(true);
270     }
271 
272     // Called when a fling gesture is not handled by the renderer.
273     // We explicitly ask the renderer not to handle fling gestures targeted at the root
274     // scroll layer.
onUnhandledFlingStartEvent()275     public void onUnhandledFlingStartEvent() {
276         flingScroll(-mLastFlingVelocityX, -mLastFlingVelocityY);
277     }
278 
279     // Starts the fling animation. Called both as a response to a fling gesture and as via the
280     // public WebView#flingScroll(int, int) API.
flingScroll(int velocityX, int velocityY)281     public void flingScroll(int velocityX, int velocityY) {
282         final int scrollX = mDelegate.getContainerViewScrollX();
283         final int scrollY = mDelegate.getContainerViewScrollY();
284         final int scrollRangeX = computeMaximumHorizontalScrollOffset();
285         final int scrollRangeY = computeMaximumVerticalScrollOffset();
286 
287         mScroller.fling(scrollX, scrollY, velocityX, velocityY,
288                 0, scrollRangeX, 0, scrollRangeY);
289         mDelegate.invalidate();
290     }
291 
292     // Called immediately before the draw to update the scroll offset.
computeScrollAndAbsorbGlow(OverScrollGlow overScrollGlow)293     public void computeScrollAndAbsorbGlow(OverScrollGlow overScrollGlow) {
294         if (!mScroller.computeScrollOffset() && !mWasFlinging) {
295             return;
296         }
297         mWasFlinging = false;
298 
299         final int oldX = mDelegate.getContainerViewScrollX();
300         final int oldY = mDelegate.getContainerViewScrollY();
301         int x = mScroller.getCurrX();
302         int y = mScroller.getCurrY();
303 
304         final int scrollRangeX = computeMaximumHorizontalScrollOffset();
305         final int scrollRangeY = computeMaximumVerticalScrollOffset();
306 
307         if (overScrollGlow != null) {
308             overScrollGlow.absorbGlow(x, y, oldX, oldY, scrollRangeX, scrollRangeY,
309                     mScroller.getCurrVelocity());
310         }
311 
312         // The mScroller is configured not to go outside of the scrollable range, so this call
313         // should never result in attempting to scroll outside of the scrollable region.
314         scrollBy(x - oldX, y - oldY);
315 
316         mDelegate.invalidate();
317     }
318 
computeDurationInMilliSec(int dx, int dy)319     private static int computeDurationInMilliSec(int dx, int dy) {
320         int distance = Math.max(Math.abs(dx), Math.abs(dy));
321         int duration = distance * 1000 / STD_SCROLL_ANIMATION_SPEED_PIX_PER_SEC;
322         return Math.min(duration, MAX_SCROLL_ANIMATION_DURATION_MILLISEC);
323     }
324 
animateScrollTo(int x, int y)325     private boolean animateScrollTo(int x, int y) {
326         final int scrollX = mDelegate.getContainerViewScrollX();
327         final int scrollY = mDelegate.getContainerViewScrollY();
328 
329         x = clampHorizontalScroll(x);
330         y = clampVerticalScroll(y);
331 
332         int dx = x - scrollX;
333         int dy = y - scrollY;
334 
335         if (dx == 0 && dy == 0)
336             return false;
337 
338         mScroller.startScroll(scrollX, scrollY, dx, dy, computeDurationInMilliSec(dx, dy));
339         mDelegate.invalidate();
340 
341         return true;
342     }
343 
344     /**
345      * See {@link android.webkit.WebView#pageUp(boolean)}
346      */
pageUp(boolean top)347     public boolean pageUp(boolean top) {
348         final int scrollX = mDelegate.getContainerViewScrollX();
349         final int scrollY = mDelegate.getContainerViewScrollY();
350 
351         if (top) {
352             // go to the top of the document
353             return animateScrollTo(scrollX, 0);
354         }
355         int dy = -mContainerViewHeight / 2;
356         if (mContainerViewHeight > 2 * PAGE_SCROLL_OVERLAP) {
357             dy = -mContainerViewHeight + PAGE_SCROLL_OVERLAP;
358         }
359         // animateScrollTo clamps the argument to the scrollable range so using (scrollY + dy) is
360         // fine.
361         return animateScrollTo(scrollX, scrollY + dy);
362     }
363 
364     /**
365      * See {@link android.webkit.WebView#pageDown(boolean)}
366      */
pageDown(boolean bottom)367     public boolean pageDown(boolean bottom) {
368         final int scrollX = mDelegate.getContainerViewScrollX();
369         final int scrollY = mDelegate.getContainerViewScrollY();
370 
371         if (bottom) {
372             return animateScrollTo(scrollX, computeVerticalScrollRange());
373         }
374         int dy = mContainerViewHeight / 2;
375         if (mContainerViewHeight > 2 * PAGE_SCROLL_OVERLAP) {
376             dy = mContainerViewHeight - PAGE_SCROLL_OVERLAP;
377         }
378         // animateScrollTo clamps the argument to the scrollable range so using (scrollY + dy) is
379         // fine.
380         return animateScrollTo(scrollX, scrollY + dy);
381     }
382 
383     /**
384      * See {@link android.webkit.WebView#requestChildRectangleOnScreen(View, Rect, boolean)}
385      */
requestChildRectangleOnScreen(int childOffsetX, int childOffsetY, Rect rect, boolean immediate)386     public boolean requestChildRectangleOnScreen(int childOffsetX, int childOffsetY, Rect rect,
387             boolean immediate) {
388         // TODO(mkosiba): WebViewClassic immediately returns false if a zoom animation is
389         // in progress. We currently can't tell if one is happening.. should we instead cancel any
390         // scroll animation when the size/pageScaleFactor changes?
391 
392         // TODO(mkosiba): Take scrollbar width into account in the screenRight/screenBotton
393         // calculations. http://crbug.com/269032
394 
395         final int scrollX = mDelegate.getContainerViewScrollX();
396         final int scrollY = mDelegate.getContainerViewScrollY();
397 
398         rect.offset(childOffsetX, childOffsetY);
399 
400         int screenTop = scrollY;
401         int screenBottom = scrollY + mContainerViewHeight;
402         int scrollYDelta = 0;
403 
404         if (rect.bottom > screenBottom) {
405             int oneThirdOfScreenHeight = mContainerViewHeight / 3;
406             if (rect.width() > 2 * oneThirdOfScreenHeight) {
407                 // If the rectangle is too tall to fit in the bottom two thirds
408                 // of the screen, place it at the top.
409                 scrollYDelta = rect.top - screenTop;
410             } else {
411                 // If the rectangle will still fit on screen, we want its
412                 // top to be in the top third of the screen.
413                 scrollYDelta = rect.top - (screenTop + oneThirdOfScreenHeight);
414             }
415         } else if (rect.top < screenTop) {
416             scrollYDelta = rect.top - screenTop;
417         }
418 
419         int screenLeft = scrollX;
420         int screenRight = scrollX + mContainerViewWidth;
421         int scrollXDelta = 0;
422 
423         if (rect.right > screenRight && rect.left > screenLeft) {
424             if (rect.width() > mContainerViewWidth) {
425                 scrollXDelta += (rect.left - screenLeft);
426             } else {
427                 scrollXDelta += (rect.right - screenRight);
428             }
429         } else if (rect.left < screenLeft) {
430             scrollXDelta -= (screenLeft - rect.left);
431         }
432 
433         if (scrollYDelta == 0 && scrollXDelta == 0) {
434             return false;
435         }
436 
437         if (immediate) {
438             scrollBy(scrollXDelta, scrollYDelta);
439             return true;
440         } else {
441             return animateScrollTo(scrollX + scrollXDelta, scrollY + scrollYDelta);
442         }
443     }
444 }
445