Upstream version 5.34.104.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     /**
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.
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
42         // this call.
43         void scrollNativeTo(int x, int y);
44
45         int getContainerViewScrollX();
46         int getContainerViewScrollY();
47
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     private boolean mFlinging;
69
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;
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 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);
257     }
258
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
261     // scroll layer.
262     public void onUnhandledFlingStartEvent(int velocityX, int velocityY) {
263         flingScroll(-velocityX, -velocityY);
264     }
265
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();
273
274         mScroller.fling(scrollX, scrollY, velocityX, velocityY,
275                 0, scrollRangeX, 0, scrollRangeY);
276         mDelegate.invalidate();
277     }
278
279     // Called immediately before the draw to update the scroll offset.
280     public void computeScrollAndAbsorbGlow(OverScrollGlow overScrollGlow) {
281         mFlinging = mScroller.computeScrollOffset();
282         if (!mFlinging) {
283             return;
284         }
285
286         final int oldX = mDelegate.getContainerViewScrollX();
287         final int oldY = mDelegate.getContainerViewScrollY();
288         int x = mScroller.getCurrX();
289         int y = mScroller.getCurrY();
290
291         final int scrollRangeX = computeMaximumHorizontalScrollOffset();
292         final int scrollRangeY = computeMaximumVerticalScrollOffset();
293
294         if (overScrollGlow != null) {
295             overScrollGlow.absorbGlow(x, y, oldX, oldY, scrollRangeX, scrollRangeY,
296                     mScroller.getCurrVelocity());
297         }
298
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);
302
303         mDelegate.invalidate();
304     }
305
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);
310     }
311
312     private boolean animateScrollTo(int x, int y) {
313         final int scrollX = mDelegate.getContainerViewScrollX();
314         final int scrollY = mDelegate.getContainerViewScrollY();
315
316         x = clampHorizontalScroll(x);
317         y = clampVerticalScroll(y);
318
319         int dx = x - scrollX;
320         int dy = y - scrollY;
321
322         if (dx == 0 && dy == 0)
323             return false;
324
325         mScroller.startScroll(scrollX, scrollY, dx, dy, computeDurationInMilliSec(dx, dy));
326         mDelegate.invalidate();
327
328         return true;
329     }
330
331     /**
332      * See {@link android.webkit.WebView#pageUp(boolean)}
333      */
334     public boolean pageUp(boolean top) {
335         final int scrollX = mDelegate.getContainerViewScrollX();
336         final int scrollY = mDelegate.getContainerViewScrollY();
337
338         if (top) {
339             // go to the top of the document
340             return animateScrollTo(scrollX, 0);
341         }
342         int dy = -mContainerViewHeight / 2;
343         if (mContainerViewHeight > 2 * PAGE_SCROLL_OVERLAP) {
344             dy = -mContainerViewHeight + PAGE_SCROLL_OVERLAP;
345         }
346         // animateScrollTo clamps the argument to the scrollable range so using (scrollY + dy) is
347         // fine.
348         return animateScrollTo(scrollX, scrollY + dy);
349     }
350
351     /**
352      * See {@link android.webkit.WebView#pageDown(boolean)}
353      */
354     public boolean pageDown(boolean bottom) {
355         final int scrollX = mDelegate.getContainerViewScrollX();
356         final int scrollY = mDelegate.getContainerViewScrollY();
357
358         if (bottom) {
359             return animateScrollTo(scrollX, computeVerticalScrollRange());
360         }
361         int dy = mContainerViewHeight / 2;
362         if (mContainerViewHeight > 2 * PAGE_SCROLL_OVERLAP) {
363             dy = mContainerViewHeight - PAGE_SCROLL_OVERLAP;
364         }
365         // animateScrollTo clamps the argument to the scrollable range so using (scrollY + dy) is
366         // fine.
367         return animateScrollTo(scrollX, scrollY + dy);
368     }
369
370     /**
371      * See {@link android.webkit.WebView#requestChildRectangleOnScreen(View, Rect, boolean)}
372      */
373     public boolean requestChildRectangleOnScreen(int childOffsetX, int childOffsetY, Rect rect,
374             boolean immediate) {
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?
378
379         // TODO(mkosiba): Take scrollbar width into account in the screenRight/screenBotton
380         // calculations. http://crbug.com/269032
381
382         final int scrollX = mDelegate.getContainerViewScrollX();
383         final int scrollY = mDelegate.getContainerViewScrollY();
384
385         rect.offset(childOffsetX, childOffsetY);
386
387         int screenTop = scrollY;
388         int screenBottom = scrollY + mContainerViewHeight;
389         int scrollYDelta = 0;
390
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;
397             } else {
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);
401             }
402         } else if (rect.top < screenTop) {
403             scrollYDelta = rect.top - screenTop;
404         }
405
406         int screenLeft = scrollX;
407         int screenRight = scrollX + mContainerViewWidth;
408         int scrollXDelta = 0;
409
410         if (rect.right > screenRight && rect.left > screenLeft) {
411             if (rect.width() > mContainerViewWidth) {
412                 scrollXDelta += (rect.left - screenLeft);
413             } else {
414                 scrollXDelta += (rect.right - screenRight);
415             }
416         } else if (rect.left < screenLeft) {
417             scrollXDelta -= (screenLeft - rect.left);
418         }
419
420         if (scrollYDelta == 0 && scrollXDelta == 0) {
421             return false;
422         }
423
424         if (immediate) {
425             scrollBy(scrollXDelta, scrollYDelta);
426             return true;
427         } else {
428             return animateScrollTo(scrollX + scrollXDelta, scrollY + scrollYDelta);
429         }
430     }
431 }