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 | import org.chromium.base.CalledByNative; |
13 | |
14 | /** |
15 | * Takes care of syncing the scroll offset between the Android View system and the |
16 | * InProcessViewRenderer. |
17 | * |
18 | * Unless otherwise values (sizes, scroll offsets) are in physical pixels. |
19 | */ |
20 | @VisibleForTesting |
21 | public class AwScrollOffsetManager { |
22 | // Values taken from WebViewClassic. |
23 | |
24 | // The amount of content to overlap between two screens when using pageUp/pageDown methiods. |
25 | private static final int PAGE_SCROLL_OVERLAP = 24; |
26 | // Standard animated scroll speed. |
27 | private static final int STD_SCROLL_ANIMATION_SPEED_PIX_PER_SEC = 480; |
28 | // Time for the longest scroll animation. |
29 | private static final int MAX_SCROLL_ANIMATION_DURATION_MILLISEC = 750; |
30 | |
31 | // The unit of all the values in this delegate are physical pixels. |
32 | public interface Delegate { |
33 | // Call View#overScrollBy on the containerView. |
34 | void overScrollContainerViewBy(int deltaX, int deltaY, int scrollX, int scrollY, |
35 | int scrollRangeX, int scrollRangeY, boolean isTouchEvent); |
36 | // Call View#scrollTo on the containerView. |
37 | void scrollContainerViewTo(int x, int y); |
38 | // Store the scroll offset in the native side. This should really be a simple store |
39 | // operation, the native side shouldn't synchronously alter the scroll offset from within |
40 | // this call. |
41 | void scrollNativeTo(int x, int y); |
42 | |
43 | int getContainerViewScrollX(); |
44 | int getContainerViewScrollY(); |
45 | |
46 | void invalidate(); |
47 | } |
48 | |
49 | private final Delegate mDelegate; |
50 | |
51 | // Scroll offset as seen by the native side. |
52 | private int mNativeScrollX; |
53 | private int mNativeScrollY; |
54 | |
55 | // Content size. |
56 | private int mContentWidth; |
57 | private int mContentHeight; |
58 | |
59 | // Size of the container view. |
60 | private int mContainerViewWidth; |
61 | private int mContainerViewHeight; |
62 | |
63 | // Whether we're in the middle of processing a touch event. |
64 | private boolean mProcessingTouchEvent; |
65 | |
66 | // Whether (and to what value) to update the native side scroll offset after we've finished |
67 | // provessing 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 Math.max(mContainerViewWidth, mContentWidth); |
87 | } |
88 | |
89 | public int computeMaximumHorizontalScrollOffset() { |
90 | return computeHorizontalScrollRange() - mContainerViewWidth; |
91 | } |
92 | |
93 | public int computeHorizontalScrollOffset() { |
94 | return mDelegate.getContainerViewScrollX(); |
95 | } |
96 | |
97 | public int computeVerticalScrollRange() { |
98 | return Math.max(mContainerViewHeight, mContentHeight); |
99 | } |
100 | |
101 | public int computeMaximumVerticalScrollOffset() { |
102 | return computeVerticalScrollRange() - mContainerViewHeight; |
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 content size changes. This needs to be the size of the on-screen content and |
116 | // therefore we can't use the WebContentsDelegate preferred size. |
117 | public void setContentSize(int width, int height) { |
118 | mContentWidth = width; |
119 | mContentHeight = height; |
120 | } |
121 | |
122 | // Called when the physical size of the view changes. |
123 | public void setContainerViewSize(int width, int height) { |
124 | mContainerViewWidth = width; |
125 | mContainerViewHeight = height; |
126 | } |
127 | |
128 | public void syncScrollOffsetFromOnDraw() { |
129 | // Unfortunately apps override onScrollChanged without calling super which is why we need |
130 | // to sync the scroll offset on every onDraw. |
131 | onContainerViewScrollChanged(mDelegate.getContainerViewScrollX(), |
132 | mDelegate.getContainerViewScrollY()); |
133 | } |
134 | |
135 | public void setProcessingTouchEvent(boolean processingTouchEvent) { |
136 | assert mProcessingTouchEvent != processingTouchEvent; |
137 | mProcessingTouchEvent = processingTouchEvent; |
138 | |
139 | if (!mProcessingTouchEvent && mApplyDeferredNativeScroll) { |
140 | mApplyDeferredNativeScroll = false; |
141 | scrollNativeTo(mDeferredNativeScrollX, mDeferredNativeScrollY); |
142 | } |
143 | } |
144 | |
145 | // Called by the native side to attempt to scroll the container view. |
146 | public void scrollContainerViewTo(int x, int y) { |
147 | mNativeScrollX = x; |
148 | mNativeScrollY = y; |
149 | |
150 | final int scrollX = mDelegate.getContainerViewScrollX(); |
151 | final int scrollY = mDelegate.getContainerViewScrollY(); |
152 | final int deltaX = x - scrollX; |
153 | final int deltaY = y - scrollY; |
154 | final int scrollRangeX = computeMaximumHorizontalScrollOffset(); |
155 | final int scrollRangeY = computeMaximumVerticalScrollOffset(); |
156 | |
157 | // We use overScrollContainerViewBy to be compatible with WebViewClassic which used this |
158 | // method for handling both over-scroll as well as in-bounds scroll. |
159 | mDelegate.overScrollContainerViewBy(deltaX, deltaY, scrollX, scrollY, |
160 | scrollRangeX, scrollRangeY, mProcessingTouchEvent); |
161 | } |
162 | |
163 | // Called by the native side to over-scroll the container view. |
164 | public void overScrollBy(int deltaX, int deltaY) { |
165 | // TODO(mkosiba): Once http://crbug.com/260663 and http://crbug.com/261239 are fixed it |
166 | // should be possible to uncomment the following asserts: |
167 | // if (deltaX < 0) assert mDelegate.getContainerViewScrollX() == 0; |
168 | // if (deltaX > 0) assert mDelegate.getContainerViewScrollX() == |
169 | // computeMaximumHorizontalScrollOffset(); |
170 | scrollBy(deltaX, deltaY); |
171 | } |
172 | |
173 | private void scrollBy(int deltaX, int deltaY) { |
174 | if (deltaX == 0 && deltaY == 0) return; |
175 | |
176 | final int scrollX = mDelegate.getContainerViewScrollX(); |
177 | final int scrollY = mDelegate.getContainerViewScrollY(); |
178 | final int scrollRangeX = computeMaximumHorizontalScrollOffset(); |
179 | final int scrollRangeY = computeMaximumVerticalScrollOffset(); |
180 | |
181 | // The android.view.View.overScrollBy method is used for both scrolling and over-scrolling |
182 | // which is why we use it here. |
183 | mDelegate.overScrollContainerViewBy(deltaX, deltaY, scrollX, scrollY, |
184 | scrollRangeX, scrollRangeY, mProcessingTouchEvent); |
185 | } |
186 | |
187 | private int clampHorizontalScroll(int scrollX) { |
188 | scrollX = Math.max(0, scrollX); |
189 | scrollX = Math.min(computeMaximumHorizontalScrollOffset(), scrollX); |
190 | return scrollX; |
191 | } |
192 | |
193 | private int clampVerticalScroll(int scrollY) { |
194 | scrollY = Math.max(0, scrollY); |
195 | scrollY = Math.min(computeMaximumVerticalScrollOffset(), scrollY); |
196 | return scrollY; |
197 | } |
198 | |
199 | // Called by the View system as a response to the mDelegate.overScrollContainerViewBy call. |
200 | public void onContainerViewOverScrolled(int scrollX, int scrollY, boolean clampedX, |
201 | boolean clampedY) { |
202 | // Clamp the scroll offset at (0, max). |
203 | scrollX = clampHorizontalScroll(scrollX); |
204 | scrollY = clampVerticalScroll(scrollY); |
205 | |
206 | mDelegate.scrollContainerViewTo(scrollX, scrollY); |
207 | |
208 | // This is only necessary if the containerView scroll offset ends up being different |
209 | // than the one set from native in which case we want the value stored on the native side |
210 | // to reflect the value stored in the containerView (and not the other way around). |
211 | scrollNativeTo(mDelegate.getContainerViewScrollX(), mDelegate.getContainerViewScrollY()); |
212 | } |
213 | |
214 | // Called by the View system when the scroll offset had changed. This might not get called if |
215 | // the embedder overrides WebView#onScrollChanged without calling super.onScrollChanged. If |
216 | // this method does get called it is called both as a response to the embedder scrolling the |
217 | // view as well as a response to mDelegate.scrollContainerViewTo. |
218 | public void onContainerViewScrollChanged(int x, int y) { |
219 | scrollNativeTo(x, y); |
220 | } |
221 | |
222 | private void scrollNativeTo(int x, int y) { |
223 | x = clampHorizontalScroll(x); |
224 | y = clampVerticalScroll(y); |
225 | |
226 | // We shouldn't do the store to native while processing a touch event since that confuses |
227 | // the gesture processing logic. |
228 | if (mProcessingTouchEvent) { |
229 | mDeferredNativeScrollX = x; |
230 | mDeferredNativeScrollY = y; |
231 | mApplyDeferredNativeScroll = true; |
232 | return; |
233 | } |
234 | |
235 | if (x == mNativeScrollX && y == mNativeScrollY) |
236 | return; |
237 | |
238 | // The scrollNativeTo call should be a simple store, so it's OK to assume it always |
239 | // succeeds. |
240 | mNativeScrollX = x; |
241 | mNativeScrollY = y; |
242 | |
243 | mDelegate.scrollNativeTo(x, y); |
244 | } |
245 | |
246 | // Called at the beginning of every fling gesture. |
247 | public void onFlingStartGesture(int velocityX, int velocityY) { |
248 | mLastFlingVelocityX = velocityX; |
249 | mLastFlingVelocityY = velocityY; |
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() { |
263 | flingScroll(-mLastFlingVelocityX, -mLastFlingVelocityY); |
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 rangeX = computeMaximumHorizontalScrollOffset(); |
272 | final int rangeY = computeMaximumVerticalScrollOffset(); |
273 | |
274 | mScroller.fling(scrollX, scrollY, velocityX, velocityY, |
275 | 0, rangeX, 0, rangeY); |
276 | mDelegate.invalidate(); |
277 | } |
278 | |
279 | // Called immediately before the draw to update the scroll offset. |
280 | public void computeScrollAndAbsorbGlow(OverScrollGlow overScrollGlow) { |
281 | final boolean stillAnimating = mScroller.computeScrollOffset(); |
282 | if (!stillAnimating) return; |
283 | |
284 | final int oldX = mDelegate.getContainerViewScrollX(); |
285 | final int oldY = mDelegate.getContainerViewScrollY(); |
286 | int x = mScroller.getCurrX(); |
287 | int y = mScroller.getCurrY(); |
288 | |
289 | int rangeX = computeMaximumHorizontalScrollOffset(); |
290 | int rangeY = computeMaximumVerticalScrollOffset(); |
291 | |
292 | if (overScrollGlow != null) { |
293 | overScrollGlow.absorbGlow(x, y, oldX, oldY, rangeX, rangeY, |
294 | mScroller.getCurrVelocity()); |
295 | } |
296 | |
297 | // The mScroller is configured not to go outside of the scrollable range, so this call |
298 | // should never result in attempting to scroll outside of the scrollable region. |
299 | scrollBy(x - oldX, y - oldY); |
300 | |
301 | mDelegate.invalidate(); |
302 | } |
303 | |
304 | private static int computeDurationInMilliSec(int dx, int dy) { |
305 | int distance = Math.max(Math.abs(dx), Math.abs(dy)); |
306 | int duration = distance * 1000 / STD_SCROLL_ANIMATION_SPEED_PIX_PER_SEC; |
307 | return Math.min(duration, MAX_SCROLL_ANIMATION_DURATION_MILLISEC); |
308 | } |
309 | |
310 | private boolean animateScrollTo(int x, int y) { |
311 | final int scrollX = mDelegate.getContainerViewScrollX(); |
312 | final int scrollY = mDelegate.getContainerViewScrollY(); |
313 | |
314 | x = clampHorizontalScroll(x); |
315 | y = clampVerticalScroll(y); |
316 | |
317 | int dx = x - scrollX; |
318 | int dy = y - scrollY; |
319 | |
320 | if (dx == 0 && dy == 0) |
321 | return false; |
322 | |
323 | mScroller.startScroll(scrollX, scrollY, dx, dy, computeDurationInMilliSec(dx, dy)); |
324 | mDelegate.invalidate(); |
325 | |
326 | return true; |
327 | } |
328 | |
329 | /** |
330 | * See {@link WebView#pageUp(boolean)} |
331 | */ |
332 | public boolean pageUp(boolean top) { |
333 | final int scrollX = mDelegate.getContainerViewScrollX(); |
334 | final int scrollY = mDelegate.getContainerViewScrollY(); |
335 | |
336 | if (top) { |
337 | // go to the top of the document |
338 | return animateScrollTo(scrollX, 0); |
339 | } |
340 | int dy = -mContainerViewHeight / 2; |
341 | if (mContainerViewHeight > 2 * PAGE_SCROLL_OVERLAP) { |
342 | dy = -mContainerViewHeight + PAGE_SCROLL_OVERLAP; |
343 | } |
344 | // animateScrollTo clamps the argument to the scrollable range so using (scrollY + dy) is |
345 | // fine. |
346 | return animateScrollTo(scrollX, scrollY + dy); |
347 | } |
348 | |
349 | /** |
350 | * See {@link WebView#pageDown(boolean)} |
351 | */ |
352 | public boolean pageDown(boolean bottom) { |
353 | final int scrollX = mDelegate.getContainerViewScrollX(); |
354 | final int scrollY = mDelegate.getContainerViewScrollY(); |
355 | |
356 | if (bottom) { |
357 | return animateScrollTo(scrollX, computeVerticalScrollRange()); |
358 | } |
359 | int dy = mContainerViewHeight / 2; |
360 | if (mContainerViewHeight > 2 * PAGE_SCROLL_OVERLAP) { |
361 | dy = mContainerViewHeight - PAGE_SCROLL_OVERLAP; |
362 | } |
363 | // animateScrollTo clamps the argument to the scrollable range so using (scrollY + dy) is |
364 | // fine. |
365 | return animateScrollTo(scrollX, scrollY + dy); |
366 | } |
367 | |
368 | /** |
369 | * See {@link WebView#requestChildRectangleOnScreen(View, Rect, boolean)} |
370 | */ |
371 | public boolean requestChildRectangleOnScreen(int childOffsetX, int childOffsetY, Rect rect, |
372 | boolean immediate) { |
373 | // TODO(mkosiba): WebViewClassic immediately returns false if a zoom animation is |
374 | // in progress. We currently can't tell if one is happening.. should we instead cancel any |
375 | // scroll animation when the size/pageScaleFactor changes? |
376 | |
377 | // TODO(mkosiba): Take scrollbar width into account in the screenRight/screenBotton |
378 | // calculations. http://crbug.com/269032 |
379 | |
380 | final int scrollX = mDelegate.getContainerViewScrollX(); |
381 | final int scrollY = mDelegate.getContainerViewScrollY(); |
382 | |
383 | rect.offset(childOffsetX, childOffsetY); |
384 | |
385 | int screenTop = scrollY; |
386 | int screenBottom = scrollY + mContainerViewHeight; |
387 | int scrollYDelta = 0; |
388 | |
389 | if (rect.bottom > screenBottom) { |
390 | int oneThirdOfScreenHeight = mContainerViewHeight / 3; |
391 | if (rect.width() > 2 * oneThirdOfScreenHeight) { |
392 | // If the rectangle is too tall to fit in the bottom two thirds |
393 | // of the screen, place it at the top. |
394 | scrollYDelta = rect.top - screenTop; |
395 | } else { |
396 | // If the rectangle will still fit on screen, we want its |
397 | // top to be in the top third of the screen. |
398 | scrollYDelta = rect.top - (screenTop + oneThirdOfScreenHeight); |
399 | } |
400 | } else if (rect.top < screenTop) { |
401 | scrollYDelta = rect.top - screenTop; |
402 | } |
403 | |
404 | int screenLeft = scrollX; |
405 | int screenRight = scrollX + mContainerViewWidth; |
406 | int scrollXDelta = 0; |
407 | |
408 | if (rect.right > screenRight && rect.left > screenLeft) { |
409 | if (rect.width() > mContainerViewWidth) { |
410 | scrollXDelta += (rect.left - screenLeft); |
411 | } else { |
412 | scrollXDelta += (rect.right - screenRight); |
413 | } |
414 | } else if (rect.left < screenLeft) { |
415 | scrollXDelta -= (screenLeft - rect.left); |
416 | } |
417 | |
418 | if (scrollYDelta == 0 && scrollXDelta == 0) { |
419 | return false; |
420 | } |
421 | |
422 | if (immediate) { |
423 | scrollBy(scrollXDelta, scrollYDelta); |
424 | return true; |
425 | } else { |
426 | return animateScrollTo(scrollX + scrollXDelta, scrollY + scrollYDelta); |
427 | } |
428 | } |
429 | } |