- add sources.
[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     // 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;
118     }
119
120     // Called when the physical size of the view changes.
121     public void setContainerViewSize(int width, int height) {
122         mContainerViewWidth = width;
123         mContainerViewHeight = height;
124     }
125
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());
131     }
132
133     public void setProcessingTouchEvent(boolean processingTouchEvent) {
134         assert mProcessingTouchEvent != processingTouchEvent;
135         mProcessingTouchEvent = processingTouchEvent;
136
137         if (!mProcessingTouchEvent && mApplyDeferredNativeScroll) {
138             mApplyDeferredNativeScroll = false;
139             scrollNativeTo(mDeferredNativeScrollX, mDeferredNativeScrollY);
140         }
141     }
142
143     // Called by the native side to scroll the container view.
144     public void scrollContainerViewTo(int x, int y) {
145         mNativeScrollX = x;
146         mNativeScrollY = y;
147
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();
154
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);
159     }
160
161     public boolean isFlingActive() {
162         return mFlinging;
163     }
164
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);
173     }
174
175     private void scrollBy(int deltaX, int deltaY) {
176         if (deltaX == 0 && deltaY == 0) return;
177
178         final int scrollX = mDelegate.getContainerViewScrollX();
179         final int scrollY = mDelegate.getContainerViewScrollY();
180         final int scrollRangeX = computeMaximumHorizontalScrollOffset();
181         final int scrollRangeY = computeMaximumVerticalScrollOffset();
182
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);
187     }
188
189     private int clampHorizontalScroll(int scrollX) {
190         scrollX = Math.max(0, scrollX);
191         scrollX = Math.min(computeMaximumHorizontalScrollOffset(), scrollX);
192         return scrollX;
193     }
194
195     private int clampVerticalScroll(int scrollY) {
196         scrollY = Math.max(0, scrollY);
197         scrollY = Math.min(computeMaximumVerticalScrollOffset(), scrollY);
198         return scrollY;
199     }
200
201     // Called by the View system as a response to the mDelegate.overScrollContainerViewBy call.
202     public void onContainerViewOverScrolled(int scrollX, int scrollY, boolean clampedX,
203             boolean clampedY) {
204         // Clamp the scroll offset at (0, max).
205         scrollX = clampHorizontalScroll(scrollX);
206         scrollY = clampVerticalScroll(scrollY);
207
208         mDelegate.scrollContainerViewTo(scrollX, scrollY);
209
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());
214     }
215
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);
222     }
223
224     private void scrollNativeTo(int x, int y) {
225         x = clampHorizontalScroll(x);
226         y = clampVerticalScroll(y);
227
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;
234             return;
235         }
236
237         if (x == mNativeScrollX && y == mNativeScrollY)
238             return;
239
240         // The scrollNativeTo call should be a simple store, so it's OK to assume it always
241         // succeeds.
242         mNativeScrollX = x;
243         mNativeScrollY = y;
244
245         mDelegate.scrollNativeTo(x, y);
246     }
247
248     // Called at the beginning of every fling gesture.
249     public void onFlingStartGesture(int velocityX, int velocityY) {
250         mLastFlingVelocityX = velocityX;
251         mLastFlingVelocityY = velocityY;
252     }
253
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);
259     }
260
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
263     // scroll layer.
264     public void onUnhandledFlingStartEvent() {
265         flingScroll(-mLastFlingVelocityX, -mLastFlingVelocityY);
266     }
267
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();
275
276         mScroller.fling(scrollX, scrollY, velocityX, velocityY,
277                 0, scrollRangeX, 0, scrollRangeY);
278         mFlinging = true;
279         mDelegate.invalidate();
280     }
281
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) {
286             mFlinging = false;
287             return;
288         }
289
290         final int oldX = mDelegate.getContainerViewScrollX();
291         final int oldY = mDelegate.getContainerViewScrollY();
292         int x = mScroller.getCurrX();
293         int y = mScroller.getCurrY();
294
295         final int scrollRangeX = computeMaximumHorizontalScrollOffset();
296         final int scrollRangeY = computeMaximumVerticalScrollOffset();
297
298         if (overScrollGlow != null) {
299             overScrollGlow.absorbGlow(x, y, oldX, oldY, scrollRangeX, scrollRangeY,
300                     mScroller.getCurrVelocity());
301         }
302
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);
306
307         mDelegate.invalidate();
308     }
309
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);
314     }
315
316     private boolean animateScrollTo(int x, int y) {
317         final int scrollX = mDelegate.getContainerViewScrollX();
318         final int scrollY = mDelegate.getContainerViewScrollY();
319
320         x = clampHorizontalScroll(x);
321         y = clampVerticalScroll(y);
322
323         int dx = x - scrollX;
324         int dy = y - scrollY;
325
326         if (dx == 0 && dy == 0)
327             return false;
328
329         mScroller.startScroll(scrollX, scrollY, dx, dy, computeDurationInMilliSec(dx, dy));
330         mDelegate.invalidate();
331
332         return true;
333     }
334
335     /**
336      * See {@link WebView#pageUp(boolean)}
337      */
338     public boolean pageUp(boolean top) {
339         final int scrollX = mDelegate.getContainerViewScrollX();
340         final int scrollY = mDelegate.getContainerViewScrollY();
341
342         if (top) {
343             // go to the top of the document
344             return animateScrollTo(scrollX, 0);
345         }
346         int dy = -mContainerViewHeight / 2;
347         if (mContainerViewHeight > 2 * PAGE_SCROLL_OVERLAP) {
348             dy = -mContainerViewHeight + PAGE_SCROLL_OVERLAP;
349         }
350         // animateScrollTo clamps the argument to the scrollable range so using (scrollY + dy) is
351         // fine.
352         return animateScrollTo(scrollX, scrollY + dy);
353     }
354
355     /**
356      * See {@link WebView#pageDown(boolean)}
357      */
358     public boolean pageDown(boolean bottom) {
359         final int scrollX = mDelegate.getContainerViewScrollX();
360         final int scrollY = mDelegate.getContainerViewScrollY();
361
362         if (bottom) {
363             return animateScrollTo(scrollX, computeVerticalScrollRange());
364         }
365         int dy = mContainerViewHeight / 2;
366         if (mContainerViewHeight > 2 * PAGE_SCROLL_OVERLAP) {
367             dy = mContainerViewHeight - PAGE_SCROLL_OVERLAP;
368         }
369         // animateScrollTo clamps the argument to the scrollable range so using (scrollY + dy) is
370         // fine.
371         return animateScrollTo(scrollX, scrollY + dy);
372     }
373
374     /**
375      * See {@link WebView#requestChildRectangleOnScreen(View, Rect, boolean)}
376      */
377     public boolean requestChildRectangleOnScreen(int childOffsetX, int childOffsetY, Rect rect,
378             boolean immediate) {
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?
382
383         // TODO(mkosiba): Take scrollbar width into account in the screenRight/screenBotton
384         // calculations. http://crbug.com/269032
385
386         final int scrollX = mDelegate.getContainerViewScrollX();
387         final int scrollY = mDelegate.getContainerViewScrollY();
388
389         rect.offset(childOffsetX, childOffsetY);
390
391         int screenTop = scrollY;
392         int screenBottom = scrollY + mContainerViewHeight;
393         int scrollYDelta = 0;
394
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;
401             } else {
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);
405             }
406         } else if (rect.top < screenTop) {
407             scrollYDelta = rect.top - screenTop;
408         }
409
410         int screenLeft = scrollX;
411         int screenRight = scrollX + mContainerViewWidth;
412         int scrollXDelta = 0;
413
414         if (rect.right > screenRight && rect.left > screenLeft) {
415             if (rect.width() > mContainerViewWidth) {
416                 scrollXDelta += (rect.left - screenLeft);
417             } else {
418                 scrollXDelta += (rect.right - screenRight);
419             }
420         } else if (rect.left < screenLeft) {
421             scrollXDelta -= (screenLeft - rect.left);
422         }
423
424         if (scrollYDelta == 0 && scrollXDelta == 0) {
425             return false;
426         }
427
428         if (immediate) {
429             scrollBy(scrollXDelta, scrollYDelta);
430             return true;
431         } else {
432             return animateScrollTo(scrollX + scrollXDelta, scrollY + scrollYDelta);
433         }
434     }
435 }