• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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