/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.documentsui; import android.graphics.Point; /** * Provides auto-scrolling upon request when user's interaction with the application * introduces a natural intent to scroll. Used by DragHoverListener to allow auto scrolling * when user either does band selection, attempting to drag and drop files to somewhere off * the current screen, or trying to motion select past top/bottom of the screen. */ public final class ViewAutoScroller implements Runnable { // ratio used to calculate the top/bottom hotspot region; used with view height public static final float TOP_BOTTOM_THRESHOLD_RATIO = 0.125f; public static final int MAX_SCROLL_STEP = 70; private ScrollHost mHost; private ScrollerCallbacks mCallbacks; public ViewAutoScroller(ScrollHost scrollHost, ScrollerCallbacks callbacks) { assert scrollHost != null; assert callbacks != null; mHost = scrollHost; mCallbacks = callbacks; } /** * Attempts to smooth-scroll the view at the given UI frame. Application should be * responsible to do any clean up (such as unsubscribing scrollListeners) after the run has * finished, and re-run this method on the next UI frame if applicable. */ @Override public void run() { // Compute the number of pixels the pointer's y-coordinate is past the view. // Negative values mean the pointer is at or before the top of the view, and // positive values mean that the pointer is at or after the bottom of the view. Note // that top/bottom threshold is added here so that the view still scrolls when the // pointer are in these buffer pixels. int pixelsPastView = 0; final int topBottomThreshold = (int) (mHost.getViewHeight() * TOP_BOTTOM_THRESHOLD_RATIO); if (mHost.getCurrentPosition().y <= topBottomThreshold) { pixelsPastView = mHost.getCurrentPosition().y - topBottomThreshold; } else if (mHost.getCurrentPosition().y >= mHost.getViewHeight() - topBottomThreshold) { pixelsPastView = mHost.getCurrentPosition().y - mHost.getViewHeight() + topBottomThreshold; } if (!mHost.isActive() || pixelsPastView == 0) { // If the operation that started the scrolling is no longer inactive, or if it is active // but not at the edge of the view, no scrolling is necessary. return; } if (pixelsPastView > topBottomThreshold) { pixelsPastView = topBottomThreshold; } // Compute the number of pixels to scroll, and scroll that many pixels. final int numPixels = computeScrollDistance(pixelsPastView); mCallbacks.scrollBy(numPixels); // Remove callback to this, and then properly run at next frame again mCallbacks.removeCallback(this); mCallbacks.runAtNextFrame(this); } /** * Computes the number of pixels to scroll based on how far the pointer is past the end * of the region. Roughly based on ItemTouchHelper's algorithm for computing the number of * pixels to scroll when an item is dragged to the end of a view. * @return */ public int computeScrollDistance(int pixelsPastView) { final int topBottomThreshold = (int) (mHost.getViewHeight() * TOP_BOTTOM_THRESHOLD_RATIO); final int direction = (int) Math.signum(pixelsPastView); final int absPastView = Math.abs(pixelsPastView); // Calculate the ratio of how far out of the view the pointer currently resides to // the top/bottom scrolling hotspot of the view. final float outOfBoundsRatio = Math.min( 1.0f, (float) absPastView / topBottomThreshold); // Interpolate this ratio and use it to compute the maximum scroll that should be // possible for this step. final int cappedScrollStep = (int) (direction * MAX_SCROLL_STEP * smoothOutOfBoundsRatio(outOfBoundsRatio)); // If the final number of pixels to scroll ends up being 0, the view should still // scroll at least one pixel. return cappedScrollStep != 0 ? cappedScrollStep : direction; } /** * Interpolates the given out of bounds ratio on a curve which starts at (0,0) and ends * at (1,1) and quickly approaches 1 near the start of that interval. This ensures that * drags that are at the edge or barely past the edge of the threshold does little to no * scrolling, while drags that are near the edge of the view does a lot of * scrolling. The equation y=x^10 is used, but this could also be tweaked if * needed. * @param ratio A ratio which is in the range [0, 1]. * @return A "smoothed" value, also in the range [0, 1]. */ private float smoothOutOfBoundsRatio(float ratio) { return (float) Math.pow(ratio, 10); } /** * Used by {@link run} to properly calculate the proper amount of pixels to scroll given time * passed since scroll started, and to properly scroll / proper listener clean up if necessary. */ public static abstract class ScrollHost { public abstract Point getCurrentPosition(); public abstract int getViewHeight(); public abstract boolean isActive(); } /** * Callback used by scroller to perform UI tasks, such as scrolling and rerunning at next UI * cycle. */ public static abstract class ScrollerCallbacks { public void scrollBy(int dy) {} public void runAtNextFrame(Runnable r) {} public void removeCallback(Runnable r) {} } }