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 }