Update DirectoryFragment to use RecyclerView.
Add MultiSelectMaanger class to manager selection on a RecyclerView instance. There are several outstanding issues that still need to be addressed surrounding Grid mode as the GridLayout manager doesn't support automatic column count calculation. Also, we're missing the puddle effect on touch... And probably other stuff. But it all *mostly* works. Oh, also. Footers are currently commented out. Add traditional unit tests for MultiSelectManager. BUG: 22225617 Change-Id: I3cd26a10683f42053556d463a5d2f0d2a0bbde84
This commit is contained in:
@@ -185,6 +185,11 @@ public class SparseBooleanArray implements Cloneable {
|
||||
mValues[index] = value;
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
public void setKeyAt(int index, int key) {
|
||||
mKeys[index] = key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index for which {@link #keyAt} would return the
|
||||
* specified key, or a negative number if the specified
|
||||
|
||||
@@ -5,7 +5,9 @@ LOCAL_MODULE_TAGS := optional
|
||||
|
||||
LOCAL_SRC_FILES := $(call all-java-files-under, src)
|
||||
|
||||
LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 guava
|
||||
LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 \
|
||||
android-support-v7-recyclerview \
|
||||
guava
|
||||
|
||||
LOCAL_PACKAGE_NAME := DocumentsUI
|
||||
LOCAL_CERTIFICATE := platform
|
||||
|
||||
@@ -28,24 +28,23 @@
|
||||
android:visibility="gone"
|
||||
style="@android:style/TextAppearance.Material.Subhead" />
|
||||
|
||||
<!-- The 'list' view is still used for RecentsCreateFragment -->
|
||||
<ListView
|
||||
android:id="@+id/list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<GridView
|
||||
android:id="@+id/grid"
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:scrollbars="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingStart="@dimen/grid_padding_horiz"
|
||||
android:paddingEnd="@dimen/grid_padding_horiz"
|
||||
android:paddingTop="@dimen/grid_padding_vert"
|
||||
android:paddingBottom="@dimen/grid_padding_vert"
|
||||
android:horizontalSpacing="@dimen/grid_item_padding"
|
||||
android:verticalSpacing="@dimen/grid_item_padding"
|
||||
android:clipToPadding="false"
|
||||
android:scrollbarStyle="outsideOverlay"
|
||||
android:drawSelectorOnTop="true"
|
||||
android:visibility="gone" />
|
||||
android:drawSelectorOnTop="true" />
|
||||
|
||||
</com.android.documentsui.DirectoryView>
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/grid_item_height"
|
||||
android:background="@color/item_doc_grid_background">
|
||||
android:background="@color/item_doc_grid_background"
|
||||
android:padding="@dimen/grid_item_padding">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon_thumb"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,548 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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;
|
||||
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.RecyclerView.Adapter;
|
||||
import android.support.v7.widget.RecyclerView.AdapterDataObserver;
|
||||
import android.util.Log;
|
||||
import android.util.SparseBooleanArray;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.GestureDetector.OnGestureListener;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* MultiSelectManager adds traditional multi-item selection support to RecyclerView.
|
||||
*/
|
||||
public final class MultiSelectManager {
|
||||
|
||||
private static final String TAG = "MultiSelectManager";
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private final Selection mSelection = new Selection();
|
||||
// Only created when selection is cleared.
|
||||
private Selection mIntermediateSelection;
|
||||
|
||||
private final List<MultiSelectManager.Callback> mCallbacks = new ArrayList<>(1);
|
||||
|
||||
private Adapter<?> mAdapter;
|
||||
private RecyclerViewHelper mHelper;
|
||||
|
||||
/**
|
||||
* @param recyclerView
|
||||
* @param gestureDelegate Option delage gesture listener.
|
||||
*/
|
||||
public MultiSelectManager(final RecyclerView recyclerView, OnGestureListener gestureDelegate) {
|
||||
this(
|
||||
recyclerView.getAdapter(),
|
||||
new RecyclerViewHelper() {
|
||||
@Override
|
||||
public int findEventPosition(MotionEvent e) {
|
||||
View view = recyclerView.findChildViewUnder(e.getX(), e.getY());
|
||||
return view != null
|
||||
? recyclerView.getChildAdapterPosition(view)
|
||||
: RecyclerView.NO_POSITION;
|
||||
}
|
||||
});
|
||||
|
||||
GestureDetector.SimpleOnGestureListener listener =
|
||||
new GestureDetector.SimpleOnGestureListener() {
|
||||
@Override
|
||||
public boolean onSingleTapUp(MotionEvent e) {
|
||||
return MultiSelectManager.this.onSingleTapUp(e);
|
||||
}
|
||||
@Override
|
||||
public void onLongPress(MotionEvent e) {
|
||||
MultiSelectManager.this.onLongPress(e);
|
||||
}
|
||||
};
|
||||
|
||||
final GestureDetector detector = new GestureDetector(
|
||||
recyclerView.getContext(),
|
||||
gestureDelegate == null
|
||||
? listener
|
||||
: new CompositeOnGestureListener(listener, gestureDelegate));
|
||||
|
||||
recyclerView.addOnItemTouchListener(
|
||||
new RecyclerView.OnItemTouchListener() {
|
||||
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
|
||||
detector.onTouchEvent(e);
|
||||
return false;
|
||||
}
|
||||
public void onTouchEvent(RecyclerView rv, MotionEvent e) {}
|
||||
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {}
|
||||
});
|
||||
}
|
||||
|
||||
MultiSelectManager(Adapter<?> adapter, RecyclerViewHelper helper) {
|
||||
if (adapter == null) {
|
||||
throw new IllegalArgumentException("Adapter cannot be null.");
|
||||
}
|
||||
if (helper == null) {
|
||||
throw new IllegalArgumentException("Helper cannot be null.");
|
||||
}
|
||||
|
||||
mHelper = helper;
|
||||
mAdapter = adapter;
|
||||
|
||||
mAdapter.registerAdapterDataObserver(
|
||||
new AdapterDataObserver() {
|
||||
|
||||
@Override
|
||||
public void onChanged() {
|
||||
mSelection.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeChanged(
|
||||
int positionStart, int itemCount, Object payload) {
|
||||
// No change in position. Ignoring.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeInserted(int positionStart, int itemCount) {
|
||||
mSelection.expand(positionStart, itemCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeRemoved(int positionStart, int itemCount) {
|
||||
mSelection.collapse(positionStart, itemCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void addCallback(MultiSelectManager.Callback callback) {
|
||||
mCallbacks.add(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Selection object that provides a live view
|
||||
* on the current selection. Callers wishing to get
|
||||
*
|
||||
* @see #getSelectionSnapshot() on how to get a snapshot
|
||||
* of the selection that will not reflect future changes
|
||||
* to selection.
|
||||
*
|
||||
* @return The current seleciton.
|
||||
*/
|
||||
public Selection getSelection() {
|
||||
return mSelection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates {@code dest} to reflect the current selection.
|
||||
* @param dest
|
||||
*
|
||||
* @return The Selection instance passed in, for convenience.
|
||||
*/
|
||||
public Selection getSelection(Selection dest) {
|
||||
dest.copyFrom(mSelection);
|
||||
return dest;
|
||||
}
|
||||
|
||||
public void selectItem(int position) {
|
||||
selectItems(position, 1);
|
||||
}
|
||||
|
||||
public void selectItems(int position, int length) {
|
||||
for (int i = position; i < position + length; i++) {
|
||||
mSelection.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
public void clearSelection() {
|
||||
if (DEBUG) Log.d(TAG, "Clearing selection");
|
||||
if (mIntermediateSelection == null) {
|
||||
mIntermediateSelection = new Selection();
|
||||
}
|
||||
getSelection(mIntermediateSelection);
|
||||
mSelection.clear();
|
||||
|
||||
for (int i = 0; i < mIntermediateSelection.size(); i++) {
|
||||
int position = mIntermediateSelection.get(i);
|
||||
mAdapter.notifyItemChanged(position);
|
||||
notifyItemStateChanged(position, false);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean onSingleTapUp(MotionEvent e) {
|
||||
if (DEBUG) Log.d(TAG, "Handling tap event.");
|
||||
if (mSelection.size() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return onSingleTapUp(mHelper.findEventPosition(e));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param position
|
||||
* @hide
|
||||
*/
|
||||
@VisibleForTesting
|
||||
boolean onSingleTapUp(int position) {
|
||||
if (mSelection.size() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (position == RecyclerView.NO_POSITION) {
|
||||
if (DEBUG) Log.i(TAG, "View is null. Cannot handle tap event.");
|
||||
return false;
|
||||
}
|
||||
|
||||
toggleSelection(position);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void onLongPress(MotionEvent e) {
|
||||
if (DEBUG) Log.d(TAG, "Handling long press event.");
|
||||
|
||||
int position = mHelper.findEventPosition(e);
|
||||
if (position == RecyclerView.NO_POSITION) {
|
||||
if (DEBUG) Log.i(TAG, "View is null. Cannot handle tap event.");
|
||||
}
|
||||
|
||||
toggleSelection(position);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param position
|
||||
* @hide
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void onLongPress(int position) {
|
||||
if (position == RecyclerView.NO_POSITION) {
|
||||
if (DEBUG) Log.i(TAG, "View is null. Cannot handle tap event.");
|
||||
}
|
||||
|
||||
toggleSelection(position);
|
||||
}
|
||||
|
||||
private void toggleSelection(int position) {
|
||||
// Position may be special "no position" during certain
|
||||
// transitional phases. If so, skip handling of the event.
|
||||
if (position == RecyclerView.NO_POSITION) {
|
||||
if (DEBUG) Log.d(TAG, "Ignoring toggle for element with no position.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (DEBUG) Log.d(TAG, "Handling long press on view: " + position);
|
||||
boolean nextState = !mSelection.contains(position);
|
||||
if (notifyBeforeItemStateChange(position, nextState)) {
|
||||
boolean selected = mSelection.flip(position);
|
||||
notifyItemStateChanged(position, selected);
|
||||
mAdapter.notifyItemChanged(position);
|
||||
if (DEBUG) Log.d(TAG, "Selection after long press: " + mSelection);
|
||||
} else {
|
||||
Log.i(TAG, "Selection change cancelled by listener.");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean notifyBeforeItemStateChange(int position, boolean nextState) {
|
||||
int lastListener = mCallbacks.size() - 1;
|
||||
for (int i = lastListener; i > -1; i--) {
|
||||
if (!mCallbacks.get(i).onBeforeItemStateChange(position, nextState)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies registered listeners when a selection changes.
|
||||
*
|
||||
* @param position
|
||||
* @param selected
|
||||
*/
|
||||
private void notifyItemStateChanged(int position, boolean selected) {
|
||||
int lastListener = mCallbacks.size() - 1;
|
||||
for (int i = lastListener; i > -1; i--) {
|
||||
mCallbacks.get(i).onItemStateChanged(position, selected);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Object representing the current selection.
|
||||
*/
|
||||
// NOTE: Much of the code in this class was copious swiped from
|
||||
// ArrayUtils, GrowingArrayUtils, and SparseBooleanArray.
|
||||
public static final class Selection {
|
||||
|
||||
private SparseBooleanArray mSelection;
|
||||
|
||||
public Selection() {
|
||||
mSelection = new SparseBooleanArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param position
|
||||
* @return true if the position is currently selected.
|
||||
*/
|
||||
public boolean contains(int position) {
|
||||
return mSelection.get(position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Useful for iterating over selection. Please note that
|
||||
* iteration should be done over a copy of the selection,
|
||||
* not the live selection.
|
||||
*
|
||||
* @see #copyTo(MultiSelectManager.Selection)
|
||||
*
|
||||
* @param index
|
||||
* @return the position value stored at specified index.
|
||||
*/
|
||||
public int get(int index) {
|
||||
return mSelection.keyAt(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return size of the selection.
|
||||
*/
|
||||
public int size() {
|
||||
return mSelection.size();
|
||||
}
|
||||
|
||||
private boolean flip(int position) {
|
||||
if (contains(position)) {
|
||||
remove(position);
|
||||
return false;
|
||||
} else {
|
||||
add(position);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
@VisibleForTesting
|
||||
void add(int position) {
|
||||
mSelection.put(position, true);
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
@VisibleForTesting
|
||||
void remove(int position) {
|
||||
mSelection.delete(position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts the selection range to reflect the existence of newly inserted values at
|
||||
* the specified positions. This has the effect of adjusting all existing selected
|
||||
* positions within the specified range accordingly.
|
||||
*
|
||||
* @param startPosition
|
||||
* @param count
|
||||
* @hide
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void expand(int startPosition, int count) {
|
||||
if (startPosition < 0) {
|
||||
throw new IllegalArgumentException("startPosition must be non-negative");
|
||||
}
|
||||
if (count < 1) {
|
||||
throw new IllegalArgumentException("countMust be greater than 0");
|
||||
}
|
||||
|
||||
for (int i = 0; i < mSelection.size(); i++) {
|
||||
int itemPosition = mSelection.keyAt(i);
|
||||
if (itemPosition >= startPosition) {
|
||||
mSelection.setKeyAt(i, itemPosition + count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts the selection range to reflect the removal specified positions. This has
|
||||
* the effect of adjusting all existing selected positions within the specified range
|
||||
* accordingly.
|
||||
*
|
||||
* @param startPosition
|
||||
* @param count The length of the range to collapse. Must be greater than 0.
|
||||
* @hide
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void collapse(int startPosition, int count) {
|
||||
if (startPosition < 0) {
|
||||
throw new IllegalArgumentException("startPosition must be non-negative");
|
||||
}
|
||||
if (count < 1) {
|
||||
throw new IllegalArgumentException("countMust be greater than 0");
|
||||
}
|
||||
|
||||
int endPosition = startPosition + count - 1;
|
||||
|
||||
SparseBooleanArray newSelection = new SparseBooleanArray();
|
||||
for (int i = 0; i < mSelection.size(); i++) {
|
||||
int itemPosition = mSelection.keyAt(i);
|
||||
if (itemPosition < startPosition) {
|
||||
newSelection.append(itemPosition, true);
|
||||
} else if (itemPosition > endPosition) {
|
||||
newSelection.append(itemPosition - count, true);
|
||||
}
|
||||
}
|
||||
mSelection = newSelection;
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
@VisibleForTesting
|
||||
void clear() {
|
||||
mSelection.clear();
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
@VisibleForTesting
|
||||
void copyFrom(Selection source) {
|
||||
mSelection = source.mSelection.clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (size() <= 0) {
|
||||
return "size=0, items=[]";
|
||||
}
|
||||
|
||||
StringBuilder buffer = new StringBuilder(mSelection.size() * 28);
|
||||
buffer.append(String.format("{size=%d, ", mSelection.size()));
|
||||
buffer.append("items=[");
|
||||
for (int i=0; i < mSelection.size(); i++) {
|
||||
if (i > 0) {
|
||||
buffer.append(", ");
|
||||
}
|
||||
buffer.append(mSelection.keyAt(i));
|
||||
}
|
||||
buffer.append("]}");
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object that) {
|
||||
if (this == that) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (that instanceof Selection) {
|
||||
Selection other = (Selection) that;
|
||||
for (int i = 0; i < mSelection.size(); i++) {
|
||||
if (mSelection.keyAt(i) != other.mSelection.keyAt(i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
interface RecyclerViewHelper {
|
||||
int findEventPosition(MotionEvent e);
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
/**
|
||||
* Called when an item is selected or unselected while in selection mode.
|
||||
*
|
||||
* @param position Adapter position of the item that was checked or unchecked
|
||||
* @param selected <code>true</code> if the item is now selected, <code>false</code>
|
||||
* if the item is now unselected.
|
||||
*/
|
||||
public void onItemStateChanged(int position, boolean selected);
|
||||
|
||||
/**
|
||||
* @param position
|
||||
* @param selected
|
||||
* @return false to cancel the change.
|
||||
*/
|
||||
public boolean onBeforeItemStateChange(int position, boolean selected);
|
||||
}
|
||||
|
||||
/**
|
||||
* A composite {@code OnGestureDetector} that allows us to delegate unhandled
|
||||
* events to other interested parties.
|
||||
*/
|
||||
private static final class CompositeOnGestureListener implements OnGestureListener {
|
||||
|
||||
private OnGestureListener[] mListeners;
|
||||
|
||||
public CompositeOnGestureListener(OnGestureListener... listeners) {
|
||||
mListeners = listeners;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDown(MotionEvent e) {
|
||||
for (int i = 0; i < mListeners.length; i++) {
|
||||
if (mListeners[i].onDown(e)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShowPress(MotionEvent e) {
|
||||
for (int i = 0; i < mListeners.length; i++) {
|
||||
mListeners[i].onShowPress(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapUp(MotionEvent e) {
|
||||
for (int i = 0; i < mListeners.length; i++) {
|
||||
if (mListeners[i].onSingleTapUp(e)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
|
||||
for (int i = 0; i < mListeners.length; i++) {
|
||||
if (mListeners[i].onScroll(e1, e2, distanceX, distanceY)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongPress(MotionEvent e) {
|
||||
for (int i = 0; i < mListeners.length; i++) {
|
||||
mListeners[i].onLongPress(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
|
||||
for (int i = 0; i < mListeners.length; i++) {
|
||||
if (mListeners[i].onFling(e1, e2, velocityX, velocityY)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.android.documentsui.MultiSelectManager.RecyclerViewHelper;
|
||||
import com.android.documentsui.MultiSelectManager.Selection;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class MultiSelectManagerTest {
|
||||
|
||||
private static final List<String> items;
|
||||
static {
|
||||
items = new ArrayList<String>();
|
||||
items.add("aaa");
|
||||
items.add("bbb");
|
||||
items.add("ccc");
|
||||
items.add("111");
|
||||
items.add("222");
|
||||
items.add("333");
|
||||
}
|
||||
|
||||
private MultiSelectManager mManager;
|
||||
private TestAdapter mAdapter;
|
||||
private TestCallback mCallback;
|
||||
|
||||
private EventHelper mEventHelper;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
mAdapter = new TestAdapter(items);
|
||||
mCallback = new TestCallback();
|
||||
mEventHelper = new EventHelper();
|
||||
mManager = new MultiSelectManager(mAdapter, mEventHelper);
|
||||
mManager.addCallback(mCallback);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singleTapDoesNotSelectBeforeLongPress() {
|
||||
mManager.onSingleTapUp(99);
|
||||
assertSelection();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void longPressStartsSelectionMode() {
|
||||
mManager.onLongPress(7);
|
||||
assertSelection(7);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void secondLongPressExtendsSelection() {
|
||||
mManager.onLongPress(7);
|
||||
mManager.onLongPress(99);
|
||||
assertSelection(7, 99);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singleTapUnselectedLastItem() {
|
||||
mManager.onLongPress(7);
|
||||
mManager.onSingleTapUp(7);
|
||||
assertSelection();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singleTapUpExtendsSelection() {
|
||||
mManager.onLongPress(99);
|
||||
mManager.onSingleTapUp(7);
|
||||
mManager.onSingleTapUp(13);
|
||||
mManager.onSingleTapUp(129899);
|
||||
assertSelection(7, 99, 13, 129899);
|
||||
}
|
||||
|
||||
private void assertSelected(int... expected) {
|
||||
for (int i = 0; i < expected.length; i++) {
|
||||
Selection selection = mManager.getSelection();
|
||||
String err = String.format(
|
||||
"Selection %s does not contain %d", selection, expected[i]);
|
||||
assertTrue(err, selection.contains(expected[i]));
|
||||
}
|
||||
}
|
||||
|
||||
private void assertSelection(int... expected) {
|
||||
assertSelectionSize(expected.length);
|
||||
assertSelected(expected);
|
||||
}
|
||||
|
||||
private void assertSelectionSize(int expected) {
|
||||
Selection selection = mManager.getSelection();
|
||||
assertEquals(expected, selection.size());
|
||||
}
|
||||
|
||||
private static final class EventHelper implements RecyclerViewHelper {
|
||||
@Override
|
||||
public int findEventPosition(MotionEvent e) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class TestCallback implements MultiSelectManager.Callback {
|
||||
|
||||
Set<Integer> ignored = new HashSet<>();
|
||||
private int mLastChangedPosition;
|
||||
private boolean mLastChangedSelected;
|
||||
|
||||
@Override
|
||||
public void onItemStateChanged(int position, boolean selected) {
|
||||
this.mLastChangedPosition = position;
|
||||
this.mLastChangedSelected = selected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBeforeItemStateChange(int position, boolean selected) {
|
||||
return !ignored.contains(position);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class TestHolder extends RecyclerView.ViewHolder {
|
||||
// each data item is just a string in this case
|
||||
public View view;
|
||||
public String string;
|
||||
public TestHolder(View view) {
|
||||
super(view);
|
||||
this.view = view;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class TestAdapter extends RecyclerView.Adapter<TestHolder> {
|
||||
|
||||
private List<String> mItems;
|
||||
|
||||
public TestAdapter(List<String> items) {
|
||||
mItems = items;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TestHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
return new TestHolder(Mockito.mock(ViewGroup.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(TestHolder holder, int position) {}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mItems.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import com.android.documentsui.MultiSelectManager.Selection;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
public class MultiSelectManager_SelectionTest {
|
||||
|
||||
private Selection selection;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
selection = new Selection();
|
||||
selection.add(3);
|
||||
selection.add(5);
|
||||
selection.add(9);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void add() {
|
||||
// We added in setUp.
|
||||
assertEquals(3, selection.size());
|
||||
assertContains(3);
|
||||
assertContains(5);
|
||||
assertContains(9);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void remove() {
|
||||
selection.remove(3);
|
||||
selection.remove(5);
|
||||
assertEquals(1, selection.size());
|
||||
assertContains(9);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clear() {
|
||||
selection.clear();
|
||||
assertEquals(0, selection.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sizeAndGet() {
|
||||
Selection other = new Selection();
|
||||
for (int i = 0; i < selection.size(); i++) {
|
||||
other.add(selection.get(i));
|
||||
}
|
||||
assertEquals(selection.size(), other.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void equalsSelf() {
|
||||
assertEquals(selection, selection);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void equalsOther() {
|
||||
Selection other = new Selection();
|
||||
other.add(3);
|
||||
other.add(5);
|
||||
other.add(9);
|
||||
assertEquals(selection, other);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void expandBefore() {
|
||||
selection.expand(2, 10);
|
||||
assertEquals(3, selection.size());
|
||||
assertContains(13);
|
||||
assertContains(15);
|
||||
assertContains(19);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void expandAfter() {
|
||||
selection.expand(10, 10);
|
||||
assertEquals(3, selection.size());
|
||||
assertContains(3);
|
||||
assertContains(5);
|
||||
assertContains(9);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void expandSplit() {
|
||||
selection.expand(5, 10);
|
||||
assertEquals(3, selection.size());
|
||||
assertContains(3);
|
||||
assertContains(15);
|
||||
assertContains(19);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void expandEncompased() {
|
||||
selection.expand(2, 10);
|
||||
assertEquals(3, selection.size());
|
||||
assertContains(13);
|
||||
assertContains(15);
|
||||
assertContains(19);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void collapseBefore() {
|
||||
selection.collapse(0, 2);
|
||||
assertEquals(3, selection.size());
|
||||
assertContains(1);
|
||||
assertContains(3);
|
||||
assertContains(7);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void collapseAfter() {
|
||||
selection.collapse(10, 10);
|
||||
assertEquals(3, selection.size());
|
||||
assertContains(3);
|
||||
assertContains(5);
|
||||
assertContains(9);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void collapseAcross() {
|
||||
selection.collapse(0, 10);
|
||||
assertEquals(0, selection.size());
|
||||
}
|
||||
|
||||
private void assertContains(int i) {
|
||||
String err = String.format("Selection %s does not contain %d", selection, i);
|
||||
assertTrue(err, selection.contains(i));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user