Use MultiSelectionUtil from Android Samples for CHOICE_MODE_MULTIPLE_MODAL
Fixed: - Background highlighting Broken: - Selecting emails by clicking pictures - Tick appearing over picture of selected emails
This commit is contained in:
3
TODO
3
TODO
@@ -1,5 +1,6 @@
|
||||
Fixes:
|
||||
- Figure out how to highlight email backgrounds when selected in list
|
||||
- Fix clicking pictures to select emails
|
||||
- Fix tick over selected emails
|
||||
- Refine view email page
|
||||
-- Delete/read/unread/move actions that don't break everything
|
||||
- Improve network status page
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
package i2p.bote.android;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Typeface;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.text.DateFormat;
|
||||
@@ -11,31 +21,19 @@ import javax.mail.Part;
|
||||
import i2p.bote.android.util.BoteHelper;
|
||||
import i2p.bote.email.Email;
|
||||
import i2p.bote.fileencryption.PasswordException;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Typeface;
|
||||
import android.util.SparseBooleanArray;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
public class EmailListAdapter extends ArrayAdapter<Email> {
|
||||
private final LayoutInflater mInflater;
|
||||
private SparseBooleanArray mSelectedEmails;
|
||||
private EmailSelector mSelector;
|
||||
private boolean mIsOutbox;
|
||||
|
||||
public interface EmailSelector {
|
||||
public void select(int position);
|
||||
public void select(View view);
|
||||
}
|
||||
|
||||
public EmailListAdapter(Context context, EmailSelector selector, boolean isOutbox) {
|
||||
super(context, android.R.layout.simple_list_item_2);
|
||||
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
mSelectedEmails = new SparseBooleanArray();
|
||||
mSelector = selector;
|
||||
mIsOutbox = isOutbox;
|
||||
}
|
||||
@@ -51,7 +49,7 @@ public class EmailListAdapter extends ArrayAdapter<Email> {
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
View v = mInflater.inflate(R.layout.listitem_email, parent, false);
|
||||
final View v = mInflater.inflate(R.layout.listitem_email, parent, false);
|
||||
final Email email = getItem(position);
|
||||
|
||||
ImageView picture = (ImageView) v.findViewById(R.id.contact_picture);
|
||||
@@ -62,13 +60,14 @@ public class EmailListAdapter extends ArrayAdapter<Email> {
|
||||
|
||||
picture.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View view) {
|
||||
mSelector.select(getPosition(email));
|
||||
mSelector.select(v);
|
||||
}
|
||||
});
|
||||
|
||||
if (mSelectedEmails.get(position)) {
|
||||
((ImageView) v.findViewById(R.id.email_selected)).setVisibility(View.VISIBLE);
|
||||
}
|
||||
// TODO fix
|
||||
//if (mSelectedEmails.get(position)) {
|
||||
// ((ImageView) v.findViewById(R.id.email_selected)).setVisibility(View.VISIBLE);
|
||||
//}
|
||||
|
||||
try {
|
||||
String fromAddress = email.getOneFromAddress();
|
||||
@@ -127,29 +126,4 @@ public class EmailListAdapter extends ArrayAdapter<Email> {
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
public void toggleSelection(int position) {
|
||||
selectView(position, !mSelectedEmails.get(position));
|
||||
}
|
||||
|
||||
public void removeSelection() {
|
||||
mSelectedEmails = new SparseBooleanArray();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void selectView(int position, boolean value) {
|
||||
if (value)
|
||||
mSelectedEmails.put(position, value);
|
||||
else
|
||||
mSelectedEmails.delete(position);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public int getSelectedCount() {
|
||||
return mSelectedEmails.size();
|
||||
}
|
||||
|
||||
public SparseBooleanArray getSelectedIds() {
|
||||
return mSelectedEmails;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,5 @@
|
||||
package i2p.bote.android;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.List;
|
||||
|
||||
import javax.mail.Flags.Flag;
|
||||
import javax.mail.MessagingException;
|
||||
|
||||
import uk.co.senab.actionbarpulltorefresh.extras.actionbarcompat.PullToRefreshLayout;
|
||||
import uk.co.senab.actionbarpulltorefresh.library.ActionBarPullToRefresh;
|
||||
import uk.co.senab.actionbarpulltorefresh.library.Options;
|
||||
import uk.co.senab.actionbarpulltorefresh.library.listeners.OnRefreshListener;
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.util.Log;
|
||||
import i2p.bote.I2PBote;
|
||||
import i2p.bote.android.util.BetterAsyncTaskLoader;
|
||||
import i2p.bote.android.util.BoteHelper;
|
||||
import i2p.bote.android.util.MoveToDialogFragment;
|
||||
import i2p.bote.email.Email;
|
||||
import i2p.bote.fileencryption.PasswordException;
|
||||
import i2p.bote.folder.EmailFolder;
|
||||
import i2p.bote.folder.FolderListener;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.ProgressDialog;
|
||||
@@ -28,6 +7,7 @@ import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.support.v4.app.ListFragment;
|
||||
@@ -43,11 +23,34 @@ import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.List;
|
||||
|
||||
import javax.mail.Flags.Flag;
|
||||
import javax.mail.MessagingException;
|
||||
|
||||
import i2p.bote.I2PBote;
|
||||
import i2p.bote.android.util.BetterAsyncTaskLoader;
|
||||
import i2p.bote.android.util.BoteHelper;
|
||||
import i2p.bote.android.util.MoveToDialogFragment;
|
||||
import i2p.bote.android.util.MultiSelectionUtil;
|
||||
import i2p.bote.email.Email;
|
||||
import i2p.bote.fileencryption.PasswordException;
|
||||
import i2p.bote.folder.EmailFolder;
|
||||
import i2p.bote.folder.FolderListener;
|
||||
import uk.co.senab.actionbarpulltorefresh.extras.actionbarcompat.PullToRefreshLayout;
|
||||
import uk.co.senab.actionbarpulltorefresh.library.ActionBarPullToRefresh;
|
||||
import uk.co.senab.actionbarpulltorefresh.library.Options;
|
||||
import uk.co.senab.actionbarpulltorefresh.library.listeners.OnRefreshListener;
|
||||
|
||||
public class EmailListFragment extends ListFragment implements
|
||||
LoaderManager.LoaderCallbacks<List<Email>>,
|
||||
MoveToDialogFragment.MoveToDialogListener,
|
||||
@@ -63,7 +66,10 @@ public class EmailListFragment extends ListFragment implements
|
||||
|
||||
private EmailListAdapter mAdapter;
|
||||
private EmailFolder mFolder;
|
||||
private ActionMode mMode;
|
||||
|
||||
// The Controller which provides CHOICE_MODE_MULTIPLE_MODAL-like functionality
|
||||
private MultiSelectionUtil.Controller mMultiSelectController;
|
||||
private ModalChoiceListener mModalChoiceListener;
|
||||
|
||||
private EditText mPasswordInput;
|
||||
private TextView mPasswordError;
|
||||
@@ -103,7 +109,7 @@ public class EmailListFragment extends ListFragment implements
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view,savedInstanceState);
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
String folderName = getArguments().getString(FOLDER_NAME);
|
||||
mFolder = BoteHelper.getMailFolder(folderName);
|
||||
|
||||
@@ -138,6 +144,7 @@ public class EmailListFragment extends ListFragment implements
|
||||
mNumIncompleteEmails = new TextView(getActivity());
|
||||
mNumIncompleteEmails.setText(getResources().getString(R.string.incomplete_emails,
|
||||
numIncompleteEmails));
|
||||
mNumIncompleteEmails.setPadding(16, 5, 16, 5);
|
||||
getListView().addHeaderView(mNumIncompleteEmails);
|
||||
}
|
||||
}
|
||||
@@ -151,16 +158,15 @@ public class EmailListFragment extends ListFragment implements
|
||||
|
||||
setListAdapter(mAdapter);
|
||||
|
||||
// Set up CAB
|
||||
mMode = null;
|
||||
getListView().setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
|
||||
@Override
|
||||
public boolean onItemLongClick(AdapterView<?> parent, View view,
|
||||
int position, long id) {
|
||||
onListItemSelect(position);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
// Attach a MultiSelectionUtil.Controller to the ListView, giving it an instance of
|
||||
// ModalChoiceListener (see below)
|
||||
mModalChoiceListener = new ModalChoiceListener();
|
||||
mMultiSelectController = MultiSelectionUtil
|
||||
.attachMultiSelectionController(getListView(), (ActionBarActivity) getActivity(),
|
||||
mModalChoiceListener);
|
||||
|
||||
// Allow the Controller to restore itself
|
||||
mMultiSelectController.restoreInstanceState(savedInstanceState);
|
||||
|
||||
if (mFolder == null) {
|
||||
setEmptyText(getResources().getString(
|
||||
@@ -267,6 +273,16 @@ public class EmailListFragment extends ListFragment implements
|
||||
getLoaderManager().initLoader(EMAIL_LIST_LOADER, null, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
|
||||
// Allow the Controller to save it's instance state so that any checked items are
|
||||
// stored
|
||||
if (mMultiSelectController != null)
|
||||
mMultiSelectController.saveInstanceState(outState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.email_list, menu);
|
||||
@@ -275,94 +291,97 @@ public class EmailListFragment extends ListFragment implements
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_new_email:
|
||||
Intent nei = new Intent(getActivity(), NewEmailActivity.class);
|
||||
startActivity(nei);
|
||||
return true;
|
||||
case R.id.action_new_email:
|
||||
Intent nei = new Intent(getActivity(), NewEmailActivity.class);
|
||||
startActivity(nei);
|
||||
return true;
|
||||
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onListItemClick(ListView parent, View view, int pos, long id) {
|
||||
super.onListItemClick(parent, view, pos, id);
|
||||
int position = mNumIncompleteEmails == null ? pos : pos - 1;
|
||||
if (position < 0) return;
|
||||
if (mMode == null) {
|
||||
mCallback.onEmailSelected(
|
||||
mFolder.getName(), mAdapter.getItem(position).getMessageID());
|
||||
} else
|
||||
onListItemSelect(position);
|
||||
final Email email = (Email) getListView().getItemAtPosition(pos);
|
||||
if (email != null)
|
||||
mCallback.onEmailSelected(mFolder.getName(), email.getMessageID());
|
||||
}
|
||||
|
||||
private void onListItemSelect(int position) {
|
||||
mAdapter.toggleSelection(position);
|
||||
boolean hasCheckedElement = mAdapter.getSelectedCount() > 0;
|
||||
|
||||
if (hasCheckedElement && mMode == null) {
|
||||
boolean unread = mAdapter.getItem(position).isUnread();
|
||||
mMode = ((ActionBarActivity) getActivity()).startSupportActionMode(new ModeCallback(unread));
|
||||
} else if (!hasCheckedElement && mMode != null) {
|
||||
mMode.finish();
|
||||
}
|
||||
|
||||
if (mMode != null)
|
||||
mMode.setTitle(getResources().getString(
|
||||
R.string.items_selected, mAdapter.getSelectedCount()));
|
||||
}
|
||||
|
||||
private final class ModeCallback implements ActionMode.Callback {
|
||||
private class ModalChoiceListener implements MultiSelectionUtil.MultiChoiceModeListener {
|
||||
private boolean areUnread;
|
||||
|
||||
public ModeCallback(boolean unread) {
|
||||
super();
|
||||
this.areUnread = unread;
|
||||
@Override
|
||||
public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
|
||||
final ListView listView = getListView();
|
||||
int numChecked = 0;
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
|
||||
final SparseBooleanArray items = listView.getCheckedItemPositions();
|
||||
for (int i = 0; i < items.size(); i++)
|
||||
if (items.valueAt(i))
|
||||
numChecked++;
|
||||
} else
|
||||
numChecked = listView.getCheckedItemCount();
|
||||
|
||||
mode.setTitle(getResources().getString(R.string.items_selected, numChecked));
|
||||
|
||||
if (checked && numChecked == 1) { // This is the first checked item
|
||||
Email email = (Email) listView.getItemAtPosition(position);
|
||||
areUnread = email.isUnread();
|
||||
mode.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
final ListView listView = getListView();
|
||||
// Respond to clicks on the actions in the CAB
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_delete:
|
||||
SparseBooleanArray toDelete = mAdapter.getSelectedIds();
|
||||
for (int i = (toDelete.size() - 1); i >= 0; i--) {
|
||||
if (toDelete.valueAt(i)) {
|
||||
Email email = mAdapter.getItem(toDelete.keyAt(i));
|
||||
// The Loader will update mAdapter
|
||||
I2PBote.getInstance().deleteEmail(mFolder, email.getMessageID());
|
||||
}
|
||||
}
|
||||
mode.finish();
|
||||
return true;
|
||||
case R.id.action_mark_read:
|
||||
case R.id.action_mark_unread:
|
||||
SparseBooleanArray selected = mAdapter.getSelectedIds();
|
||||
for (int i = (selected.size() - 1); i >= 0; i--) {
|
||||
if (selected.valueAt(i)) {
|
||||
Email email = mAdapter.getItem(selected.keyAt(i));
|
||||
try {
|
||||
case R.id.action_delete:
|
||||
SparseBooleanArray toDelete = listView.getCheckedItemPositions();
|
||||
if (toDelete.size() == 0)
|
||||
return false;
|
||||
|
||||
for (int i = (toDelete.size() - 1); i >= 0; i--) {
|
||||
if (toDelete.valueAt(i)) {
|
||||
Email email = (Email) listView.getItemAtPosition(toDelete.keyAt(i));
|
||||
// The Loader will update mAdapter
|
||||
mFolder.setNew(email, !areUnread);
|
||||
} catch (PasswordException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
} catch (GeneralSecurityException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
I2PBote.getInstance().deleteEmail(mFolder, email.getMessageID());
|
||||
}
|
||||
}
|
||||
}
|
||||
areUnread = !areUnread;
|
||||
mMode.invalidate();
|
||||
return true;
|
||||
case R.id.action_move_to:
|
||||
DialogFragment f = MoveToDialogFragment.newInstance(mFolder);
|
||||
f.show(getFragmentManager(), "moveTo");
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
mode.finish();
|
||||
return true;
|
||||
|
||||
case R.id.action_mark_read:
|
||||
case R.id.action_mark_unread:
|
||||
SparseBooleanArray selected = listView.getCheckedItemPositions();
|
||||
for (int i = (selected.size() - 1); i >= 0; i--) {
|
||||
if (selected.valueAt(i)) {
|
||||
Email email = (Email) listView.getItemAtPosition(selected.keyAt(i));
|
||||
try {
|
||||
// The Loader will update mAdapter
|
||||
mFolder.setNew(email, !areUnread);
|
||||
} catch (PasswordException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
} catch (GeneralSecurityException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
areUnread = !areUnread;
|
||||
mode.invalidate();
|
||||
return true;
|
||||
|
||||
case R.id.action_move_to:
|
||||
DialogFragment f = MoveToDialogFragment.newInstance(mFolder);
|
||||
f.show(getFragmentManager(), "moveTo");
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,16 +401,6 @@ public class EmailListFragment extends ListFragment implements
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
// Here you can make any necessary updates to the activity when
|
||||
// the CAB is removed.
|
||||
mAdapter.removeSelection();
|
||||
|
||||
if (mode == mMode)
|
||||
mMode = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
// Here you can perform updates to the CAB due to
|
||||
@@ -402,19 +411,24 @@ public class EmailListFragment extends ListFragment implements
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
}
|
||||
}
|
||||
|
||||
// Called by EmailListActivity.onFolderSelected()
|
||||
|
||||
public void onFolderSelected(EmailFolder newFolder) {
|
||||
SparseBooleanArray toMove = mAdapter.getSelectedIds();
|
||||
final ListView listView = getListView();
|
||||
SparseBooleanArray toMove = listView.getCheckedItemPositions();
|
||||
for (int i = (toMove.size() - 1); i >= 0; i--) {
|
||||
if (toMove.valueAt(i)) {
|
||||
Email email = mAdapter.getItem(toMove.keyAt(i));
|
||||
Email email = (Email) listView.getItemAtPosition(toMove.keyAt(i));
|
||||
mFolder.move(email, newFolder);
|
||||
}
|
||||
}
|
||||
mMode.finish();
|
||||
mMultiSelectController.finish();
|
||||
}
|
||||
|
||||
// LoaderManager.LoaderCallbacks<List<Email>>
|
||||
@@ -473,7 +487,7 @@ public class EmailListFragment extends ListFragment implements
|
||||
}
|
||||
|
||||
public void onLoadFinished(Loader<List<Email>> loader,
|
||||
List<Email> data) {
|
||||
List<Email> data) {
|
||||
// Clear recent flags
|
||||
for (Email email : data)
|
||||
try {
|
||||
@@ -508,8 +522,12 @@ public class EmailListFragment extends ListFragment implements
|
||||
|
||||
// EmailListAdapter.EmailSelector
|
||||
|
||||
public void select(int position) {
|
||||
onListItemSelect(position);
|
||||
public void select(View view) {
|
||||
// TODO temporarily disabled while broken, need to fix
|
||||
//final ListView listView = getListView();
|
||||
//final int position = listView.getPositionForView(view);
|
||||
//listView.setItemChecked(position, !listView.isItemChecked(position));
|
||||
//view.performLongClick();
|
||||
}
|
||||
|
||||
// OnRefreshListener
|
||||
@@ -527,7 +545,8 @@ public class EmailListFragment extends ListFragment implements
|
||||
while (I2PBote.getInstance().isCheckingForMail()) {
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {}
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
293
app/src/main/java/i2p/bote/android/util/MultiSelectionUtil.java
Normal file
293
app/src/main/java/i2p/bote/android/util/MultiSelectionUtil.java
Normal file
@@ -0,0 +1,293 @@
|
||||
/*
|
||||
* 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.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);
|
||||
}
|
||||
}
|
||||
7
app/src/main/res/drawable/listitem_checked.xml
Normal file
7
app/src/main/res/drawable/listitem_checked.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@color/translucent_blue" android:state_activated="true" />
|
||||
<item android:drawable="@color/translucent_blue" android:state_checked="true" />
|
||||
<item android:drawable="@android:color/transparent" />
|
||||
</selector>
|
||||
@@ -2,6 +2,7 @@
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/listitem_checked"
|
||||
android:padding="5dp" >
|
||||
|
||||
<ImageView
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<!-- A translucent holo blue -->
|
||||
<color name="translucent_blue">#9033B5E5</color>
|
||||
<color name="uva_color">#c31756</color>
|
||||
<color name="default_color">#111</color>
|
||||
<color name="error_color">#f00</color>
|
||||
|
||||
Reference in New Issue
Block a user