Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 31d9b44b authored by Ben Lin's avatar Ben Lin Committed by Android (Google) Code Review
Browse files

Merge "Allow drag-n-drop to auto-scroll when near top/bottom of dirlist." into nyc-andromeda-dev

parents 0f8e3cde c5e3e8eb
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -46,4 +46,6 @@
    <dimen name="drag_shadow_width">160dp</dimen>
    <dimen name="drag_shadow_height">48dp</dimen>

    <dimen name="autoscroll_edge_height">32dp</dimen>

</resources>
+0 −5
Original line number Diff line number Diff line
@@ -21,7 +21,6 @@ import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Looper;
import android.provider.DocumentsContract;
import android.text.TextUtils;
@@ -30,10 +29,6 @@ import android.text.format.Time;
import android.util.Log;
import android.view.WindowManager;

import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.RootInfo;

import java.io.FileNotFoundException;
import java.text.Collator;
import java.util.ArrayList;
import java.util.List;
+29 −120
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.documentsui.dirlist;
import static com.android.documentsui.Shared.DEBUG;
import static com.android.documentsui.dirlist.ModelBackedDocumentsAdapter.ITEM_TYPE_DIRECTORY;
import static com.android.documentsui.dirlist.ModelBackedDocumentsAdapter.ITEM_TYPE_DOCUMENT;
import static com.android.documentsui.dirlist.ViewAutoScroller.NOT_SET;

import android.graphics.Point;
import android.graphics.Rect;
@@ -39,6 +40,8 @@ import com.android.documentsui.Events.InputEvent;
import com.android.documentsui.Events.MotionInputEvent;
import com.android.documentsui.R;
import com.android.documentsui.dirlist.MultiSelectManager.Selection;
import com.android.documentsui.dirlist.ViewAutoScroller.ScrollActionDelegate;
import com.android.documentsui.dirlist.ViewAutoScroller.ScrollDistanceDelegate;

import java.util.ArrayList;
import java.util.Collections;
@@ -54,15 +57,14 @@ import java.util.Set;
 */
public class BandController extends RecyclerView.OnScrollListener {

    private static final int NOT_SET = -1;

    private static final String TAG = "BandController";
    private static final int AUTOSCROLL_EDGE_HEIGHT = 1;

    private final Runnable mModelBuilder;
    private final SelectionEnvironment mEnvironment;
    private final DocumentsAdapter mAdapter;
    private final MultiSelectManager mSelectionManager;
    private final Runnable mViewScroller = new ViewScroller();
    private final Runnable mViewScroller;
    private final GridModel.OnSelectionChangedListener mGridListener;

