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

Commit 0436a757 authored by Ben Kwa's avatar Ben Kwa
Browse files

Rework selection handling for items in the DirectoryFragment.

- Remove the gesture detector from the MultiSelectManager, and
  consolidate gesture detection and event dispatch logic in
  DirectoryFragment.GestureListener.

- Route single-tap events through the DocumentHolder, so that it can
  apply view-specific logic, like making a tap on the item's icon
  select rather than activate.

- Consolidate event handling logic in the ItemEventListener.

- Add new unit tests for DocumentHandler.

BUG=24326546

Change-Id: Id15cdd11b13e4c063c1baff95aa8ee09c190d6c3
parent 00f33ec9
Loading
Loading
Loading
Loading
+3 −5
Original line number Diff line number Diff line
@@ -111,17 +111,15 @@ public final class Events {

    public static final class MotionInputEvent implements InputEvent {
        private final MotionEvent mEvent;
        private final RecyclerView mView;
        private final int mPosition;

        public MotionInputEvent(MotionEvent event, RecyclerView view) {
            mEvent = event;
            mView = view;

            // Consider determining position lazily as an optimization.
            View child = mView.findChildViewUnder(mEvent.getX(), mEvent.getY());
            View child = view.findChildViewUnder(mEvent.getX(), mEvent.getY());
            mPosition = (child!= null)
                    ? mView.getChildAdapterPosition(child)
                    ? view.getChildAdapterPosition(child)
                    : RecyclerView.NO_POSITION;
        }

+65 −37
Original line number Diff line number Diff line
@@ -84,6 +84,7 @@ import com.android.documentsui.DocumentClipper;
import com.android.documentsui.DocumentsActivity;
import com.android.documentsui.DocumentsApplication;
import com.android.documentsui.Events;
import com.android.documentsui.Events.MotionInputEvent;
import com.android.documentsui.Menus;
import com.android.documentsui.MessageBar;
import com.android.documentsui.MimePredicate;
@@ -138,7 +139,7 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi
    private Model mModel;
    private MultiSelectManager mSelectionManager;
    private Model.UpdateListener mModelUpdateListener = new ModelUpdateListener();
    private ItemClickListener mItemClickListener = new ItemClickListener();
    private ItemEventListener mItemEventListener = new ItemEventListener();

    private IconHelper mIconHelper;

@@ -297,19 +298,7 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi

        mRecView.setAdapter(mAdapter);

        GestureDetector.SimpleOnGestureListener listener =
                new GestureDetector.SimpleOnGestureListener() {
                    @Override
                    public boolean onSingleTapUp(MotionEvent e) {
                        return DirectoryFragment.this.onSingleTapUp(e);
                    }
                    @Override
                    public boolean onDoubleTap(MotionEvent e) {
                        Log.d(TAG, "Handling double tap.");
                        return DirectoryFragment.this.onDoubleTap(e);
                    }
                };

        GestureListener listener = new GestureListener();
        final GestureDetector detector = new GestureDetector(this.getContext(), listener);
        detector.setOnDoubleTapListener(listener);

@@ -466,22 +455,8 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi
                operationType);
    }

    private boolean onSingleTapUp(MotionEvent e) {
        // Only respond to touch events.  Single-click mouse events are selection events and are
        // handled by the selection manager.  Tap events that occur while the selection manager is
        // active are also selection events.
        if (Events.isTouchEvent(e) && !mSelectionManager.hasSelection()) {
            String id = getModelId(e);
            if (id != null) {
                return handleViewItem(id);
            }
        }
        return false;
    }

    protected boolean onDoubleTap(MotionEvent e) {
        if (Events.isMouseEvent(e)) {
            Log.d(TAG, "Handling double tap from mouse.");
            String id = getModelId(e);
            if (id != null) {
                return handleViewItem(id);
@@ -926,7 +901,7 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi

    @Override
    public void initDocumentHolder(DocumentHolder holder) {
        holder.addClickListener(mItemClickListener);
        holder.addEventListener(mItemEventListener);
        holder.addOnKeyListener(mSelectionManager);
    }

@@ -1330,15 +1305,18 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi
        return mSelectionManager.getSelection().contains(modelId);
    }

    private class ItemClickListener implements DocumentHolder.ClickListener {
    private class ItemEventListener implements DocumentHolder.EventListener {
        @Override
        public void onClick(DocumentHolder doc) {
            if (mSelectionManager.hasSelection()) {
                mSelectionManager.toggleSelection(doc.modelId);
                mSelectionManager.setSelectionRangeBegin(doc.getAdapterPosition());
            } else {
        public boolean onActivate(DocumentHolder doc) {
            handleViewItem(doc.modelId);
            return true;
        }

        @Override
        public boolean onSelect(DocumentHolder doc) {
            mSelectionManager.toggleSelection(doc.modelId);
            mSelectionManager.setSelectionRangeBegin(doc.getAdapterPosition());
            return true;
        }
    }

@@ -1366,4 +1344,54 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi
            showErrorView();
        }
    }

    /**
     * The gesture listener for items in the list/grid view. Interprets gestures and sends the
     * events to the target DocumentHolder, whence they are routed to the appropriate listener.
     */
    private class GestureListener extends GestureDetector.SimpleOnGestureListener {
        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            // Single tap logic:
            // If the selection manager is active, it gets first whack at handling tap
            // events. Otherwise, tap events are routed to the target DocumentHolder.
            boolean handled = mSelectionManager.onSingleTapUp(
                        new MotionInputEvent(e, mRecView));

            if (handled) {
                return handled;
            }

            // Give the DocumentHolder a crack at the event.
            DocumentHolder holder = getTarget(e);
            if (holder != null) {
                handled = holder.onSingleTapUp(e);
            }

            return handled;
        }

        @Override
        public void onLongPress(MotionEvent e) {
            // Long-press events get routed directly to the selection manager. They can be
            // changed to route through the DocumentHolder if necessary.
            mSelectionManager.onLongPress(new MotionInputEvent(e, mRecView));
        }

        @Override
        public boolean onDoubleTap(MotionEvent e) {
            // Double-tap events are handled directly by the DirectoryFragment. They can be changed
            // to route through the DocumentHolder if necessary.
            return DirectoryFragment.this.onDoubleTap(e);
        }

        private @Nullable DocumentHolder getTarget(MotionEvent e) {
            View childView = mRecView.findChildViewUnder(e.getX(), e.getY());
            if (childView != null) {
                return (DocumentHolder) mRecView.getChildViewHolder(childView);
            } else {
                return null;
            }
        }
    }
}
+57 −12
Original line number Diff line number Diff line
@@ -16,17 +16,21 @@

package com.android.documentsui.dirlist;

import static com.android.internal.util.Preconditions.checkNotNull;
import static com.android.internal.util.Preconditions.checkState;

import android.content.Context;
import android.database.Cursor;
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;

import com.android.documentsui.Events;
import com.android.documentsui.R;
import com.android.documentsui.State;

@@ -41,8 +45,9 @@ public abstract class DocumentHolder
    final boolean mAlwaysShowSummary;
    final Context mContext;

    private ListDocumentHolder.ClickListener mClickListener;
    DocumentHolder.EventListener mEventListener;
    private View.OnKeyListener mKeyListener;
    private View mSelectionHotspot;

    public DocumentHolder(Context context, ViewGroup parent, int layout) {
        this(context, inflateLayout(context, parent, layout));
@@ -58,6 +63,8 @@ public abstract class DocumentHolder
        mDefaultItemColor = context.getColor(R.color.item_doc_background);
        mSelectedItemColor = context.getColor(R.color.item_doc_background_selected);
        mAlwaysShowSummary = context.getResources().getBoolean(R.bool.always_show_summary);

        mSelectionHotspot = itemView.findViewById(R.id.icon_check);
    }

    /**
@@ -75,23 +82,21 @@ public abstract class DocumentHolder

    @Override
    public boolean onKey(View v, int keyCode, KeyEvent event) {
        // Event listener should always be set.
        checkNotNull(mEventListener);
        // Intercept enter key-up events, and treat them as clicks.  Forward other events.
        if (event.getAction() == KeyEvent.ACTION_UP &&
                keyCode == KeyEvent.KEYCODE_ENTER) {
            if (mClickListener != null) {
                mClickListener.onClick(this);
            }
            return true;
        if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_ENTER) {
            return mEventListener.onActivate(this);
        } else if (mKeyListener != null) {
            return mKeyListener.onKey(v, keyCode, event);
        }
        return false;
    }

    public void addClickListener(ListDocumentHolder.ClickListener listener) {
    public void addEventListener(DocumentHolder.EventListener listener) {
        // Just handle one for now; switch to a list if necessary.
        checkState(mClickListener == null);
        mClickListener = listener;
        checkState(mEventListener == null);
        mEventListener = listener;
    }

    public void addOnKeyListener(View.OnKeyListener listener) {
@@ -104,6 +109,33 @@ public abstract class DocumentHolder
        setEnabledRecursive(itemView, enabled);
    }

    public boolean onSingleTapUp(MotionEvent event) {
        if (Events.isMouseEvent(event)) {
            // Mouse clicks select.
            // TODO:  && input.isPrimaryButtonPressed(), but it is returning false.
            if (mEventListener != null) {
                return mEventListener.onSelect(this);
            }
        } else if (Events.isTouchEvent(event)) {
            // Touch events select if they occur in the selection hotspot, otherwise they activate.
            if (mEventListener == null) {
                return false;
            }

            // Do everything in global coordinates - it makes things simpler.
            Rect rect = new Rect();
            mSelectionHotspot.getGlobalVisibleRect(rect);

            // If the tap occurred within the icon rect, consider it a selection.
            if (rect.contains((int)event.getRawX(), (int)event.getRawY())) {
                return mEventListener.onSelect(this);
            } else {
                return mEventListener.onActivate(this);
            }
        }
        return false;
    }

    static void setEnabledRecursive(View itemView, boolean enabled) {
        if (itemView == null) return;
        if (itemView.isEnabled() == enabled) return;
@@ -122,7 +154,20 @@ public abstract class DocumentHolder
        return inflater.inflate(layout, parent, false);
    }

    interface ClickListener {
        public void onClick(DocumentHolder doc);
    /**
     * Implement this in order to be able to respond to events coming from DocumentHolders.
     */
    interface EventListener {
        /**
         * @param doc The target DocumentHolder
         * @return Whether the event was handled.
         */
        public boolean onActivate(DocumentHolder doc);

        /**
         * @param doc The target DocumentHolder
         * @return Whether the event was handled.
         */
        public boolean onSelect(DocumentHolder doc);
    }
}
+1 −27
Original line number Diff line number Diff line
@@ -32,7 +32,6 @@ import android.util.Log;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
import android.view.GestureDetector;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
@@ -90,29 +89,10 @@ public final class MultiSelectManager implements View.OnKeyListener {
            mBandManager = new BandController();
        }

        GestureDetector.SimpleOnGestureListener listener =
                new GestureDetector.SimpleOnGestureListener() {
                    @Override
                    public boolean onSingleTapUp(MotionEvent e) {
                        return MultiSelectManager.this.onSingleTapUp(
                                new MotionInputEvent(e, recyclerView));
                    }
                    @Override
                    public void onLongPress(MotionEvent e) {
                        MultiSelectManager.this.onLongPress(
                                new MotionInputEvent(e, recyclerView));
                    }
                };

        final GestureDetector detector = new GestureDetector(recyclerView.getContext(), listener);
        detector.setOnDoubleTapListener(listener);

        recyclerView.addOnItemTouchListener(
                new RecyclerView.OnItemTouchListener() {
                    @Override
                    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
                        detector.onTouchEvent(e);

                        if (mBandManager != null) {
                            return mBandManager.handleEvent(new MotionInputEvent(e, recyclerView));
                        }
@@ -287,13 +267,7 @@ public final class MultiSelectManager implements View.OnKeyListener {
    boolean onSingleTapUp(InputEvent input) {
        if (DEBUG) Log.d(TAG, "Processing tap event.");
        if (!hasSelection()) {
            // if this is a mouse click on an item, start selection mode.
            // TODO:  && input.isPrimaryButtonPressed(), but it is returning false.
            if (input.isOverItem() && input.isMouseEvent()) {
                int position = input.getItemPosition();
                toggleSelection(position);
                setSelectionRangeBegin(position);
            }
            // No selection active - do nothing.
            return false;
        }

+134 −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.database.Cursor;
import android.graphics.Rect;
import android.os.SystemClock;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.MotionEvent.PointerCoords;
import android.view.MotionEvent.PointerProperties;

import com.android.documentsui.R;
import com.android.documentsui.State;

@SmallTest
public class DocumentHolderTest extends AndroidTestCase {

    DocumentHolder mHolder;
    TestListener mListener;

    public void setUp() throws Exception {
        Context context = getContext();
        LayoutInflater inflater = LayoutInflater.from(context);
        mHolder = new DocumentHolder(getContext(), inflater.inflate(R.layout.item_doc_list, null)) {
            @Override
            public void bind(Cursor cursor, String modelId, State state) {}
        };

        mListener = new TestListener();
        mHolder.addEventListener(mListener);

        mHolder.itemView.requestLayout();
        mHolder.itemView.invalidate();
    }

    public void testClickActivates() {
        click();
        mListener.assertSelected();
    }

    public void testTapActivates() {
        tap();
        mListener.assertActivated();
    }

    public void click() {
        mHolder.onSingleTapUp(createEvent(MotionEvent.TOOL_TYPE_MOUSE));
    }

    public void tap() {
        mHolder.onSingleTapUp(createEvent(MotionEvent.TOOL_TYPE_FINGER));
    }

    public MotionEvent createEvent(int tooltype) {
        long time = SystemClock.uptimeMillis();

        PointerProperties properties[] = new PointerProperties[] {
                new PointerProperties()
        };
        properties[0].toolType = tooltype;

        PointerCoords coords[] = new PointerCoords[] {
                new PointerCoords()
        };

        Rect rect = new Rect();
        mHolder.itemView.getHitRect(rect);
        coords[0].x = rect.left;
        coords[0].y = rect.top;

        return MotionEvent.obtain(
                time, // down time
                time, // event time
                MotionEvent.ACTION_UP, // action
                1, // pointer count
                properties, // pointer properties
                coords, // pointer coords
                0, // metastate
                0, // button state
                0, // xprecision
                0, // yprecision
                0, // deviceid
                0, // edgeflags
                0, // source
                0 // flags
                );
    }

    private class TestListener implements DocumentHolder.EventListener {
        private boolean mActivated = false;
        private boolean mSelected = false;

        public void assertActivated() {
            assertTrue(mActivated);
            assertFalse(mSelected);
        }

        public void assertSelected() {
            assertTrue(mSelected);
            assertFalse(mActivated);
        }

        @Override
        public boolean onActivate(DocumentHolder doc) {
            mActivated = true;
            return true;
        }

        @Override
        public boolean onSelect(DocumentHolder doc) {
            mSelected = true;
            return true;
        }

    }
}
Loading