| /* |
| * 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); |
| } |
| } |