    @Nullable private Rect mBounds;
@@ -70,9 +72,6 @@ public class BandController extends RecyclerView.OnScrollListener {
    @Nullable private Point mOrigin;
    @Nullable private BandController.GridModel mModel;

    // The time at which the current band selection-induced scroll began. If no scroll is in
    // progress, the value is NOT_SET.
    private long mScrollStartTime = NOT_SET;
    private Selection mSelection;

    public BandController(
@@ -114,6 +113,25 @@ public class BandController extends RecyclerView.OnScrollListener {
        mSelectionManager = selectionManager;

        mEnvironment.addOnScrollListener(this);
        mViewScroller = new ViewAutoScroller(
                AUTOSCROLL_EDGE_HEIGHT,
                new ScrollDistanceDelegate() {
                    @Override
                    public Point getCurrentPosition() {
                        return mCurrentPosition;
                    }

                    @Override
                    public int getViewHeight() {
                        return mEnvironment.getHeight();
                    }

                    @Override
                    public boolean isActive() {
                        return BandController.this.isActive();
                    }
                },
                env);

        mAdapter.registerAdapterDataObserver(
                new RecyclerView.AdapterDataObserver() {
@@ -173,6 +191,10 @@ public class BandController extends RecyclerView.OnScrollListener {
        };
    }

    private boolean isActive() {
        return mModel != null;
    }

    void bindSelection(Selection selection) {
        mSelection = selection;
    }
@@ -212,10 +234,6 @@ public class BandController extends RecyclerView.OnScrollListener {
        return isActive();
    }

    private boolean isActive() {
        return mModel != null;
    }

    /**
     * Handle a change in layout by cleaning up and getting rid of the old model and creating
     * a new model which will track the new layout.
@@ -336,112 +354,6 @@ public class BandController extends RecyclerView.OnScrollListener {
        return mSelectionManager.notifyBeforeItemStateChange(id, nextState);
    }

    private class ViewScroller implements Runnable {
        /**
         * The number of milliseconds of scrolling at which scroll speed continues to increase.
         * At first, the scroll starts slowly; then, the rate of scrolling increases until it
         * reaches its maximum value at after this many milliseconds.
         */
        private static final long SCROLL_ACCELERATION_LIMIT_TIME_MS = 2000;

        @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 one additional pixel is added here so that the view still scrolls when the
            // pointer is exactly at the top or bottom.
            int pixelsPastView = 0;
            if (mCurrentPosition.y <= 0) {
                pixelsPastView = mCurrentPosition.y - 1;
            } else if (mCurrentPosition.y >= mEnvironment.getHeight() - 1) {
                pixelsPastView = mCurrentPosition.y - mEnvironment.getHeight() + 1;
            }

            if (!isActive() || pixelsPastView == 0) {
                // If band selection is inactive, or if it is active but not at the edge of the
                // view, no scrolling is necessary.
                mScrollStartTime = NOT_SET;
                return;
            }

            if (mScrollStartTime == NOT_SET) {
                // If the pointer was previously not at the edge of the view but now is, set the
                // start time for the scroll.
                mScrollStartTime = System.currentTimeMillis();
            }

            // Compute the number of pixels to scroll, and scroll that many pixels.
            final int numPixels = computeScrollDistance(
                    pixelsPastView, System.currentTimeMillis() - mScrollStartTime);
            mEnvironment.scrollBy(numPixels);

            mEnvironment.removeCallback(mViewScroller);
            mEnvironment.runAtNextFrame(this);
        }

        /**
         * Computes the number of pixels to scroll based on how far the pointer is past the end
         * of the view and how long it has been there. Roughly based on ItemTouchHelper's
         * algorithm for computing the number of pixels to scroll when an item is dragged to the
         * end of a {@link RecyclerView}.
         * @param pixelsPastView
         * @param scrollDuration
         * @return
         */
        private int computeScrollDistance(int pixelsPastView, long scrollDuration) {
            final int maxScrollStep = mEnvironment.getHeight();
            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 entire height of the view.
            final float outOfBoundsRatio = Math.min(
                    1.0f, (float) absPastView / mEnvironment.getHeight());
            // Interpolate this ratio and use it to compute the maximum scroll that should be
            // possible for this step.
            final float cappedScrollStep =
                    direction * maxScrollStep * smoothOutOfBoundsRatio(outOfBoundsRatio);

            // Likewise, calculate the ratio of the time spent in the scroll to the limit.
            final float timeRatio = Math.min(
                    1.0f, (float) scrollDuration / SCROLL_ACCELERATION_LIMIT_TIME_MS);
            // Interpolate this ratio and use it to compute the final number of pixels to
            // scroll.
            final int numPixels = (int) (cappedScrollStep * smoothTimeRatio(timeRatio));

            // If the final number of pixels to scroll ends up being 0, the view should still
            // scroll at least one pixel.
            return numPixels != 0 ? numPixels : 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 view still cause sufficient
         * scrolling. The equation y=(x-1)^5+1 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 - 1.0f, 5) + 1.0f;
        }

        /**
         * Interpolates the given time ratio on a curve which starts at (0,0) and ends at (1,1)
         * and stays close to 0 for most input values except those very close to 1. This ensures
         * that scrolls start out very slowly but speed up drastically after the scroll has been
         * in progress close to SCROLL_ACCELERATION_LIMIT_TIME_MS. The equation y=x^5 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 smoothTimeRatio(float ratio) {
            return (float) Math.pow(ratio, 5);
        }
    };

    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        if (!isActive()) {
@@ -1110,16 +1022,13 @@ public class BandController extends RecyclerView.OnScrollListener {
     * Provides functionality for BandController. Exists primarily to tests that are
     * fully isolated from RecyclerView.
     */
    interface SelectionEnvironment {
    interface SelectionEnvironment extends ScrollActionDelegate {
        void showBand(Rect rect);
        void hideBand();
        void addOnScrollListener(RecyclerView.OnScrollListener listener);
        void removeOnScrollListener(RecyclerView.OnScrollListener listener);
        void scrollBy(int dy);
        int getHeight();
        void invalidateView();
        void runAtNextFrame(Runnable r);
        void removeCallback(Runnable r);
        Point createAbsolutePoint(Point relativePoint);
        Rect getAbsoluteRectForChildViewAt(int index);
        int getAdapterPositionAt(int index);
+4 −3
Original line number Diff line number Diff line
@@ -74,7 +74,6 @@ import android.widget.Toolbar;
import com.android.documentsui.BaseActivity;
import com.android.documentsui.DirectoryLoader;
import com.android.documentsui.DirectoryResult;
import com.android.documentsui.clipping.DocumentClipper;
import com.android.documentsui.DocumentsActivity;
import com.android.documentsui.DocumentsApplication;
import com.android.documentsui.Events.InputEvent;
@@ -93,6 +92,7 @@ import com.android.documentsui.Shared;
import com.android.documentsui.Snackbars;
import com.android.documentsui.State;
import com.android.documentsui.State.ViewMode;
import com.android.documentsui.clipping.DocumentClipper;
import com.android.documentsui.clipping.UrisSupplier;
import com.android.documentsui.dirlist.MultiSelectManager.Selection;
import com.android.documentsui.dirlist.UserInputHandler.DocumentDetails;
@@ -183,7 +183,7 @@ public class DirectoryFragment extends Fragment
    private @Nullable BandController mBandController;
    private @Nullable ActionMode mActionMode;

    private DirectoryDragListener mOnDragListener;
    private DragScrollListener mOnDragListener;
    private MenuManager mMenuManager;

    @Override
@@ -210,7 +210,8 @@ public class DirectoryFragment extends Fragment

        mRecView.setItemAnimator(new DirectoryItemAnimator(getActivity()));

        mOnDragListener = new DirectoryDragListener(this);
        mOnDragListener = DragScrollListener.create(
                getActivity(), new DirectoryDragListener(this), mRecView);

        // Make the recycler and the empty views responsive to drop events.
        mRecView.setOnDragListener(mOnDragListener);
+167 −0
Original line number Diff line number Diff line
/*
 * 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.dirlist;

import android.content.Context;
import android.graphics.Point;
import android.view.DragEvent;
import android.view.View;
import android.view.View.OnDragListener;

import com.android.documentsui.ItemDragListener;
import com.android.documentsui.ItemDragListener.DragHost;
import com.android.documentsui.dirlist.ViewAutoScroller.ScrollActionDelegate;
import com.android.documentsui.dirlist.ViewAutoScroller.ScrollDistanceDelegate;
import com.android.documentsui.R;

import java.util.function.BooleanSupplier;
import java.util.function.IntSupplier;

import javax.annotation.Nullable;

/**
 * This class acts as a middle-man handler for potential auto-scrolling before passing the dragEvent
 * onto {@link DirectoryDragListener}.
 */
class DragScrollListener implements OnDragListener {

    private final ItemDragListener<? extends DragHost> mDragHandler;
    private final IntSupplier mHeight;
    private final BooleanSupplier mCanScrollUp;
    private final BooleanSupplier mCanScrollDown;
    private final int mAutoScrollEdgeHeight;
    private final Runnable mDragScroller;

    private boolean mDragHappening;
    private @Nullable Point mCurrentPosition;

    private DragScrollListener(
            Context context,
            ItemDragListener<? extends DragHost> dragHandler,
            IntSupplier heightSupplier,
            BooleanSupplier scrollUpSupplier,
            BooleanSupplier scrollDownSupplier,
            ViewAutoScroller.ScrollActionDelegate actionDelegate) {
        mDragHandler = dragHandler;
        mAutoScrollEdgeHeight = (int) context.getResources()
                .getDimension(R.dimen.autoscroll_edge_height);
        mHeight = heightSupplier;
        mCanScrollUp = scrollUpSupplier;
        mCanScrollDown = scrollDownSupplier;

        ScrollDistanceDelegate distanceDelegate = new ScrollDistanceDelegate() {
            @Override
            public Point getCurrentPosition() {
                return mCurrentPosition;
            }

            @Override
            public int getViewHeight() {
                return mHeight.getAsInt();
            }

            @Override
            public boolean isActive() {
                return mDragHappening;
            }
        };

        mDragScroller = new ViewAutoScroller(
                mAutoScrollEdgeHeight, distanceDelegate, actionDelegate);
    }

    static DragScrollListener create(
            Context context, ItemDragListener<? extends DragHost> dragHandler, View scrollView) {
        ScrollActionDelegate actionDelegate = new ScrollActionDelegate() {
            @Override
            public void scrollBy(int dy) {
                scrollView.scrollBy(0, dy);
            }

            @Override
            public void runAtNextFrame(Runnable r) {
                scrollView.postOnAnimation(r);

            }

            @Override
            public void removeCallback(Runnable r) {
                scrollView.removeCallbacks(r);
            }
        };
        DragScrollListener listener = new DragScrollListener(
                context,
                dragHandler,
                scrollView::getHeight,
                () -> {
                    return scrollView.canScrollVertically(-1);
                },
                () -> {
                    return scrollView.canScrollVertically(1);
                },
                actionDelegate);
        return listener;
    }

    @Override
    public boolean onDrag(View v, DragEvent event) {
        boolean handled = false;
        switch (event.getAction()) {
            case DragEvent.ACTION_DRAG_STARTED:
                mDragHappening = true;
                break;
            case DragEvent.ACTION_DRAG_ENDED:
                mDragHappening = false;
                break;
            case DragEvent.ACTION_DRAG_ENTERED:
                handled = insideDragZone();
                break;
            case DragEvent.ACTION_DRAG_LOCATION:
                handled = handleLocationEvent(v, event.getX(), event.getY());
                break;
            default:
                break;
        }

        if (!handled) {
            handled = mDragHandler.onDrag(v, event);
        }

        return handled;
    }

    private boolean handleLocationEvent(View v, float x, float y) {
        mCurrentPosition = new Point(Math.round(v.getX() + x), Math.round(v.getY() + y));
        if (insideDragZone()) {
            mDragScroller.run();
            return true;
        }
        return false;
    }

    private boolean insideDragZone() {
        if (mCurrentPosition == null) {
            return false;
        }

        boolean shouldScrollUp = mCurrentPosition.y < mAutoScrollEdgeHeight
                && mCanScrollUp.getAsBoolean();
        boolean shouldScrollDown = mCurrentPosition.y > mHeight.getAsInt() - mAutoScrollEdgeHeight
                && mCanScrollDown.getAsBoolean();
        return shouldScrollUp || shouldScrollDown;
    }
}
 No newline at end of file
Loading