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