Home | History | Annotate | Download | only in android_webview
      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.
     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