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 5package org.chromium.android_webview; 6 7import android.graphics.Rect; 8import android.widget.OverScroller; 9 10import 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 19public 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} 436