blob: 482f6edf54994af69757894bd730a90234d24aaa [file] [log] [blame]
/*
* 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 com.example.android.common.actionbarcompat;
import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.support.v7.view.ActionMode;
import android.util.Pair;
import android.util.SparseBooleanArray;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AbsListView;
import android.widget.Adapter;
import android.widget.AdapterView;
import android.widget.ListView;
import java.util.HashSet;
/**
* Utilities for handling multiple selection in list views. Contains functionality similar to {@link
* AbsListView#CHOICE_MODE_MULTIPLE_MODAL} which works with {@link ActionBarActivity} and
* backward-compatible action bars.
*/
public class MultiSelectionUtil {
/**
* Attach a Controller to the given <code>listView</code>, <code>activity</code>
* and <code>listener</code>.
*
* @param listView ListView 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 ListView listView,
final ActionBarActivity activity, final MultiChoiceModeListener listener) {
return new Controller(listView, activity, listener);
}
/**
* Class which provides functionality similar to {@link AbsListView#CHOICE_MODE_MULTIPLE_MODAL}
* for the {@link ListView} provided to it. A
* {@link android.widget.AdapterView.OnItemLongClickListener} is set on the ListView so that
* when an item is long-clicked an ActionBarCompat Action Mode is started. Once started, a
* {@link android.widget.AdapterView.OnItemClickListener} is set so that an item click toggles
* that item's checked state.
*/
public static class Controller {
private final ListView mListView;
private final ActionBarActivity 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;
// Reference to the replace OnItemClickListener (so it can be restored later)
private AdapterView.OnItemClickListener mOldItemClickListener;
private final Runnable mSetChoiceModeNoneRunnable = new Runnable() {
@Override
public void run() {
mListView.setChoiceMode(AbsListView.CHOICE_MODE_NONE);
}
};
private Controller(ListView listView, ActionBarActivity activity,
MultiChoiceModeListener listener) {
mListView = listView;
mActivity = activity;
mListener = listener;
mCallbacks = new Callbacks();
// We set ourselves as the OnItemLongClickListener so we know when to start
// an Action Mode
listView.setOnItemLongClickListener(mCallbacks);
}
/**
* 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 ActionBarActivity} 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 ActionBarActivity#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 && mListView.getAdapter().hasStableIds()) {
outState.putLongArray(getStateKey(), mListView.getCheckedItemIds());
}
}
// Internal utility methods
private String getStateKey() {
return MultiSelectionUtil.class.getSimpleName() + "_" + mListView.getId();
}
private void tryRestoreInstanceState(HashSet<Long> idsToCheckOnRestore) {
if (idsToCheckOnRestore == null || mListView.getAdapter() == null) {
return;
}
boolean idsFound = false;
Adapter adapter = mListView.getAdapter();
for (int pos = adapter.getCount() - 1; pos >= 0; pos--) {
if (idsToCheckOnRestore.contains(adapter.getItemId(pos))) {
idsFound = true;
if (mItemsToCheck == null) {
mItemsToCheck = new HashSet<Pair<Integer, Long>>();
}
mItemsToCheck.add(new Pair<Integer, Long>(pos, adapter.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, AdapterView.OnItemClickListener,
AdapterView.OnItemLongClickListener {
@Override
public final boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
if (mListener.onCreateActionMode(actionMode, menu)) {
mActionMode = actionMode;
// Keep a reference to the existing OnItemClickListener so we can restore it
mOldItemClickListener = mListView.getOnItemClickListener();
// Set-up the ListView to emulate CHOICE_MODE_MULTIPLE_MODAL
mListView.setOnItemClickListener(this);
mListView.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE);
mListView.removeCallbacks(mSetChoiceModeNoneRunnable);
// If there are some items to check, do it now
if (mItemsToCheck != null) {
for (Pair<Integer, Long> posAndId : mItemsToCheck) {
mListView.setItemChecked(posAndId.first, true);
// 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
SparseBooleanArray checkedPositions = mListView.getCheckedItemPositions();
if (checkedPositions != null) {
for (int i = 0; i < checkedPositions.size(); i++) {
mListView.setItemChecked(checkedPositions.keyAt(i), false);
}
}
// Restore the original onItemClickListener
mListView.setOnItemClickListener(mOldItemClickListener);
// Clear the Action Mode
mActionMode = null;
// Reset the ListView's Choice Mode
mListView.post(mSetChoiceModeNoneRunnable);
}
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
// Check to see what the new checked state is, and then notify the listener
final boolean checked = mListView.isItemChecked(position);
mListener.onItemCheckedStateChanged(mActionMode, position, id, checked);
boolean hasCheckedItem = checked;
// Check to see if we have any checked items
if (!hasCheckedItem) {
SparseBooleanArray checkedItemPositions = mListView.getCheckedItemPositions();
if (checkedItemPositions != null) {
// Iterate through the SparseBooleanArray to see if there is a checked item
int i = 0;
while (!hasCheckedItem && i < checkedItemPositions.size()) {
hasCheckedItem = checkedItemPositions.valueAt(i++);
}
}
}
// If we don't have any checked items, finish the action mode
if (!hasCheckedItem) {
mActionMode.finish();
}
}
@Override
public boolean onItemLongClick(AdapterView<?> adapterView, View view, int position,
long id) {
// If we already have an action mode started return false
// (onItemClick will be called anyway)
if (mActionMode != null) {
return false;
}
mItemsToCheck = new HashSet<Pair<Integer, Long>>();
mItemsToCheck.add(new Pair<Integer, Long>(position, id));
mActionMode = mActivity.startSupportActionMode(this);
return true;
}
}
}
/**
* @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);
}
}