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 org.chromium.base.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 // 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 private OverScroller mScroller; 78 79 public AwScrollOffsetManager(Delegate delegate, OverScroller overScroller) { 80 mDelegate = delegate; 81 mScroller = overScroller; 82 } 83 84 //----- Scroll range and extent calculation methods ------------------------------------------- 85 86 public int computeHorizontalScrollRange() { 87 return mContainerViewWidth + mMaxHorizontalScrollOffset; 88 } 89 90 public int computeMaximumHorizontalScrollOffset() { 91 return mMaxHorizontalScrollOffset; 92 } 93 94 public int computeHorizontalScrollOffset() { 95 return mDelegate.getContainerViewScrollX(); 96 } 97 98 public int computeVerticalScrollRange() { 99 return mContainerViewHeight + mMaxVerticalScrollOffset; 100 } 101 102 public int computeMaximumVerticalScrollOffset() { 103 return mMaxVerticalScrollOffset; 104 } 105 106 public int computeVerticalScrollOffset() { 107 return mDelegate.getContainerViewScrollY(); 108 } 109 110 public int computeVerticalScrollExtent() { 111 return mContainerViewHeight; 112 } 113 114 //--------------------------------------------------------------------------------------------- 115 /** 116 * Called when the scroll range changes. This needs to be the size of the on-screen content. 117 */ 118 public void setMaxScrollOffset(int width, int height) { 119 mMaxHorizontalScrollOffset = width; 120 mMaxVerticalScrollOffset = height; 121 } 122 123 /** 124 * Called when the physical size of the view changes. 125 */ 126 public void setContainerViewSize(int width, int height) { 127 mContainerViewWidth = width; 128 mContainerViewHeight = height; 129 } 130 131 public void syncScrollOffsetFromOnDraw() { 132 // Unfortunately apps override onScrollChanged without calling super which is why we need 133 // to sync the scroll offset on every onDraw. 134 onContainerViewScrollChanged(mDelegate.getContainerViewScrollX(), 135 mDelegate.getContainerViewScrollY()); 136 } 137 138 public void setProcessingTouchEvent(boolean processingTouchEvent) { 139 assert mProcessingTouchEvent != processingTouchEvent; 140 mProcessingTouchEvent = processingTouchEvent; 141 142 if (!mProcessingTouchEvent && mApplyDeferredNativeScroll) { 143 mApplyDeferredNativeScroll = false; 144 scrollNativeTo(mDeferredNativeScrollX, mDeferredNativeScrollY); 145 } 146 } 147 148 // Called by the native side to scroll the container view. 149 public void scrollContainerViewTo(int x, int y) { 150 mNativeScrollX = x; 151 mNativeScrollY = y; 152 153 final int scrollX = mDelegate.getContainerViewScrollX(); 154 final int scrollY = mDelegate.getContainerViewScrollY(); 155 final int deltaX = x - scrollX; 156 final int deltaY = y - scrollY; 157 final int scrollRangeX = computeMaximumHorizontalScrollOffset(); 158 final int scrollRangeY = computeMaximumVerticalScrollOffset(); 159 160 // We use overScrollContainerViewBy to be compatible with WebViewClassic which used this 161 // method for handling both over-scroll as well as in-bounds scroll. 162 mDelegate.overScrollContainerViewBy(deltaX, deltaY, scrollX, scrollY, 163 scrollRangeX, scrollRangeY, mProcessingTouchEvent); 164 } 165 166 public boolean isFlingActive() { 167 boolean flinging = mScroller.computeScrollOffset(); 168 mWasFlinging |= flinging; 169 return flinging; 170 } 171 172 // Called by the native side to over-scroll the container view. 173 public void overScrollBy(int deltaX, int deltaY) { 174 // TODO(mkosiba): Once http://crbug.com/260663 and http://crbug.com/261239 are fixed it 175 // should be possible to uncomment the following asserts: 176 // if (deltaX < 0) assert mDelegate.getContainerViewScrollX() == 0; 177 // if (deltaX > 0) assert mDelegate.getContainerViewScrollX() == 178 // computeMaximumHorizontalScrollOffset(); 179 scrollBy(deltaX, deltaY); 180 } 181 182 private void scrollBy(int deltaX, int deltaY) { 183 if (deltaX == 0 && deltaY == 0) return; 184 185 final int scrollX = mDelegate.getContainerViewScrollX(); 186 final int scrollY = mDelegate.getContainerViewScrollY(); 187 final int scrollRangeX = computeMaximumHorizontalScrollOffset(); 188 final int scrollRangeY = computeMaximumVerticalScrollOffset(); 189 190 // The android.view.View.overScrollBy method is used for both scrolling and over-scrolling 191 // which is why we use it here. 192 mDelegate.overScrollContainerViewBy(deltaX, deltaY, scrollX, scrollY, 193 scrollRangeX, scrollRangeY, mProcessingTouchEvent); 194 } 195 196 private int clampHorizontalScroll(int scrollX) { 197 scrollX = Math.max(0, scrollX); 198 scrollX = Math.min(computeMaximumHorizontalScrollOffset(), scrollX); 199 return scrollX; 200 } 201 202 private int clampVerticalScroll(int scrollY) { 203 scrollY = Math.max(0, scrollY); 204 scrollY = Math.min(computeMaximumVerticalScrollOffset(), scrollY); 205 return scrollY; 206 } 207 208 // Called by the View system as a response to the mDelegate.overScrollContainerViewBy call. 209 public void onContainerViewOverScrolled(int scrollX, int scrollY, boolean clampedX, 210 boolean clampedY) { 211 // Clamp the scroll offset at (0, max). 212 scrollX = clampHorizontalScroll(scrollX); 213 scrollY = clampVerticalScroll(scrollY); 214 215 mDelegate.scrollContainerViewTo(scrollX, scrollY); 216 217 // This is only necessary if the containerView scroll offset ends up being different 218 // than the one set from native in which case we want the value stored on the native side 219 // to reflect the value stored in the containerView (and not the other way around). 220 scrollNativeTo(mDelegate.getContainerViewScrollX(), mDelegate.getContainerViewScrollY()); 221 } 222 223 // Called by the View system when the scroll offset had changed. This might not get called if 224 // the embedder overrides WebView#onScrollChanged without calling super.onScrollChanged. If 225 // this method does get called it is called both as a response to the embedder scrolling the 226 // view as well as a response to mDelegate.scrollContainerViewTo. 227 public void onContainerViewScrollChanged(int x, int y) { 228 scrollNativeTo(x, y); 229 } 230 231 private void scrollNativeTo(int x, int y) { 232 x = clampHorizontalScroll(x); 233 y = clampVerticalScroll(y); 234 235 // We shouldn't do the store to native while processing a touch event since that confuses 236 // the gesture processing logic. 237 if (mProcessingTouchEvent) { 238 mDeferredNativeScrollX = x; 239 mDeferredNativeScrollY = y; 240 mApplyDeferredNativeScroll = true; 241 return; 242 } 243 244 if (x == mNativeScrollX && y == mNativeScrollY) 245 return; 246 247 // The scrollNativeTo call should be a simple store, so it's OK to assume it always 248 // succeeds. 249 mNativeScrollX = x; 250 mNativeScrollY = y; 251 252 mDelegate.scrollNativeTo(x, y); 253 } 254 255 // Called whenever some other touch interaction requires the fling gesture to be canceled. 256 public void onFlingCancelGesture() { 257 // TODO(mkosiba): Support speeding up a fling by flinging again. 258 // http://crbug.com/265841 259 mScroller.forceFinished(true); 260 } 261 262 // Called when a fling gesture is not handled by the renderer. 263 // We explicitly ask the renderer not to handle fling gestures targeted at the root 264 // scroll layer. 265 public void onUnhandledFlingStartEvent(int velocityX, int velocityY) { 266 flingScroll(-velocityX, -velocityY); 267 } 268 269 // Starts the fling animation. Called both as a response to a fling gesture and as via the 270 // public WebView#flingScroll(int, int) API. 271 public void flingScroll(int velocityX, int velocityY) { 272 final int scrollX = mDelegate.getContainerViewScrollX(); 273 final int scrollY = mDelegate.getContainerViewScrollY(); 274 final int scrollRangeX = computeMaximumHorizontalScrollOffset(); 275 final int scrollRangeY = computeMaximumVerticalScrollOffset(); 276 277 mScroller.fling(scrollX, scrollY, velocityX, velocityY, 278 0, scrollRangeX, 0, scrollRangeY); 279 mDelegate.invalidate(); 280 } 281 282 // Called immediately before the draw to update the scroll offset. 283 public void computeScrollAndAbsorbGlow(OverScrollGlow overScrollGlow) { 284 if (!mScroller.computeScrollOffset() && !mWasFlinging) { 285 return; 286 } 287 mWasFlinging = false; 288 289 final int oldX = mDelegate.getContainerViewScrollX(); 290 final int oldY = mDelegate.getContainerViewScrollY(); 291 int x = mScroller.getCurrX(); 292 int y = mScroller.getCurrY(); 293 294 final int scrollRangeX = computeMaximumHorizontalScrollOffset(); 295 final int scrollRangeY = computeMaximumVerticalScrollOffset(); 296 297 if (overScrollGlow != null) { 298 overScrollGlow.absorbGlow(x, y, oldX, oldY, scrollRangeX, scrollRangeY, 299 mScroller.getCurrVelocity()); 300 } 301 302 // The mScroller is configured not to go outside of the scrollable range, so this call 303 // should never result in attempting to scroll outside of the scrollable region. 304 scrollBy(x - oldX, y - oldY); 305 306 mDelegate.invalidate(); 307 } 308 309 private static int computeDurationInMilliSec(int dx, int dy) { 310 int distance = Math.max(Math.abs(dx), Math.abs(dy)); 311 int duration = distance * 1000 / STD_SCROLL_ANIMATION_SPEED_PIX_PER_SEC; 312 return Math.min(duration, MAX_SCROLL_ANIMATION_DURATION_MILLISEC); 313 } 314 315 private boolean animateScrollTo(int x, int y) { 316 final int scrollX = mDelegate.getContainerViewScrollX(); 317 final int scrollY = mDelegate.getContainerViewScrollY(); 318 319 x = clampHorizontalScroll(x); 320 y = clampVerticalScroll(y); 321 322 int dx = x - scrollX; 323 int dy = y - scrollY; 324 325 if (dx == 0 && dy == 0) 326 return false; 327 328 mScroller.startScroll(scrollX, scrollY, dx, dy, computeDurationInMilliSec(dx, dy)); 329 mDelegate.invalidate(); 330 331 return true; 332 } 333 334 /** 335 * See {@link android.webkit.WebView#pageUp(boolean)} 336 */ 337 public boolean pageUp(boolean top) { 338 final int scrollX = mDelegate.getContainerViewScrollX(); 339 final int scrollY = mDelegate.getContainerViewScrollY(); 340 341 if (top) { 342 // go to the top of the document 343 return animateScrollTo(scrollX, 0); 344 } 345 int dy = -mContainerViewHeight / 2; 346 if (mContainerViewHeight > 2 * PAGE_SCROLL_OVERLAP) { 347 dy = -mContainerViewHeight + PAGE_SCROLL_OVERLAP; 348 } 349 // animateScrollTo clamps the argument to the scrollable range so using (scrollY + dy) is 350 // fine. 351 return animateScrollTo(scrollX, scrollY + dy); 352 } 353 354 /** 355 * See {@link android.webkit.WebView#pageDown(boolean)} 356 */ 357 public boolean pageDown(boolean bottom) { 358 final int scrollX = mDelegate.getContainerViewScrollX(); 359 final int scrollY = mDelegate.getContainerViewScrollY(); 360 361 if (bottom) { 362 return animateScrollTo(scrollX, computeVerticalScrollRange()); 363 } 364 int dy = mContainerViewHeight / 2; 365 if (mContainerViewHeight > 2 * PAGE_SCROLL_OVERLAP) { 366 dy = mContainerViewHeight - PAGE_SCROLL_OVERLAP; 367 } 368 // animateScrollTo clamps the argument to the scrollable range so using (scrollY + dy) is 369 // fine. 370 return animateScrollTo(scrollX, scrollY + dy); 371 } 372 373 /** 374 * See {@link android.webkit.WebView#requestChildRectangleOnScreen(View, Rect, boolean)} 375 */ 376 public boolean requestChildRectangleOnScreen(int childOffsetX, int childOffsetY, Rect rect, 377 boolean immediate) { 378 // TODO(mkosiba): WebViewClassic immediately returns false if a zoom animation is 379 // in progress. We currently can't tell if one is happening.. should we instead cancel any 380 // scroll animation when the size/pageScaleFactor changes? 381 382 // TODO(mkosiba): Take scrollbar width into account in the screenRight/screenBotton 383 // calculations. http://crbug.com/269032 384 385 final int scrollX = mDelegate.getContainerViewScrollX(); 386 final int scrollY = mDelegate.getContainerViewScrollY(); 387 388 rect.offset(childOffsetX, childOffsetY); 389 390 int screenTop = scrollY; 391 int screenBottom = scrollY + mContainerViewHeight; 392 int scrollYDelta = 0; 393 394 if (rect.bottom > screenBottom) { 395 int oneThirdOfScreenHeight = mContainerViewHeight / 3; 396 if (rect.width() > 2 * oneThirdOfScreenHeight) { 397 // If the rectangle is too tall to fit in the bottom two thirds 398 // of the screen, place it at the top. 399 scrollYDelta = rect.top - screenTop; 400 } else { 401 // If the rectangle will still fit on screen, we want its 402 // top to be in the top third of the screen. 403 scrollYDelta = rect.top - (screenTop + oneThirdOfScreenHeight); 404 } 405 } else if (rect.top < screenTop) { 406 scrollYDelta = rect.top - screenTop; 407 } 408 409 int screenLeft = scrollX; 410 int screenRight = scrollX + mContainerViewWidth; 411 int scrollXDelta = 0; 412 413 if (rect.right > screenRight && rect.left > screenLeft) { 414 if (rect.width() > mContainerViewWidth) { 415 scrollXDelta += (rect.left - screenLeft); 416 } else { 417 scrollXDelta += (rect.right - screenRight); 418 } 419 } else if (rect.left < screenLeft) { 420 scrollXDelta -= (screenLeft - rect.left); 421 } 422 423 if (scrollYDelta == 0 && scrollXDelta == 0) { 424 return false; 425 } 426 427 if (immediate) { 428 scrollBy(scrollXDelta, scrollYDelta); 429 return true; 430 } else { 431 return animateScrollTo(scrollX + scrollXDelta, scrollY + scrollYDelta); 432 } 433 } 434 } 435