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

This commit is contained in:
Ben Kwa
2016-01-21 21:28:46 +00:00
committed by Android (Google) Code Review
6 changed files with 272 additions and 86 deletions

View File

@@ -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;
}

View File

@@ -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;
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}
}

View File

@@ -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();
}