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 package com.android.documentsui; 18 19 import android.graphics.Point; 20 21 /** 22 * Provides auto-scrolling upon request when user's interaction with the application 23 * introduces a natural intent to scroll. Used by DragHoverListener to allow auto scrolling 24 * when user either does band selection, attempting to drag and drop files to somewhere off 25 * the current screen, or trying to motion select past top/bottom of the screen. 26 */ 27 public final class ViewAutoScroller implements Runnable { 28 29 // ratio used to calculate the top/bottom hotspot region; used with view height 30 public static final float TOP_BOTTOM_THRESHOLD_RATIO = 0.125f; 31 public static final int MAX_SCROLL_STEP = 70; 32 33 private ScrollHost mHost; 34 private ScrollerCallbacks mCallbacks; 35 ViewAutoScroller(ScrollHost scrollHost, ScrollerCallbacks callbacks)36 public ViewAutoScroller(ScrollHost scrollHost, ScrollerCallbacks callbacks) { 37 assert scrollHost != null; 38 assert callbacks != null; 39 40 mHost = scrollHost; 41 mCallbacks = callbacks; 42 } 43 44 /** 45 * Attempts to smooth-scroll the view at the given UI frame. Application should be 46 * responsible to do any clean up (such as unsubscribing scrollListeners) after the run has 47 * finished, and re-run this method on the next UI frame if applicable. 48 */ 49 @Override run()50 public void run() { 51 // Compute the number of pixels the pointer's y-coordinate is past the view. 52 // Negative values mean the pointer is at or before the top of the view, and 53 // positive values mean that the pointer is at or after the bottom of the view. Note 54 // that top/bottom threshold is added here so that the view still scrolls when the 55 // pointer are in these buffer pixels. 56 int pixelsPastView = 0; 57 58 final int topBottomThreshold = (int) (mHost.getViewHeight() 59 * TOP_BOTTOM_THRESHOLD_RATIO); 60 61 if (mHost.getCurrentPosition().y <= topBottomThreshold) { 62 pixelsPastView = mHost.getCurrentPosition().y - topBottomThreshold; 63 } else if (mHost.getCurrentPosition().y >= mHost.getViewHeight() 64 - topBottomThreshold) { 65 pixelsPastView = mHost.getCurrentPosition().y - mHost.getViewHeight() 66 + topBottomThreshold; 67 } 68 69 if (!mHost.isActive() || pixelsPastView == 0) { 70 // If the operation that started the scrolling is no longer inactive, or if it is active 71 // but not at the edge of the view, no scrolling is necessary. 72 return; 73 } 74 75 if (pixelsPastView > topBottomThreshold) { 76 pixelsPastView = topBottomThreshold; 77 } 78 79 // Compute the number of pixels to scroll, and scroll that many pixels. 80 final int numPixels = computeScrollDistance(pixelsPastView); 81 mCallbacks.scrollBy(numPixels); 82 83 // Remove callback to this, and then properly run at next frame again 84 mCallbacks.removeCallback(this); 85 mCallbacks.runAtNextFrame(this); 86 } 87 88 /** 89 * Computes the number of pixels to scroll based on how far the pointer is past the end 90 * of the region. Roughly based on ItemTouchHelper's algorithm for computing the number of 91 * pixels to scroll when an item is dragged to the end of a view. 92 * @return 93 */ computeScrollDistance(int pixelsPastView)94 public int computeScrollDistance(int pixelsPastView) { 95 final int topBottomThreshold = 96 (int) (mHost.getViewHeight() * TOP_BOTTOM_THRESHOLD_RATIO); 97 98 final int direction = (int) Math.signum(pixelsPastView); 99 final int absPastView = Math.abs(pixelsPastView); 100 101 // Calculate the ratio of how far out of the view the pointer currently resides to 102 // the top/bottom scrolling hotspot of the view. 103 final float outOfBoundsRatio = Math.min( 104 1.0f, (float) absPastView / topBottomThreshold); 105 // Interpolate this ratio and use it to compute the maximum scroll that should be 106 // possible for this step. 107 final int cappedScrollStep = 108 (int) (direction * MAX_SCROLL_STEP * smoothOutOfBoundsRatio(outOfBoundsRatio)); 109 110 // If the final number of pixels to scroll ends up being 0, the view should still 111 // scroll at least one pixel. 112 return cappedScrollStep != 0 ? cappedScrollStep : direction; 113 } 114 115 /** 116 * Interpolates the given out of bounds ratio on a curve which starts at (0,0) and ends 117 * at (1,1) and quickly approaches 1 near the start of that interval. This ensures that 118 * drags that are at the edge or barely past the edge of the threshold does little to no 119 * scrolling, while drags that are near the edge of the view does a lot of 120 * scrolling. The equation y=x^10 is used, but this could also be tweaked if 121 * needed. 122 * @param ratio A ratio which is in the range [0, 1]. 123 * @return A "smoothed" value, also in the range [0, 1]. 124 */ smoothOutOfBoundsRatio(float ratio)125 private float smoothOutOfBoundsRatio(float ratio) { 126 return (float) Math.pow(ratio, 10); 127 } 128 129 /** 130 * Used by {@link run} to properly calculate the proper amount of pixels to scroll given time 131 * passed since scroll started, and to properly scroll / proper listener clean up if necessary. 132 */ 133 public static abstract class ScrollHost { getCurrentPosition()134 public abstract Point getCurrentPosition(); getViewHeight()135 public abstract int getViewHeight(); isActive()136 public abstract boolean isActive(); 137 } 138 139 /** 140 * Callback used by scroller to perform UI tasks, such as scrolling and rerunning at next UI 141 * cycle. 142 */ 143 public static abstract class ScrollerCallbacks { scrollBy(int dy)144 public void scrollBy(int dy) {} runAtNextFrame(Runnable r)145 public void runAtNextFrame(Runnable r) {} removeCallback(Runnable r)146 public void removeCallback(Runnable r) {} 147 } 148 } 149