/**
* Copyright (C) 2015 str4d
* Copyright (C) 2013 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 i2p.bote.android.util;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.view.ActionMode;
import android.support.v7.widget.RecyclerView;
import android.util.Pair;
import android.util.SparseBooleanArray;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.AbsListView;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
/**
* Utilities for handling multiple selection in list views. Contains functionality similar to {@link
* AbsListView#CHOICE_MODE_MULTIPLE_MODAL} which works with {@link AppCompatActivity} and
* backward-compatible action bars.
*/
public class MultiSelectionUtil {
/**
* Attach a Controller to the given <code>recyclerView</code>, <code>activity</code>
* and <code>listener</code>.
*
* @param recyclerView RecyclerView which displays {@link android.widget.Checkable} items.
* @param activity Activity which contains the ListView.
* @param listener Listener that will manage the selection mode.
* @return the attached Controller instance.
*/
public static Controller attachMultiSelectionController(final RecyclerView recyclerView,
final AppCompatActivity activity, final MultiChoiceModeListener listener) {
if (!(recyclerView.getAdapter() instanceof SelectableAdapter))
throw new IllegalArgumentException("Adapter must extend SelectableAdapter");
return new Controller(recyclerView, activity, listener);
}
public interface Selector {
public boolean inActionMode();
public void selectItem(int position, long id);
}
/**
* Class which provides functionality similar to {@link AbsListView#CHOICE_MODE_MULTIPLE_MODAL}
* for the {@link RecyclerView} provided to it.
*/
public static class Controller implements Selector {
private final RecyclerView mRecyclerView;
private final SelectableAdapter mAdapter;
private final AppCompatActivity mActivity;
private final MultiChoiceModeListener mListener;
private final Callbacks mCallbacks;
// Current Action Mode (if there is one)
private ActionMode mActionMode;
// Keeps record of any items that should be checked on the next action mode creation
private HashSet<Pair<Integer, Long>> mItemsToCheck;
private Controller(RecyclerView recyclerView, AppCompatActivity activity,
MultiChoiceModeListener listener) {
mRecyclerView = recyclerView;
mAdapter = (SelectableAdapter) recyclerView.getAdapter();
mActivity = activity;
mListener = listener;
mCallbacks = new Callbacks();
mAdapter.setSelector(this);
}
@Override
public boolean inActionMode() {
return mActionMode != null;
}
@Override
public void selectItem(int position, long id) {
if (mActionMode == null) {
mItemsToCheck = new HashSet<Pair<Integer, Long>>();
mItemsToCheck.add(new Pair<Integer, Long>(position, id));
mActionMode = mActivity.startSupportActionMode(mCallbacks);
} else {
mAdapter.toggleSelection(position);
// Check to see what the new checked state is, and then notify the listener
final boolean checked = mAdapter.isSelected(position);
mListener.onItemCheckedStateChanged(mActionMode, position, id, checked);
boolean hasCheckedItem = checked;
// Check to see if we have any checked items
if (!hasCheckedItem)
hasCheckedItem = mAdapter.getSelectedItemCount() > 0;
// If we don't have any checked items, finish the action mode
if (!hasCheckedItem)
mActionMode.finish();
}
}
/**
* Finish the current Action Mode (if there is one).
*/
public void finish() {
if (mActionMode != null) {
mActionMode.finish();
}
}
/**
* This method should be called from your {@link AppCompatActivity} or
* {@link android.support.v4.app.Fragment Fragment} to allow the controller to restore any
* instance state.
*
* @param savedInstanceState - The state passed to your Activity or Fragment.
*/
public void restoreInstanceState(Bundle savedInstanceState) {
if (savedInstanceState != null) {
long[] checkedIds = savedInstanceState.getLongArray(getStateKey());
if (checkedIds != null && checkedIds.length > 0) {
HashSet<Long> idsToCheckOnRestore = new HashSet<Long>();
for (long id : checkedIds) {
idsToCheckOnRestore.add(id);
}
tryRestoreInstanceState(idsToCheckOnRestore);
}
}
}
/**
* This method should be called from
* {@link AppCompatActivity#onSaveInstanceState(android.os.Bundle)} or
* {@link android.support.v4.app.Fragment#onSaveInstanceState(android.os.Bundle)
* Fragment.onSaveInstanceState(Bundle)} to allow the controller to save its instance
* state.
*
* @param outState - The state passed to your Activity or Fragment.
*/
public void saveInstanceState(Bundle outState) {
if (mActionMode != null && mAdapter.hasStableIds()) {
List<Integer> selectedItems = mAdapter.getSelectedItems();
long[] selectedItemIds = new long[selectedItems.size()];
for (int i = 0; i < selectedItems.size(); i++) {
selectedItemIds[i] = mAdapter.getItemId(selectedItems.get(i));
}
outState.putLongArray(getStateKey(), selectedItemIds);
}
}
// Internal utility methods
private String getStateKey() {
return MultiSelectionUtil.class.getSimpleName() + "_" + mRecyclerView.getId();
}
private void tryRestoreInstanceState(HashSet<Long> idsToCheckOnRestore) {
if (idsToCheckOnRestore == null) {
return;
}
boolean idsFound = false;
for (int pos = mAdapter.getItemCount() - 1; pos >= 0; pos--) {
if (idsToCheckOnRestore.contains(mAdapter.getItemId(pos))) {
idsFound = true;
if (mItemsToCheck == null) {
mItemsToCheck = new HashSet<Pair<Integer, Long>>();
}
mItemsToCheck.add(new Pair<Integer, Long>(pos, mAdapter.getItemId(pos)));
}
}
if (idsFound) {
// We found some IDs that were checked. Let's now restore the multi-selection
// state.
mActionMode = mActivity.startSupportActionMode(mCallbacks);
}
}
/**
* This class encapsulates all of the callbacks necessary for the controller class.
*/
final class Callbacks implements ActionMode.Callback {
@Override
public final boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
if (mListener.onCreateActionMode(actionMode, menu)) {
mActionMode = actionMode;
// If there are some items to check, do it now
if (mItemsToCheck != null) {
for (Pair<Integer, Long> posAndId : mItemsToCheck) {
mAdapter.toggleSelection(posAndId.first);
// Notify the listener that the item has been checked
mListener.onItemCheckedStateChanged(mActionMode, posAndId.first,
posAndId.second, true);
}
}
return true;
}
return false;
}
@Override
public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
// Proxy listener
return mListener.onPrepareActionMode(actionMode, menu);
}
@Override
public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
// Proxy listener
return mListener.onActionItemClicked(actionMode, menuItem);
}
@Override
public void onDestroyActionMode(ActionMode actionMode) {
mListener.onDestroyActionMode(actionMode);
// Clear all the checked items
mAdapter.clearSelections();
// Clear the Action Mode
mActionMode = null;
}
}
}
/**
* @see android.widget.AbsListView.MultiChoiceModeListener
*/
public static interface MultiChoiceModeListener extends ActionMode.Callback {
/**
* @see android.widget.AbsListView.MultiChoiceModeListener#onItemCheckedStateChanged(
*android.view.ActionMode, int, long, boolean)
*/
public void onItemCheckedStateChanged(ActionMode mode, int position, long id,
boolean checked);
}
public static abstract class SelectableAdapter<VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> {
private Selector mSelector;
private SparseBooleanArray selectedItems;
public SelectableAdapter() {
selectedItems = new SparseBooleanArray();
}
public void setSelector(Selector selector) {
mSelector = selector;
}
public Selector getSelector() {
return mSelector;
}
public void toggleSelection(int position) {
if (selectedItems.get(position, false)) {
selectedItems.delete(position);
} else {
selectedItems.put(position, true);
}
notifyItemChanged(position);
}
public boolean isSelected(int position) {
return selectedItems.get(position, false);
}
public void clearSelections() {
selectedItems.clear();
notifyDataSetChanged();
}
public int getSelectedItemCount() {
return selectedItems.size();
}
public List<Integer> getSelectedItems() {
List<Integer> items =
new ArrayList<Integer>(selectedItems.size());
for (int i = 0; i < selectedItems.size(); i++) {
items.add(selectedItems.keyAt(i));
}
return items;
}
}
}