Upstream version 5.34.92.0
[platform/framework/web/crosswalk.git] / src / android_webview / java / src / org / chromium / android_webview / AwScrollOffsetManager.java
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     // 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
38         // this call.
39         void scrollNativeTo(int x, int y);
40
41         int getContainerViewScrollX();
42         int getContainerViewScrollY();
43
44         void invalidate();
45     }
46
47     private final Delegate mDelegate;
48
49     // Scroll offset as seen by the native side.
50     private int mNativeScrollX;
51     private int mNativeScrollY;
52
53     // How many pixels can we scroll in a given direction.
54     private int mMaxHorizontalScrollOffset;
55     private int mMaxVerticalScrollOffset;
56
57     // Size of the container view.
58     private int mContainerViewWidth;
59     private int mContainerViewHeight;
60
61     // Whether we're in the middle of processing a touch event.
62     private boolean mProcessingTouchEvent;
63
64     private boolean mFlinging;
65
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;
71
72     // The velocity of the last recorded fling,
73     private int mLastFlingVelocityX;
74     private int mLastFlingVelocityY;
75
76     private OverScroller mScroller;
77
78     public AwScrollOffsetManager(Delegate delegate, OverScroller overScroller) {
79         mDelegate = delegate;
80         mScroller = overScroller;
81     }
82
83     //----- Scroll range and extent calculation methods -------------------------------------------
84
85     public int computeHorizontalScrollRange() {
86         return mContainerViewWidth + mMaxHorizontalScrollOffset;
87     }
88
89     public int computeMaximumHorizontalScrollOffset() {
90         return mMaxHorizontalScrollOffset;
91     }
92
93     public int computeHorizontalScrollOffset() {
94         return mDelegate.getContainerViewScrollX();
95     }
96
97     public int computeVerticalScrollRange() {
98         return mContainerViewHeight + mMaxVerticalScrollOffset;
99     }
100
101     public int computeMaximumVerticalScrollOffset() {
102         return mMaxVerticalScrollOffset;
103     }
104
105     public int computeVerticalScrollOffset() {
106         return mDelegate.getContainerViewScrollY();
107     }
108
109     public int computeVerticalScrollExtent() {
110         return mContainerViewHeight;
111     }
112
113     //---------------------------------------------------------------------------------------------
114     /**
115      * Called when the scroll range changes. This needs to be the size of the on-screen content.
116      */
117     public void setMaxScrollOffset(int width, int height) {
118         mMaxHorizontalScrollOffset = width;
119         mMaxVerticalScrollOffset = height;
120     }
121
122     /**
123      * Called when the physical size of the view changes.
124      */
125     public void setContainerViewSize(int width, int height) {
126         mContainerViewWidth = width;
127         mContainerViewHeight = height;
128     }
129
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());
135     }
136
137     public void setProcessingTouchEvent(boolean processingTouchEvent) {
138         assert mProcessingTouchEvent != processingTouchEvent;
139         mProcessingTouchEvent = processingTouchEvent;
140
141         if (!mProcessingTouchEvent && mApplyDeferredNativeScroll) {
142             mApplyDeferredNativeScroll = false;
143             scrollNativeTo(mDeferredNativeScrollX, mDeferredNativeScrollY);
144         }
145     }
146
147     // Called by the native side to scroll the container view.
148     public void scrollContainerViewTo(int x, int y) {
149         mNativeScrollX = x;
150         mNativeScrollY = y;
151
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();
158
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);
163     }
164
165     public boolean isFlingActive() {
166         return mFlinging;
167     }
168
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);
177     }
178
179     private void scrollBy(int deltaX, int deltaY) {
180         if (deltaX == 0 && deltaY == 0) return;
181
182         final int scrollX = mDelegate.getContainerViewScrollX();
183         final int scrollY = mDelegate.getContainerViewScrollY();
184         final int scrollRangeX = computeMaximumHorizontalScrollOffset();
185         final int scrollRangeY = computeMaximumVerticalScrollOffset();
186
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);
191     }
192
193     private int clampHorizontalScroll(int scrollX) {
194         scrollX = Math.max(0, scrollX);
195         scrollX = Math.min(computeMaximumHorizontalScrollOffset(), scrollX);
196         return scrollX;
197     }
198
199     private int clampVerticalScroll(int scrollY) {
200         scrollY = Math.max(0, scrollY);
201         scrollY = Math.min(computeMaximumVerticalScrollOffset(), scrollY);
202         return scrollY;
203     }
204
205     // Called by the View system as a response to the mDelegate.overScrollContainerViewBy call.
206     public void onContainerViewOverScrolled(int scrollX, int scrollY, boolean clampedX,
207             boolean clampedY) {
208         // Clamp the scroll offset at (0, max).
209         scrollX = clampHorizontalScroll(scrollX);
210         scrollY = clampVerticalScroll(scrollY);
211
212         mDelegate.scrollContainerViewTo(scrollX, scrollY);
213
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());
218     }
219
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);
226     }
227
228     private void scrollNativeTo(int x, int y) {
229         x = clampHorizontalScroll(x);
230         y = clampVerticalScroll(y);
231
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;
238             return;
239         }
240
241         if (x == mNativeScrollX && y == mNativeScrollY)
242             return;
243
244         // The scrollNativeTo call should be a simple store, so it's OK to assume it always
245         // succeeds.
246         mNativeScrollX = x;
247         mNativeScrollY = y;
248
249         mDelegate.scrollNativeTo(x, y);
250     }
251
252     // Called at the beginning of every fling gesture.
253     public void onFlingStartGesture(int velocityX, int velocityY) {
254         mLastFlingVelocityX = velocityX;
255         mLastFlingVelocityY = velocityY;
256     }
257
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);
263     }
264
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
267     // scroll layer.
268     public void onUnhandledFlingStartEvent() {
269         flingScroll(-mLastFlingVelocityX, -mLastFlingVelocityY);
270     }
271
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();
279
280         mScroller.fling(scrollX, scrollY, velocityX, velocityY,
281                 0, scrollRangeX, 0, scrollRangeY);
282         mFlinging = true;
283         mDelegate.invalidate();
284     }
285
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) {
290             mFlinging = false;
291             return;
292         }
293
294         final int oldX = mDelegate.getContainerViewScrollX();
295         final int oldY = mDelegate.getContainerViewScrollY();
296         int x = mScroller.getCurrX();
297         int y = mScroller.getCurrY();
298
299         final int scrollRangeX = computeMaximumHorizontalScrollOffset();
300         final int scrollRangeY = computeMaximumVerticalScrollOffset();
301
302         if (overScrollGlow != null) {
303             overScrollGlow.absorbGlow(x, y, oldX, oldY, scrollRangeX, scrollRangeY,
304                     mScroller.getCurrVelocity());
305         }
306
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);
310
311         mDelegate.invalidate();
312     }
313
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);
318     }
319
320     private boolean animateScrollTo(int x, int y) {
321         final int scrollX = mDelegate.getContainerViewScrollX();
322         final int scrollY = mDelegate.getContainerViewScrollY();
323
324         x = clampHorizontalScroll(x);
325         y = clampVerticalScroll(y);
326
327         int dx = x - scrollX;
328         int dy = y - scrollY;
329
330         if (dx == 0 && dy == 0)
331             return false;
332
333         mScroller.startScroll(scrollX, scrollY, dx, dy, computeDurationInMilliSec(dx, dy));
334         mDelegate.invalidate();
335
336         return true;
337     }
338
339     /**
340      * See {@link android.webkit.WebView#pageUp(boolean)}
341      */
342     public boolean pageUp(boolean top) {
343         final int scrollX = mDelegate.getContainerViewScrollX();
344         final int scrollY = mDelegate.getContainerViewScrollY();
345
346         if (top) {
347             // go to the top of the document
348             return animateScrollTo(scrollX, 0);
349         }
350         int dy = -mContainerViewHeight / 2;
351         if (mContainerViewHeight > 2 * PAGE_SCROLL_OVERLAP) {
352             dy = -mContainerViewHeight + PAGE_SCROLL_OVERLAP;
353         }
354         // animateScrollTo clamps the argument to the scrollable range so using (scrollY + dy) is
355         // fine.
356         return animateScrollTo(scrollX, scrollY + dy);
357     }
358
359     /**
360      * See {@link android.webkit.WebView#pageDown(boolean)}
361      */
362     public boolean pageDown(boolean bottom) {
363         final int scrollX = mDelegate.getContainerViewScrollX();
364         final int scrollY = mDelegate.getContainerViewScrollY();
365
366         if (bottom) {
367             return animateScrollTo(scrollX, computeVerticalScrollRange());
368         }
369         int dy = mContainerViewHeight / 2;
370         if (mContainerViewHeight > 2 * PAGE_SCROLL_OVERLAP) {
371             dy = mContainerViewHeight - PAGE_SCROLL_OVERLAP;
372         }
373         // animateScrollTo clamps the argument to the scrollable range so using (scrollY + dy) is
374         // fine.
375         return animateScrollTo(scrollX, scrollY + dy);
376     }
377
378     /**
379      * See {@link android.webkit.WebView#requestChildRectangleOnScreen(View, Rect, boolean)}
380      */
381     public boolean requestChildRectangleOnScreen(int childOffsetX, int childOffsetY, Rect rect,
382             boolean immediate) {
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?
386
387         // TODO(mkosiba): Take scrollbar width into account in the screenRight/screenBotton
388         // calculations. http://crbug.com/269032
389
390         final int scrollX = mDelegate.getContainerViewScrollX();
391         final int scrollY = mDelegate.getContainerViewScrollY();
392
393         rect.offset(childOffsetX, childOffsetY);
394
395         int screenTop = scrollY;
396         int screenBottom = scrollY + mContainerViewHeight;
397         int scrollYDelta = 0;
398
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;
405             } else {
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);
409             }
410         } else if (rect.top < screenTop) {
411             scrollYDelta = rect.top - screenTop;
412         }
413
414         int screenLeft = scrollX;
415         int screenRight = scrollX + mContainerViewWidth;
416         int scrollXDelta = 0;
417
418         if (rect.right > screenRight && rect.left > screenLeft) {
419             if (rect.width() > mContainerViewWidth) {
420                 scrollXDelta += (rect.left - screenLeft);
421             } else {
422                 scrollXDelta += (rect.right - screenRight);
423             }
424         } else if (rect.left < screenLeft) {
425             scrollXDelta -= (screenLeft - rect.left);
426         }
427
428         if (scrollYDelta == 0 && scrollXDelta == 0) {
429             return false;
430         }
431
432         if (immediate) {
433             scrollBy(scrollXDelta, scrollYDelta);
434             return true;
435         } else {
436             return animateScrollTo(scrollX + scrollXDelta, scrollY + scrollYDelta);
437         }
438     }
439 }