1 /* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.example.android.common.actionbarcompat; 18 19 import android.os.Bundle; 20 import android.support.v7.app.AppCompatActivity; 21 import android.support.v7.view.ActionMode; 22 import android.util.Pair; 23 import android.util.SparseBooleanArray; 24 import android.view.Menu; 25 import android.view.MenuItem; 26 import android.view.View; 27 import android.widget.AbsListView; 28 import android.widget.Adapter; 29 import android.widget.AdapterView; 30 import android.widget.ListView; 31 32 import java.util.HashSet; 33 34 /** 35 * Utilities for handling multiple selection in list views. Contains functionality similar to {@link 36 * AbsListView#CHOICE_MODE_MULTIPLE_MODAL} which works with {@link AppCompatActivity} and 37 * backward-compatible action bars. 38 */ 39 public class MultiSelectionUtil { 40 41 /** 42 * Attach a Controller to the given <code>listView</code>, <code>activity</code> 43 * and <code>listener</code>. 44 * 45 * @param listView ListView which displays {@link android.widget.Checkable} items. 46 * @param activity Activity which contains the ListView. 47 * @param listener Listener that will manage the selection mode. 48 * @return the attached Controller instance. 49 */ attachMultiSelectionController(final ListView listView, final AppCompatActivity activity, final MultiChoiceModeListener listener)50 public static Controller attachMultiSelectionController(final ListView listView, 51 final AppCompatActivity activity, final MultiChoiceModeListener listener) { 52 return new Controller(listView, activity, listener); 53 } 54 55 /** 56 * Class which provides functionality similar to {@link AbsListView#CHOICE_MODE_MULTIPLE_MODAL} 57 * for the {@link ListView} provided to it. A 58 * {@link android.widget.AdapterView.OnItemLongClickListener} is set on the ListView so that 59 * when an item is long-clicked an ActionBarCompat Action Mode is started. Once started, a 60 * {@link android.widget.AdapterView.OnItemClickListener} is set so that an item click toggles 61 * that item's checked state. 62 */ 63 public static class Controller { 64 65 private final ListView mListView; 66 private final AppCompatActivity mActivity; 67 private final MultiChoiceModeListener mListener; 68 private final Callbacks mCallbacks; 69 70 // Current Action Mode (if there is one) 71 private ActionMode mActionMode; 72 73 // Keeps record of any items that should be checked on the next action mode creation 74 private HashSet<Pair<Integer, Long>> mItemsToCheck; 75 76 // Reference to the replace OnItemClickListener (so it can be restored later) 77 private AdapterView.OnItemClickListener mOldItemClickListener; 78 79 private final Runnable mSetChoiceModeNoneRunnable = new Runnable() { 80 @Override 81 public void run() { 82 mListView.setChoiceMode(AbsListView.CHOICE_MODE_NONE); 83 } 84 }; 85 Controller(ListView listView, AppCompatActivity activity, MultiChoiceModeListener listener)86 private Controller(ListView listView, AppCompatActivity activity, 87 MultiChoiceModeListener listener) { 88 mListView = listView; 89 mActivity = activity; 90 mListener = listener; 91 mCallbacks = new Callbacks(); 92 93 // We set ourselves as the OnItemLongClickListener so we know when to start 94 // an Action Mode 95 listView.setOnItemLongClickListener(mCallbacks); 96 } 97 98 /** 99 * Finish the current Action Mode (if there is one). 100 */ finish()101 public void finish() { 102 if (mActionMode != null) { 103 mActionMode.finish(); 104 } 105 } 106 107 /** 108 * This method should be called from your {@link AppCompatActivity} or 109 * {@link android.support.v4.app.Fragment Fragment} to allow the controller to restore any 110 * instance state. 111 * 112 * @param savedInstanceState - The state passed to your Activity or Fragment. 113 */ restoreInstanceState(Bundle savedInstanceState)114 public void restoreInstanceState(Bundle savedInstanceState) { 115 if (savedInstanceState != null) { 116 long[] checkedIds = savedInstanceState.getLongArray(getStateKey()); 117 if (checkedIds != null && checkedIds.length > 0) { 118 HashSet<Long> idsToCheckOnRestore = new HashSet<Long>(); 119 for (long id : checkedIds) { 120 idsToCheckOnRestore.add(id); 121 } 122 tryRestoreInstanceState(idsToCheckOnRestore); 123 } 124 } 125 } 126 127 /** 128 * This method should be called from 129 * {@link AppCompatActivity#onSaveInstanceState(android.os.Bundle)} or 130 * {@link android.support.v4.app.Fragment#onSaveInstanceState(android.os.Bundle) 131 * Fragment.onSaveInstanceState(Bundle)} to allow the controller to save its instance 132 * state. 133 * 134 * @param outState - The state passed to your Activity or Fragment. 135 */ saveInstanceState(Bundle outState)136 public void saveInstanceState(Bundle outState) { 137 if (mActionMode != null && mListView.getAdapter().hasStableIds()) { 138 outState.putLongArray(getStateKey(), mListView.getCheckedItemIds()); 139 } 140 } 141 142 // Internal utility methods 143 getStateKey()144 private String getStateKey() { 145 return MultiSelectionUtil.class.getSimpleName() + "_" + mListView.getId(); 146 } 147 tryRestoreInstanceState(HashSet<Long> idsToCheckOnRestore)148 private void tryRestoreInstanceState(HashSet<Long> idsToCheckOnRestore) { 149 if (idsToCheckOnRestore == null || mListView.getAdapter() == null) { 150 return; 151 } 152 153 boolean idsFound = false; 154 Adapter adapter = mListView.getAdapter(); 155 for (int pos = adapter.getCount() - 1; pos >= 0; pos--) { 156 if (idsToCheckOnRestore.contains(adapter.getItemId(pos))) { 157 idsFound = true; 158 if (mItemsToCheck == null) { 159 mItemsToCheck = new HashSet<Pair<Integer, Long>>(); 160 } 161 mItemsToCheck.add(new Pair<Integer, Long>(pos, adapter.getItemId(pos))); 162 } 163 } 164 165 if (idsFound) { 166 // We found some IDs that were checked. Let's now restore the multi-selection 167 // state. 168 mActionMode = mActivity.startSupportActionMode(mCallbacks); 169 } 170 } 171 172 /** 173 * This class encapsulates all of the callbacks necessary for the controller class. 174 */ 175 final class Callbacks implements ActionMode.Callback, AdapterView.OnItemClickListener, 176 AdapterView.OnItemLongClickListener { 177 178 @Override onCreateActionMode(ActionMode actionMode, Menu menu)179 public final boolean onCreateActionMode(ActionMode actionMode, Menu menu) { 180 if (mListener.onCreateActionMode(actionMode, menu)) { 181 mActionMode = actionMode; 182 // Keep a reference to the existing OnItemClickListener so we can restore it 183 mOldItemClickListener = mListView.getOnItemClickListener(); 184 185 // Set-up the ListView to emulate CHOICE_MODE_MULTIPLE_MODAL 186 mListView.setOnItemClickListener(this); 187 mListView.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE); 188 mListView.removeCallbacks(mSetChoiceModeNoneRunnable); 189 190 // If there are some items to check, do it now 191 if (mItemsToCheck != null) { 192 for (Pair<Integer, Long> posAndId : mItemsToCheck) { 193 mListView.setItemChecked(posAndId.first, true); 194 // Notify the listener that the item has been checked 195 mListener.onItemCheckedStateChanged(mActionMode, posAndId.first, 196 posAndId.second, true); 197 } 198 } 199 return true; 200 } 201 return false; 202 } 203 204 @Override onPrepareActionMode(ActionMode actionMode, Menu menu)205 public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) { 206 // Proxy listener 207 return mListener.onPrepareActionMode(actionMode, menu); 208 } 209 210 @Override onActionItemClicked(ActionMode actionMode, MenuItem menuItem)211 public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) { 212 // Proxy listener 213 return mListener.onActionItemClicked(actionMode, menuItem); 214 } 215 216 @Override onDestroyActionMode(ActionMode actionMode)217 public void onDestroyActionMode(ActionMode actionMode) { 218 mListener.onDestroyActionMode(actionMode); 219 220 // Clear all the checked items 221 SparseBooleanArray checkedPositions = mListView.getCheckedItemPositions(); 222 if (checkedPositions != null) { 223 for (int i = 0; i < checkedPositions.size(); i++) { 224 mListView.setItemChecked(checkedPositions.keyAt(i), false); 225 } 226 } 227 228 // Restore the original onItemClickListener 229 mListView.setOnItemClickListener(mOldItemClickListener); 230 231 // Clear the Action Mode 232 mActionMode = null; 233 234 // Reset the ListView's Choice Mode 235 mListView.post(mSetChoiceModeNoneRunnable); 236 } 237 238 @Override onItemClick(AdapterView<?> adapterView, View view, int position, long id)239 public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) { 240 // Check to see what the new checked state is, and then notify the listener 241 final boolean checked = mListView.isItemChecked(position); 242 mListener.onItemCheckedStateChanged(mActionMode, position, id, checked); 243 244 boolean hasCheckedItem = checked; 245 246 // Check to see if we have any checked items 247 if (!hasCheckedItem) { 248 SparseBooleanArray checkedItemPositions = mListView.getCheckedItemPositions(); 249 if (checkedItemPositions != null) { 250 // Iterate through the SparseBooleanArray to see if there is a checked item 251 int i = 0; 252 while (!hasCheckedItem && i < checkedItemPositions.size()) { 253 hasCheckedItem = checkedItemPositions.valueAt(i++); 254 } 255 } 256 } 257 258 // If we don't have any checked items, finish the action mode 259 if (!hasCheckedItem) { 260 mActionMode.finish(); 261 } 262 } 263 264 @Override onItemLongClick(AdapterView<?> adapterView, View view, int position, long id)265 public boolean onItemLongClick(AdapterView<?> adapterView, View view, int position, 266 long id) { 267 // If we already have an action mode started return false 268 // (onItemClick will be called anyway) 269 if (mActionMode != null) { 270 return false; 271 } 272 273 mItemsToCheck = new HashSet<Pair<Integer, Long>>(); 274 mItemsToCheck.add(new Pair<Integer, Long>(position, id)); 275 mActionMode = mActivity.startSupportActionMode(this); 276 return true; 277 } 278 } 279 } 280 281 /** 282 * @see android.widget.AbsListView.MultiChoiceModeListener 283 */ 284 public static interface MultiChoiceModeListener extends ActionMode.Callback { 285 286 /** 287 * @see android.widget.AbsListView.MultiChoiceModeListener#onItemCheckedStateChanged( 288 *android.view.ActionMode, int, long, boolean) 289 */ onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked)290 public void onItemCheckedStateChanged(ActionMode mode, int position, long id, 291 boolean checked); 292 } 293 } 294