// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.android_webview;
import android.graphics.Rect;
import org.chromium.android_webview.common.Lifetime;
/**
* Takes care of syncing the scroll offset between the Android View system and the
* InProcessViewRenderer.
*
* Unless otherwise values (sizes, scroll offsets) are in physical pixels.
*/
@Lifetime.WebView
public class AwScrollOffsetManager {
// Values taken from WebViewClassic.
// The amount of content to overlap between two screens when using pageUp/pageDown methiods.
private static final int PAGE_SCROLL_OVERLAP = 24;
// Standard animated scroll speed.
private static final int STD_SCROLL_ANIMATION_SPEED_PIX_PER_SEC = 480;
// Time for the longest scroll animation.
private static final int MAX_SCROLL_ANIMATION_DURATION_MILLISEC = 750;
/**
* The interface that all users of AwScrollOffsetManager should implement.
*
* The unit of all the values in this delegate are physical pixels.
*/
public interface Delegate {
// Call View#overScrollBy on the containerView.
void overScrollContainerViewBy(
int deltaX,
int deltaY,
int scrollX,
int scrollY,
int scrollRangeX,
int scrollRangeY,
boolean isTouchEvent);
// Call View#scrollTo on the containerView.
void scrollContainerViewTo(int x, int y);
// Store the scroll offset in the native side. This should really be a simple store
// operation, the native side shouldn't synchronously alter the scroll offset from within
// this call.
void scrollNativeTo(int x, int y);
/**
* Smooth scrolls the view to targetX, targetY, within durationMs.
* @param targetX x-coordinate of target scroll position.
* @param targetY y-coordinate of target scroll position.
* @param durationMs the animation duration in milliseconds.
*/
void smoothScroll(int targetX, int targetY, long durationMs);
int getContainerViewScrollX();
int getContainerViewScrollY();
void invalidate();
void cancelFling();
}
private final Delegate mDelegate;
// Scroll offset as seen by the native side.
private int mNativeScrollX;
private int mNativeScrollY;
// How many pixels can we scroll in a given direction.
private int mMaxHorizontalScrollOffset;
private int mMaxVerticalScrollOffset;
// Size of the container view.
private int mContainerViewWidth;
private int mContainerViewHeight;
// Whether we're in the middle of processing a touch event.
private boolean mProcessingTouchEvent;
// Whether (and to what value) to update the native side scroll offset after we've finished
// processing a touch event.
private boolean mApplyDeferredNativeScroll;
private int mDeferredNativeScrollX;
private int mDeferredNativeScrollY;
public AwScrollOffsetManager(Delegate delegate) {
mDelegate = delegate;
}
// ----- Scroll range and extent calculation methods -------------------------------------------
public int computeHorizontalScrollRange() {
return mContainerViewWidth + mMaxHorizontalScrollOffset;
}
public int computeMaximumHorizontalScrollOffset() {
return mMaxHorizontalScrollOffset;
}
public int computeHorizontalScrollOffset() {
return mDelegate.getContainerViewScrollX();
}
public int computeVerticalScrollRange() {
return mContainerViewHeight + mMaxVerticalScrollOffset;
}
public int computeMaximumVerticalScrollOffset() {
return mMaxVerticalScrollOffset;
}
public int computeVerticalScrollOffset() {
return mDelegate.getContainerViewScrollY();
}
public int computeVerticalScrollExtent() {
return mContainerViewHeight;
}
// ---------------------------------------------------------------------------------------------
/** Called when the scroll range changes. This needs to be the size of the on-screen content. */
public void setMaxScrollOffset(int width, int height) {
mMaxHorizontalScrollOffset = width;
mMaxVerticalScrollOffset = height;
}
/** Called when the physical size of the view changes. */
public void setContainerViewSize(int width, int height) {
mContainerViewWidth = width;
mContainerViewHeight = height;
}
public void syncScrollOffsetFromOnDraw() {
// Unfortunately apps override onScrollChanged without calling super which is why we need
// to sync the scroll offset on every onDraw.
onContainerViewScrollChanged(
mDelegate.getContainerViewScrollX(), mDelegate.getContainerViewScrollY());
}
public void setProcessingTouchEvent(boolean processingTouchEvent) {
assert mProcessingTouchEvent != processingTouchEvent;
mProcessingTouchEvent = processingTouchEvent;
if (!mProcessingTouchEvent && mApplyDeferredNativeScroll) {
mApplyDeferredNativeScroll = false;
scrollNativeTo(mDeferredNativeScrollX, mDeferredNativeScrollY);
}
}
// Called by the native side to scroll the container view.
public void scrollContainerViewTo(int x, int y) {
mNativeScrollX = x;
mNativeScrollY = y;
final int scrollX = mDelegate.getContainerViewScrollX();
final int scrollY = mDelegate.getContainerViewScrollY();
final int deltaX = x - scrollX;
final int deltaY = y - scrollY;
final int scrollRangeX = computeMaximumHorizontalScrollOffset();
final int scrollRangeY = computeMaximumVerticalScrollOffset();
// We use overScrollContainerViewBy to be compatible with WebViewClassic which used this
// method for handling both over-scroll as well as in-bounds scroll.
mDelegate.overScrollContainerViewBy(
deltaX,
deltaY,
scrollX,
scrollY,
scrollRangeX,
scrollRangeY,
mProcessingTouchEvent);
}
// Called by the native side to over-scroll the container view.
public void overScrollBy(int deltaX, int deltaY) {
// TODO(mkosiba): Once http://crbug.com/260663 and http://crbug.com/261239 are fixed it
// should be possible to uncomment the following asserts:
// if (deltaX < 0) assert mDelegate.getContainerViewScrollX() == 0;
// if (deltaX > 0) assert mDelegate.getContainerViewScrollX() ==
// computeMaximumHorizontalScrollOffset();
scrollBy(deltaX, deltaY);
}
private void scrollBy(int deltaX, int deltaY) {
if (deltaX == 0 && deltaY == 0) return;
final int scrollX = mDelegate.getContainerViewScrollX();
final int scrollY = mDelegate.getContainerViewScrollY();
final int scrollRangeX = computeMaximumHorizontalScrollOffset();
final int scrollRangeY = computeMaximumVerticalScrollOffset();
// The android.view.View.overScrollBy method is used for both scrolling and over-scrolling
// which is why we use it here.
mDelegate.overScrollContainerViewBy(
deltaX,
deltaY,
scrollX,
scrollY,
scrollRangeX,
scrollRangeY,
mProcessingTouchEvent);
}
private int clampHorizontalScroll(int scrollX) {
scrollX = Math.max(0, scrollX);
scrollX = Math.min(computeMaximumHorizontalScrollOffset(), scrollX);
return scrollX;
}
private int clampVerticalScroll(int scrollY) {
scrollY = Math.max(0, scrollY);
scrollY = Math.min(computeMaximumVerticalScrollOffset(), scrollY);
return scrollY;
}
// Called by the View system as a response to the mDelegate.overScrollContainerViewBy call.
public void onContainerViewOverScrolled(
int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
// Clamp the scroll offset at (0, max).
scrollX = clampHorizontalScroll(scrollX);
scrollY = clampVerticalScroll(scrollY);
mDelegate.scrollContainerViewTo(scrollX, scrollY);
// This is only necessary if the containerView scroll offset ends up being different
// than the one set from native in which case we want the value stored on the native side
// to reflect the value stored in the containerView (and not the other way around).
scrollNativeTo(mDelegate.getContainerViewScrollX(), mDelegate.getContainerViewScrollY());
}
// Called by the View system when the scroll offset had changed. This might not get called if
// the embedder overrides WebView#onScrollChanged without calling super.onScrollChanged. If
// this method does get called it is called both as a response to the embedder scrolling the
// view as well as a response to mDelegate.scrollContainerViewTo.
public void onContainerViewScrollChanged(int x, int y) {
scrollNativeTo(x, y);
}
private void scrollNativeTo(int x, int y) {
x = clampHorizontalScroll(x);
y = clampVerticalScroll(y);
// We shouldn't do the store to native while processing a touch event since that confuses
// the gesture processing logic.
if (mProcessingTouchEvent) {
mDeferredNativeScrollX = x;
mDeferredNativeScrollY = y;
mApplyDeferredNativeScroll = true;
return;
}
if (x == mNativeScrollX && y == mNativeScrollY) return;
// The scrollNativeTo call should be a simple store, so it's OK to assume it always
// succeeds.
mNativeScrollX = x;
mNativeScrollY = y;
mDelegate.scrollNativeTo(x, y);
}
int getScrollX() {
return mNativeScrollX;
}
int getScrollY() {
return mNativeScrollY;
}
private static int computeDurationInMilliSec(int dx, int dy) {
int distance = Math.max(Math.abs(dx), Math.abs(dy));
int duration = distance * 1000 / STD_SCROLL_ANIMATION_SPEED_PIX_PER_SEC;
return Math.min(duration, MAX_SCROLL_ANIMATION_DURATION_MILLISEC);
}
private boolean animateScrollTo(int x, int y) {
final int scrollX = mDelegate.getContainerViewScrollX();
final int scrollY = mDelegate.getContainerViewScrollY();
x = clampHorizontalScroll(x);
y = clampVerticalScroll(y);
int dx = x - scrollX;
int dy = y - scrollY;
if (dx == 0 && dy == 0) return false;
mDelegate.smoothScroll(scrollX + dx, scrollY + dy, computeDurationInMilliSec(dx, dy));
mDelegate.invalidate();
return true;
}
/** See {@link android.webkit.WebView#pageUp(boolean)} */
public boolean pageUp(boolean top) {
final int scrollX = mDelegate.getContainerViewScrollX();
final int scrollY = mDelegate.getContainerViewScrollY();
if (top) {
// go to the top of the document
return animateScrollTo(scrollX, 0);
}
int dy = -mContainerViewHeight / 2;
if (mContainerViewHeight > 2 * PAGE_SCROLL_OVERLAP) {
dy = -mContainerViewHeight + PAGE_SCROLL_OVERLAP;
}
// animateScrollTo clamps the argument to the scrollable range so using (scrollY + dy) is
// fine.
return animateScrollTo(scrollX, scrollY + dy);
}
/** See {@link android.webkit.WebView#pageDown(boolean)} */
public boolean pageDown(boolean bottom) {
final int scrollX = mDelegate.getContainerViewScrollX();
final int scrollY = mDelegate.getContainerViewScrollY();
if (bottom) {
return animateScrollTo(scrollX, computeVerticalScrollRange());
}
int dy = mContainerViewHeight / 2;
if (mContainerViewHeight > 2 * PAGE_SCROLL_OVERLAP) {
dy = mContainerViewHeight - PAGE_SCROLL_OVERLAP;
}
// animateScrollTo clamps the argument to the scrollable range so using (scrollY + dy) is
// fine.
return animateScrollTo(scrollX, scrollY + dy);
}
/** See {@link android.webkit.WebView#requestChildRectangleOnScreen(View, Rect, boolean)} */
public boolean requestChildRectangleOnScreen(
int childOffsetX, int childOffsetY, Rect rect, boolean immediate) {
// TODO(mkosiba): WebViewClassic immediately returns false if a zoom animation is
// in progress. We currently can't tell if one is happening.. should we instead cancel any
// scroll animation when the size/pageScaleFactor changes?
// TODO(mkosiba): Take scrollbar width into account in the screenRight/screenBotton
// calculations. http://crbug.com/269032
final int scrollX = mDelegate.getContainerViewScrollX();
final int scrollY = mDelegate.getContainerViewScrollY();
rect.offset(childOffsetX, childOffsetY);
int screenTop = scrollY;
int screenBottom = scrollY + mContainerViewHeight;
int scrollYDelta = 0;
if (rect.bottom > screenBottom) {
int oneThirdOfScreenHeight = mContainerViewHeight / 3;
if (rect.height() > 2 * oneThirdOfScreenHeight) {
// If the rectangle is too tall to fit in the bottom two thirds
// of the screen, place it at the top.
scrollYDelta = rect.top - screenTop;
} else {
// If the rectangle will still fit on screen, we want its
// top to be in the top third of the screen.
scrollYDelta = rect.top - (screenTop + oneThirdOfScreenHeight);
}
} else if (rect.top < screenTop) {
scrollYDelta = rect.top - screenTop;
}
int screenLeft = scrollX;
int screenRight = scrollX + mContainerViewWidth;
int scrollXDelta = 0;
if (rect.right > screenRight && rect.left > screenLeft) {
if (rect.width() > mContainerViewWidth) {
scrollXDelta += (rect.left - screenLeft);
} else {
scrollXDelta += (rect.right - screenRight);
}
} else if (rect.left < screenLeft) {
scrollXDelta -= (screenLeft - rect.left);
}
if (scrollYDelta == 0 && scrollXDelta == 0) {
return false;
}
if (immediate) {
scrollBy(scrollXDelta, scrollYDelta);
return true;
} else {
return animateScrollTo(scrollX + scrollXDelta, scrollY + scrollYDelta);
}
}
}