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

Commit 8099a431 authored by Ben Kwa's avatar Ben Kwa Committed by Android (Google) Code Review
Browse files

Merge "Rework selection handling for items in the DirectoryFragment."

parents d71f9067 9fe25a7a
Loading
Loading
Loading
Loading
+3 −5
Original line number Original line Diff line number Diff line
@@ -111,17 +111,15 @@ public final class Events {


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


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


            // Consider determining position lazily as an optimization.
            // 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)
            mPosition = (child!= null)
                    ? mView.getChildAdapterPosition(child)
                    ? view.getChildAdapterPosition(child)
                    : RecyclerView.NO_POSITION;
                    : RecyclerView.NO_POSITION;
        }
        }


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


    private IconHelper mIconHelper;
    private IconHelper mIconHelper;


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


        mRecView.setAdapter(mAdapter);
        mRecView.setAdapter(mAdapter);


        GestureDetector.SimpleOnGestureListener listener =
        GestureListener listener = new GestureListener();
                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);
                    }
                };

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


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


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


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


    private class ItemClickListener implements DocumentHolder.ClickListener {
    private class ItemEventListener implements DocumentHolder.EventListener {
        @Override
        @Override
        public void onClick(DocumentHolder doc) {
        public boolean onActivate(DocumentHolder doc) {
            if (mSelectionManager.hasSelection()) {
                mSelectionManager.toggleSelection(doc.modelId);
                mSelectionManager.setSelectionRangeBegin(doc.getAdapterPosition());
            } else {
            handleViewItem(doc.modelId);
            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();
            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 Original line Diff line number Diff line
@@ -16,17 +16,21 @@


package com.android.documentsui.dirlist;
package com.android.documentsui.dirlist;


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


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


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


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


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


    public DocumentHolder(Context context, ViewGroup parent, int layout) {
    public DocumentHolder(Context context, ViewGroup parent, int layout) {
        this(context, inflateLayout(context, parent, layout));
        this(context, inflateLayout(context, parent, layout));
@@ -58,6 +63,8 @@ public abstract class DocumentHolder
        mDefaultItemColor = context.getColor(R.color.item_doc_background);
        mDefaultItemColor = context.getColor(R.color.item_doc_background);
        mSelectedItemColor = context.getColor(R.color.item_doc_background_selected);
        mSelectedItemColor = context.getColor(R.color.item_doc_background_selected);
        mAlwaysShowSummary = context.getResources().getBoolean(R.bool.always_show_summary);
        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
    @Override
    public boolean onKey(View v, int keyCode, KeyEvent event) {
    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.
        // Intercept enter key-up events, and treat them as clicks.  Forward other events.
        if (event.getAction() == KeyEvent.ACTION_UP &&
        if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_ENTER) {
                keyCode == KeyEvent.KEYCODE_ENTER) {
            return mEventListener.onActivate(this);
            if (mClickListener != null) {
                mClickListener.onClick(this);
            }
            return true;
        } else if (mKeyListener != null) {
        } else if (mKeyListener != null) {
            return mKeyListener.onKey(v, keyCode, event);
            return mKeyListener.onKey(v, keyCode, event);
        }
        }
        return false;
        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.
        // Just handle one for now; switch to a list if necessary.
        checkState(mClickListener == null);
        checkState(mEventListener == null);
        mClickListener = listener;
        mEventListener = listener;
    }
    }


    public void addOnKeyListener(View.OnKeyListener listener) {
    public void addOnKeyListener(View.OnKeyListener listener) {
@@ -104,6 +109,33 @@ public abstract class DocumentHolder
        setEnabledRecursive(itemView, enabled);
        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) {
    static void setEnabledRecursive(View itemView, boolean enabled) {
        if (itemView == null) return;
        if (itemView == null) return;
        if (itemView.isEnabled() == enabled) return;
        if (itemView.isEnabled() == enabled) return;
@@ -122,7 +154,20 @@ public abstract class DocumentHolder
        return inflater.inflate(layout, parent, false);
        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 Original line Diff line number Diff line
@@ -32,7 +32,6 @@ import android.util.Log;
import android.util.SparseArray;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
import android.util.SparseIntArray;
import android.view.GestureDetector;
import android.view.KeyEvent;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.View;
@@ -90,29 +89,10 @@ public final class MultiSelectManager implements View.OnKeyListener {
            mBandManager = new BandController();
            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(
        recyclerView.addOnItemTouchListener(
                new RecyclerView.OnItemTouchListener() {
                new RecyclerView.OnItemTouchListener() {
                    @Override
                    @Override
                    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
                    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
                        detector.onTouchEvent(e);

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


+134 −0
Original line number Original line 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