Home | History | Annotate | Download | only in selection
      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.selection;
     18 
     19 import static com.android.documentsui.base.Shared.DEBUG;
     20 
     21 import android.graphics.Point;
     22 import android.support.annotation.VisibleForTesting;
     23 import android.support.v7.widget.RecyclerView;
     24 import android.util.Log;
     25 import android.view.View;
     26 
     27 import com.android.documentsui.DirectoryReloadLock;
     28 import com.android.documentsui.base.Events.InputEvent;
     29 import com.android.documentsui.ui.ViewAutoScroller;
     30 import com.android.documentsui.ui.ViewAutoScroller.ScrollActionDelegate;
     31 import com.android.documentsui.ui.ViewAutoScroller.ScrollDistanceDelegate;
     32 
     33 import java.util.function.IntSupplier;
     34 
     35 import javax.annotation.Nullable;
     36 
     37 /*
     38  * Helper class used to intercept events that could cause a gesture multi-select, and keeps
     39  * the interception going if necessary.
     40  */
     41 public final class GestureSelector {
     42     private final String TAG = "GestureSelector";
     43 
     44     private final SelectionManager mSelectionMgr;
     45     private final Runnable mDragScroller;
     46     private final IntSupplier mHeight;
     47     private final ViewFinder mViewFinder;
     48     private final DirectoryReloadLock mLock;
     49     private int mLastStartedItemPos = -1;
     50     private boolean mStarted = false;
     51     private Point mLastInterceptedPoint;
     52 
     53     GestureSelector(
     54             SelectionManager selectionMgr,
     55             IntSupplier heightSupplier,
     56             ViewFinder viewFinder,
     57             ScrollActionDelegate actionDelegate,
     58             DirectoryReloadLock lock) {
     59         mSelectionMgr = selectionMgr;
     60         mHeight = heightSupplier;
     61         mViewFinder = viewFinder;
     62         mLock = lock;
     63 
     64         ScrollDistanceDelegate distanceDelegate = new ScrollDistanceDelegate() {
     65             @Override
     66             public Point getCurrentPosition() {
     67                 return mLastInterceptedPoint;
     68             }
     69 
     70             @Override
     71             public int getViewHeight() {
     72                 return mHeight.getAsInt();
     73             }
     74 
     75             @Override
     76             public boolean isActive() {
     77                 return mStarted && mSelectionMgr.hasSelection();
     78             }
     79         };
     80 
     81         mDragScroller = new ViewAutoScroller(distanceDelegate, actionDelegate);
     82     }
     83 
     84     public static GestureSelector create(
     85             SelectionManager selectionMgr,
     86             RecyclerView scrollView,
     87             DirectoryReloadLock lock) {
     88         ScrollActionDelegate actionDelegate = new ScrollActionDelegate() {
     89             @Override
     90             public void scrollBy(int dy) {
     91                 scrollView.scrollBy(0, dy);
     92             }
     93 
     94             @Override
     95             public void runAtNextFrame(Runnable r) {
     96                 scrollView.postOnAnimation(r);
     97             }
     98 
     99             @Override
    100             public void removeCallback(Runnable r) {
    101                 scrollView.removeCallbacks(r);
    102             }
    103         };
    104         GestureSelector helper =
    105                 new GestureSelector(
    106                         selectionMgr,
    107                         scrollView::getHeight,
    108                         scrollView::findChildViewUnder,
    109                         actionDelegate,
    110                         lock);
    111 
    112         return helper;
    113     }
    114 
    115     // Explicitly kick off a gesture multi-select.
    116     public boolean start(InputEvent event) {
    117         //the anchor must already be set before a multi-select event can be started
    118         if (mLastStartedItemPos < 0) {
    119             if (DEBUG) Log.d(TAG, "Tried to start multi-select without setting an anchor.");
    120             return false;
    121         }
    122         if (mStarted) {
    123             return false;
    124         }
    125         mStarted = true;
    126         return true;
    127     }
    128 
    129     public boolean onInterceptTouchEvent(InputEvent e) {
    130         if (e.isMouseEvent()) {
    131             return false;
    132         }
    133 
    134         boolean handled = false;
    135 
    136         if (e.isActionDown()) {
    137             handled = handleInterceptedDownEvent(e);
    138         }
    139 
    140         if (e.isActionMove()) {
    141             handled = handleInterceptedMoveEvent(e);
    142         }
    143 
    144         return handled;
    145     }
    146 
    147     public void onTouchEvent(RecyclerView rv, InputEvent e) {
    148         if (!mStarted) {
    149             return;
    150         }
    151 
    152         if (e.isActionUp()) {
    153             handleUpEvent(e);
    154         }
    155 
    156         if (e.isActionCancel()) {
    157             handleCancelEvent(e);
    158         }
    159 
    160         if (e.isActionMove()) {
    161             handleOnTouchMoveEvent(rv, e);
    162         }
    163     }
    164 
    165     // Called when an ACTION_DOWN event is intercepted.
    166     // If down event happens on a file/doc, we mark that item's position as last started.
    167     private boolean handleInterceptedDownEvent(InputEvent e) {
    168         View itemView = mViewFinder.findView(e.getX(), e.getY());
    169         if (itemView != null) {
    170             mLastStartedItemPos = e.getItemPosition();
    171         }
    172         return false;
    173     }
    174 
    175     // Called when an ACTION_MOVE event is intercepted.
    176     private boolean handleInterceptedMoveEvent(InputEvent e) {
    177         mLastInterceptedPoint = e.getOrigin();
    178         if (mStarted) {
    179             mSelectionMgr.startRangeSelection(mLastStartedItemPos);
    180             // Gesture Selection about to start
    181             mLock.block();
    182             return true;
    183         }
    184         return false;
    185     }
    186 
    187     // Called when ACTION_UP event is to be handled.
    188     // Essentially, since this means all gesture movement is over, reset everything and apply
    189     // provisional selection.
    190     private void handleUpEvent(InputEvent e) {
    191         mSelectionMgr.getSelection().applyProvisionalSelection();
    192         endSelection();
    193     }
    194 
    195     // Called when ACTION_CANCEL event is to be handled.
    196     // This means this gesture selection is aborted, so reset everything and abandon provisional
    197     // selection.
    198     private void handleCancelEvent(InputEvent e) {
    199         mSelectionMgr.cancelProvisionalSelection();
    200         endSelection();
    201     }
    202 
    203     private void endSelection() {
    204         assert(mStarted);
    205         mLastStartedItemPos = -1;
    206         mStarted = false;
    207         mLock.unblock();
    208     }
    209 
    210     // Call when an intercepted ACTION_MOVE event is passed down.
    211     // At this point, we are sure user wants to gesture multi-select.
    212     private void handleOnTouchMoveEvent(RecyclerView rv, InputEvent e) {
    213         mLastInterceptedPoint = e.getOrigin();
    214 
    215         // If user has moved his pointer to the bottom-right empty pane (ie. to the right of the
    216         // last item of the recycler view), we would want to set that as the currentItemPos
    217         View lastItem = rv.getLayoutManager()
    218                 .getChildAt(rv.getLayoutManager().getChildCount() - 1);
    219         int direction = rv.getContext().getResources().getConfiguration().getLayoutDirection();
    220         final boolean pastLastItem = isPastLastItem(lastItem.getTop(),
    221                 lastItem.getLeft(),
    222                 lastItem.getRight(),
    223                 e,
    224                 direction);
    225 
    226         // Since views get attached & detached from RecyclerView,
    227         // {@link LayoutManager#getChildCount} can return a different number from the actual
    228         // number
    229         // of items in the adapter. Using the adapter is the for sure way to get the actual last
    230         // item position.
    231         final float inboundY = getInboundY(rv.getHeight(), e.getY());
    232         final int lastGlidedItemPos = (pastLastItem) ? rv.getAdapter().getItemCount() - 1
    233                 : rv.getChildAdapterPosition(rv.findChildViewUnder(e.getX(), inboundY));
    234         if (lastGlidedItemPos != RecyclerView.NO_POSITION) {
    235             doGestureMultiSelect(lastGlidedItemPos);
    236         }
    237         scrollIfNecessary();
    238     }
    239 
    240     // It's possible for events to go over the top/bottom of the RecyclerView.
    241     // We want to get a Y-coordinate within the RecyclerView so we can find the childView underneath
    242     // correctly.
    243     private static float getInboundY(float max, float y) {
    244         if (y < 0f) {
    245             return 0f;
    246         } else if (y > max) {
    247             return max;
    248         }
    249         return y;
    250     }
    251 
    252     /*
    253      * Check to see an InputEvent if past a particular item, i.e. to the right or to the bottom
    254      * of the item.
    255      * For RTL, it would to be to the left or to the bottom of the item.
    256      */
    257     @VisibleForTesting
    258     static boolean isPastLastItem(int top, int left, int right, InputEvent e, int direction) {
    259         if (direction == View.LAYOUT_DIRECTION_LTR) {
    260             return e.getX() > right && e.getY() > top;
    261         } else {
    262             return e.getX() < left && e.getY() > top;
    263         }
    264     }
    265 
    266     /* Given the end position, select everything in-between.
    267      * @param endPos  The adapter position of the end item.
    268      */
    269     private void doGestureMultiSelect(int endPos) {
    270         mSelectionMgr.snapProvisionalRangeSelection(endPos);
    271     }
    272 
    273     private void scrollIfNecessary() {
    274         mDragScroller.run();
    275     }
    276 
    277     @FunctionalInterface
    278     interface ViewFinder {
    279         @Nullable View findView(float x, float y);
    280     }
    281 }