diff --git a/packages/DocumentsUI/src/com/android/documentsui/Events.java b/packages/DocumentsUI/src/com/android/documentsui/Events.java index 1b5b60de6f243..10a78b9f8b51f 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/Events.java +++ b/packages/DocumentsUI/src/com/android/documentsui/Events.java @@ -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()); - mPosition = (child != null) - ? mView.getChildAdapterPosition(child) + View child = view.findChildViewUnder(mEvent.getX(), mEvent.getY()); + mPosition = (child!= null) + ? view.getChildAdapterPosition(child) : RecyclerView.NO_POSITION; } diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java index 580e2d8d97b74..053b618ae3ac6 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java +++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java @@ -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 { - handleViewItem(doc.modelId); - } + 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; + } + } + } } diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DocumentHolder.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DocumentHolder.java index 9ac905711ba27..8acf1af8713bf 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DocumentHolder.java +++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DocumentHolder.java @@ -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); } } diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java index d868fb46de980..9cbcf8cfa3829 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java +++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java @@ -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; } diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/DocumentHolderTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/DocumentHolderTest.java new file mode 100644 index 0000000000000..16efc6e981746 --- /dev/null +++ b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/DocumentHolderTest.java @@ -0,0 +1,134 @@ +/* + * 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; + } + + } +} diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java index 7a3b6d4309706..d3ef9aa64ffbd 100644 --- a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java +++ b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java @@ -23,7 +23,6 @@ import android.util.SparseBooleanArray; import com.android.documentsui.TestInputEvent; import com.android.documentsui.dirlist.MultiSelectManager.Selection; - import com.google.common.collect.Lists; import java.util.ArrayList; @@ -55,13 +54,21 @@ public class MultiSelectManagerTest extends AndroidTestCase { mManager.addCallback(mCallback); } - public void testMouseClick_StartsSelectionMode() { - click(7); + public void testSelection() { + // Check selection. + mManager.toggleSelection(items.get(7)); assertSelection(items.get(7)); + // Check deselection. + mManager.toggleSelection(items.get(7)); + assertSelectionSize(0); } - public void testMouseClick_NotifiesSelectionChanged() { - click(7); + public void testSelection_NotifiesSelectionChanged() { + // Selection should notify. + mManager.toggleSelection(items.get(7)); + mCallback.assertSelectionChanged(); + // Deselection should notify. + mManager.toggleSelection(items.get(7)); mCallback.assertSelectionChanged(); }