Home | History | Annotate | Download | only in ui
      1 /*
      2  * Copyright (C) 2016 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 
     18 package com.android.documentsui.ui;
     19 
     20 import android.graphics.Point;
     21 
     22 /**
     23  * Provides auto-scrolling upon request when user's interaction with the application
     24  * introduces a natural intent to scroll. Used by BandController, GestureSelector,
     25  * and DragHoverListener to allow auto scrolling when user either does band selection,
     26  * attempting to drag and drop files to somewhere off the current screen, or trying to motion select
     27  * past top/bottom of the screen.
     28  */
     29 public final class ViewAutoScroller implements Runnable {
     30     public static final int NOT_SET = -1;
     31     // ratio used to calculate the top/bottom hotspot region; used with view height
     32     public static final float TOP_BOTTOM_THRESHOLD_RATIO = 0.125f;
     33     public static final int MAX_SCROLL_STEP = 70;
     34 
     35     // Top and bottom inner buffer such that user's cursor does not have to be exactly off screen
     36     // for auto scrolling to begin
     37     private final ScrollDistanceDelegate mCalcDelegate;
     38     private final ScrollActionDelegate mUiDelegate;
     39 
     40     public ViewAutoScroller(ScrollDistanceDelegate calcDelegate, ScrollActionDelegate uiDelegate) {
     41         mCalcDelegate = calcDelegate;
     42         mUiDelegate = uiDelegate;
     43     }
     44 
     45     /**
     46      * Attempts to smooth-scroll the view at the given UI frame. Application should be
     47      * responsible to do any clean up (such as unsubscribing scrollListeners) after the run has
     48      * finished, and re-run this method on the next UI frame if applicable.
     49      */
     50     @Override
     51     public void run() {
     52         // Compute the number of pixels the pointer's y-coordinate is past the view.
     53         // Negative values mean the pointer is at or before the top of the view, and
     54         // positive values mean that the pointer is at or after the bottom of the view. Note
     55         // that top/bottom threshold is added here so that the view still scrolls when the
     56         // pointer are in these buffer pixels.
     57         int pixelsPastView = 0;
     58 
     59         final int topBottomThreshold = (int) (mCalcDelegate.getViewHeight()
     60                 * TOP_BOTTOM_THRESHOLD_RATIO);
     61 
     62         if (mCalcDelegate.getCurrentPosition().y <= topBottomThreshold) {
     63             pixelsPastView = mCalcDelegate.getCurrentPosition().y - topBottomThreshold;
     64         } else if (mCalcDelegate.getCurrentPosition().y >= mCalcDelegate.getViewHeight()
     65                 - topBottomThreshold) {
     66             pixelsPastView = mCalcDelegate.getCurrentPosition().y - mCalcDelegate.getViewHeight()
     67                     + topBottomThreshold;
     68         }
     69 
     70         if (!mCalcDelegate.isActive() || pixelsPastView == 0) {
     71             // If the operation that started the scrolling is no longer inactive, or if it is active
     72             // but not at the edge of the view, no scrolling is necessary.
     73             return;
     74         }
     75 
     76         if (pixelsPastView > topBottomThreshold) {
     77             pixelsPastView = topBottomThreshold;
     78         }
     79 
     80         // Compute the number of pixels to scroll, and scroll that many pixels.
     81         final int numPixels = computeScrollDistance(pixelsPastView);
     82         mUiDelegate.scrollBy(numPixels);
     83 
     84         // Remove callback to this, and then properly run at next frame again
     85         mUiDelegate.removeCallback(this);
     86         mUiDelegate.runAtNextFrame(this);
     87     }
     88 
     89     /**
     90      * Computes the number of pixels to scroll based on how far the pointer is past the end
     91      * of the region. Roughly based on ItemTouchHelper's algorithm for computing the number of
     92      * pixels to scroll when an item is dragged to the end of a view.
     93      * @return
     94      */
     95     public int computeScrollDistance(int pixelsPastView) {
     96         final int topBottomThreshold = (int) (mCalcDelegate.getViewHeight()
     97                 * TOP_BOTTOM_THRESHOLD_RATIO);
     98 
     99         final int direction = (int) Math.signum(pixelsPastView);
    100         final int absPastView = Math.abs(pixelsPastView);
    101 
    102         // Calculate the ratio of how far out of the view the pointer currently resides to
    103         // the top/bottom scrolling hotspot of the view.
    104         final float outOfBoundsRatio = Math.min(
    105                 1.0f, (float) absPastView / topBottomThreshold);
    106         // Interpolate this ratio and use it to compute the maximum scroll that should be
    107         // possible for this step.
    108         final int cappedScrollStep =
    109                 (int) (direction * MAX_SCROLL_STEP * smoothOutOfBoundsRatio(outOfBoundsRatio));
    110 
    111         // If the final number of pixels to scroll ends up being 0, the view should still
    112         // scroll at least one pixel.
    113         return cappedScrollStep != 0 ? cappedScrollStep : direction;
    114     }
    115 
    116     /**
    117      * Interpolates the given out of bounds ratio on a curve which starts at (0,0) and ends
    118      * at (1,1) and quickly approaches 1 near the start of that interval. This ensures that
    119      * drags that are at the edge or barely past the edge of the threshold does little to no
    120      * scrolling, while drags that are near the edge of the view does a lot of
    121      * scrolling. The equation y=x^10 is used, but this could also be tweaked if
    122      * needed.
    123      * @param ratio A ratio which is in the range [0, 1].
    124      * @return A "smoothed" value, also in the range [0, 1].
    125      */
    126     private float smoothOutOfBoundsRatio(float ratio) {
    127         return (float) Math.pow(ratio, 10);
    128     }
    129 
    130     /**
    131      * Used by {@link run} to properly calculate the proper amount of pixels to scroll given time
    132      * passed since scroll started, and to properly scroll / proper listener clean up if necessary.
    133      */
    134     public interface ScrollDistanceDelegate {
    135         public Point getCurrentPosition();
    136         public int getViewHeight();
    137         public boolean isActive();
    138     }
    139 
    140     /**
    141      * Used by {@link run} to do UI tasks, such as scrolling and rerunning at next UI cycle.
    142      */
    143     public interface ScrollActionDelegate {
    144         public void scrollBy(int dy);
    145         public void runAtNextFrame(Runnable r);
    146         public void removeCallback(Runnable r);
    147     }
    148 }