Merge "Rework selection handling for items in the DirectoryFragment."
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user