/* * Copyright (C) 2013 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.launcher3; import android.graphics.Rect; import android.graphics.RectF; import android.os.SystemClock; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.animation.AccelerateInterpolator; import android.view.animation.Interpolator; import android.widget.AbsListView; class AutoScroller implements View.OnTouchListener, Runnable { private static final int SCALE_RELATIVE = 0; private static final int SCALE_ABSOLUTE = 1; private final View mTarget; private final RampUpScroller mScroller; /** Interpolator used to scale velocity with touch position, may be null. */ private Interpolator mEdgeInterpolator = new AccelerateInterpolator(); /** * Type of maximum velocity scaling to use, one of: * */ private int mMaxVelocityScale = SCALE_RELATIVE; /** * Type of activation edge scaling to use, one of: * */ private int mActivationEdgeScale = SCALE_RELATIVE; /** Edge insets used to activate auto-scrolling. */ private RectF mActivationEdges = new RectF(0.2f, 0.2f, 0.2f, 0.2f); /** Delay after entering an activation edge before auto-scrolling begins. */ private int mActivationDelay; /** Maximum horizontal scrolling velocity. */ private float mMaxVelocityX = 0.001f; /** Maximum vertical scrolling velocity. */ private float mMaxVelocityY = 0.001f; /** * Whether positive insets should also extend beyond the view bounds when * auto-scrolling is already active. This allows a user to start scrolling * at an inside edge, then move beyond the edge and continue scrolling. */ private boolean mExtendsBeyondEdges = true; /** Whether to start activation immediately. */ private boolean mSkipDelay; /** Whether to reset the scroller start time on the next animation. */ private boolean mResetScroller; /** Whether the auto-scroller is active. */ private boolean mActive; private long[] mScrollStart = new long[2]; /** * If the event is within this percentage of the edge of the scrolling area, * use accelerated scrolling. */ private float mFastScrollingRange = 0.8f; /** * Duration of time spent in accelerated scrolling area before reaching * maximum velocity */ private float mDurationToMax = 2500f; private static final int X = 0; private static final int Y = 1; public AutoScroller(View target) { mTarget = target; mScroller = new RampUpScroller(250); mActivationDelay = ViewConfiguration.getTapTimeout(); } /** * Sets the maximum scrolling velocity as a fraction of the host view size * per second. For example, a maximum Y velocity of 1 would scroll one * vertical page per second. By default, both values are 1. * * @param x The maximum X velocity as a fraction of the host view width per * second. * @param y The maximum Y velocity as a fraction of the host view height per * second. */ public void setMaximumVelocityRelative(float x, float y) { mMaxVelocityScale = SCALE_RELATIVE; mMaxVelocityX = x / 1000f; mMaxVelocityY = y / 1000f; } /** * Sets the maximum scrolling velocity as an absolute pixel distance per * second. For example, a maximum Y velocity of 100 would scroll one hundred * pixels per second. * * @param x The maximum X velocity as a fraction of the host view width per * second. * @param y The maximum Y velocity as a fraction of the host view height per * second. */ public void setMaximumVelocityAbsolute(float x, float y) { mMaxVelocityScale = SCALE_ABSOLUTE; mMaxVelocityX = x / 1000f; mMaxVelocityY = y / 1000f; } /** * Sets the delay after entering an activation edge before activation of * auto-scrolling. By default, the activation delay is set to * {@link ViewConfiguration#getTapTimeout()}. * * @param delayMillis The delay in milliseconds. */ public void setActivationDelay(int delayMillis) { mActivationDelay = delayMillis; } /** * Sets the activation edges in pixels. Edges are treated as insets, so * positive values expand into the view bounds while negative values extend * outside the bounds. * * @param l The left activation edge, in pixels. * @param t The top activation edge, in pixels. * @param r The right activation edge, in pixels. * @param b The bottom activation edge, in pixels. */ public void setEdgesAbsolute(int l, int t, int r, int b) { mActivationEdgeScale = SCALE_ABSOLUTE; mActivationEdges.set(l, t, r, b); } /** * Whether positive insets should also extend beyond the view bounds when * auto-scrolling is already active. This allows a user to start scrolling * at an inside edge, then move beyond the edge and continue scrolling. * * @param e */ public void setExtendsBeyondEdges(boolean e) { mExtendsBeyondEdges = e; } /** * Sets the activation edges as fractions of the host view size. Edges are * treated as insets, so positive values expand into the view bounds while * negative values extend outside the bounds. By default, all values are * 0.25. * * @param l The left activation edge, as a fraction of view size. * @param t The top activation edge, as a fraction of view size. * @param r The right activation edge, as a fraction of view size. * @param b The bottom activation edge, as a fraction of view size. */ public void setEdgesRelative(float l, float t, float r, float b) { mActivationEdgeScale = SCALE_RELATIVE; mActivationEdges.set(l, t, r, b); } /** * Sets the {@link Interpolator} used for scaling touches within activation * edges. By default, uses the {@link AccelerateInterpolator} to gradually * speed up scrolling. * * @param edgeInterpolator The interpolator to use for activation edges, or * {@code null} to use a fixed velocity during auto-scrolling. */ public void setEdgeInterpolator(Interpolator edgeInterpolator) { mEdgeInterpolator = edgeInterpolator; } /** * Stop tracking scrolling. */ public void stop() { stop(true); } /** * Pass the rectangle defining the drawing region for the object used to * trigger drag scrolling. * * @param v View on which the scrolling regions are defined * @param r Rect defining the drawing bounds of the object being dragged * @return whether the event was handled */ public boolean onTouch(View v, Rect r) { MotionEvent event = MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_MOVE, r.left, r.top, 0); return onTouch(v, event); } @Override public boolean onTouch(View v, MotionEvent event) { final int action = event.getActionMasked(); switch (action) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_MOVE: final int sourceWidth = v.getWidth(); final int sourceHeight = v.getHeight(); final float x = event.getX(); final float y = event.getY(); final float l; final float t; final float r; final float b; final RectF activationEdges = mActivationEdges; if (mActivationEdgeScale == SCALE_ABSOLUTE) { l = activationEdges.left; t = activationEdges.top; r = activationEdges.right; b = activationEdges.bottom; } else { l = activationEdges.left * sourceWidth; t = activationEdges.top * sourceHeight; r = activationEdges.right * sourceWidth; b = activationEdges.bottom * sourceHeight; } final float maxVelX; final float maxVelY; if (mMaxVelocityScale == SCALE_ABSOLUTE) { maxVelX = mMaxVelocityX; maxVelY = mMaxVelocityY; } else { maxVelX = mMaxVelocityX * mTarget.getWidth(); maxVelY = mMaxVelocityY * mTarget.getHeight(); } final float velocityX = getEdgeVelocity(X, l, r, x, sourceWidth, event); final float velocityY = getEdgeVelocity(Y, t, b, y, sourceHeight, event); mScroller.setTargetVelocity(velocityX * maxVelX, velocityY * maxVelY); if ((velocityX != 0 || velocityY != 0) && !mActive) { mActive = true; mResetScroller = true; if (mSkipDelay) { mTarget.postOnAnimation(this); } else { mSkipDelay = true; mTarget.postOnAnimationDelayed(this, mActivationDelay); } } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: stop(true); break; } return false; } /** * @param leading Size of the leading activation inset. * @param trailing Size of the trailing activation inset. * @param current Position within within the total area. * @param size Size of the total area. * @return The fraction of the activation area. */ private float getEdgeVelocity(int dir, float leading, float trailing, float current, float size, MotionEvent ev) { float valueLeading = 0; if (leading > 0) { if (current < leading) { if (current > 0) { // Movement up to the edge is scaled. valueLeading = 1f - current / leading; } else if (mActive && mExtendsBeyondEdges) { // Movement beyond the edge is always maximum. valueLeading = 1f; } } } else if (leading < 0) { if (current < 0) { // Movement beyond the edge is scaled. valueLeading = current / leading; } } float valueTrailing = 0; if (trailing > 0) { if (current > size - trailing) { if (current < size) { // Movement up to the edge is scaled. valueTrailing = 1f - (size - current) / trailing; } else if (mActive && mExtendsBeyondEdges) { // Movement beyond the edge is always maximum. valueTrailing = 1f; } } } else if (trailing < 0) { if (current > size) { // Movement beyond the edge is scaled. valueTrailing = (size - current) / trailing; } } float value = (valueTrailing - valueLeading); if ((value > mFastScrollingRange || value < -mFastScrollingRange) && mScrollStart[dir] == 0) { // within auto scrolling area mScrollStart[dir] = ev.getEventTime(); } else { // Outside fast scrolling area; reset duration mScrollStart[dir] = 0; } final float duration = (ev.getEventTime() - mScrollStart[dir])/mDurationToMax; final float interpolated; if (value < 0) { if (value < -mFastScrollingRange) { // Close to top; use duration! value += mEdgeInterpolator.getInterpolation(-duration); } interpolated = mEdgeInterpolator == null ? -1 : -mEdgeInterpolator.getInterpolation(-value); } else if (value > 0) { // Close to bottom; use duration if (value > mFastScrollingRange) { // Close to bottom; use duration! value += mEdgeInterpolator.getInterpolation(duration); } interpolated = mEdgeInterpolator == null ? 1 : mEdgeInterpolator.getInterpolation(value); } else { mScrollStart[dir] = 0; return 0; } return constrain(interpolated, -1, 1); } private static float constrain(float value, float min, float max) { if (value > max) { return max; } else if (value < min) { return min; } else { return value; } } /** * Stops auto-scrolling immediately, optionally reseting the auto-scrolling * delay. * * @param reset Whether to reset the auto-scrolling delay. */ private void stop(boolean reset) { mActive = false; mSkipDelay = !reset; mTarget.removeCallbacks(this); } @Override public void run() { if (!mActive) { return; } if (mResetScroller) { mResetScroller = false; mScroller.start(); } final View target = mTarget; final RampUpScroller scroller = mScroller; final float targetVelocityX = scroller.getTargetVelocityX(); final float targetVelocityY = scroller.getTargetVelocityY(); if ((targetVelocityY == 0 || !target.canScrollVertically(targetVelocityY > 0 ? 1 : -1) && (targetVelocityX == 0 || !target.canScrollHorizontally(targetVelocityX > 0 ? 1 : -1)))) { stop(false); return; } scroller.computeScrollDelta(); final int deltaX = scroller.getDeltaX(); final int deltaY = scroller.getDeltaY(); if (target instanceof AbsListView) { final AbsListView list = (AbsListView) target; list.smoothScrollBy(deltaY, 0); } else { target.scrollBy(deltaX, deltaY); } target.postOnAnimation(this); } }