I think the easier way to describe my bug is by showing it in a video. But the general idea is that I use a database to keep track of which items in a list of checkboxes is checked off. Whenever I click on an item and activate that list's action mode, it immediately forget those changes, in spite of them having already made their way to the database. What makes this worse is when multiple items become afflicted with this bug, they end up forming a strange bit of circular logic, wherein one item in the database will become checked and the other will become unchecked, and the two will continue this in a cycle as actions are done on the list due to the checkboxes constantly cycling between a checked state and an unchecked state and thus triggering the checkbox's onCheckedChangedListener. There is no code in onListItemClick() that should be causing these changes to happen on either a list level or database level, and no code in onPrepareActionMode or onCreateActionMode that would reload the CursorLoader or manipulate the selected item. Can anyone help me figure out what's going on to cause this? (My implementation can be found in the video, but I also included it below.)
public class TaskCursorAdapter extends ResourceCursorAdapter{
public View newView(Context context, Cursor cursor, ViewGroup parent){
final LayoutInflater inflater = LayoutInflater.from(context);
final long assignedID = cursor.getInt(cursor.getColumnIndex(NagTasksDatabaseHelper.ID));
View v = inflater.inflate(layout, parent, false);
CheckBox taskCheckBox = (CheckBox)v.findViewById(R.id.taskCheckBox);
TextView taskText = (TextView)v.findViewById(R.id.taskTitle);
TextView taskNote = (TextView)v.findViewById(R.id.taskNote);
if (cursor.getInt(cursor.getColumnIndex(NagTasksDatabaseHelper.CHECKED))==1){
taskText.setPaintFlags(taskText.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
taskNote.setPaintFlags(taskNote.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
}
taskCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
//Current method:
AlertDialog.Builder builder = new AlertDialog.Builder(buttonView.getContext());
builder.setCancelable(true);
Log.i("TaskCursorAdapter", "CheckBox assignedID="+assignedID);
if (assignedID!=-1) {
NagTasksDatabaseHelper helper = new NagTasksDatabaseHelper(buttonView.getContext());
if (isChecked) {
helper.checkOffTask(assignedID);
//builder.setMessage("Item "+assignedID+" checked");
} else {
helper.uncheckTask(assignedID);
//builder.setMessage("Item "+assignedID+" unchecked");
}
helper.close();
} else {
builder.setMessage("No ID found. Try something else.");
builder.create().show();
}
}
});
return v;
}
public void bindView(View v, Context context, Cursor cursor){
CheckBox taskCheckBox = (CheckBox)v.findViewById(R.id.taskCheckBox);
TextView taskText = (TextView)v.findViewById(R.id.taskTitle);
TextView taskNote = (TextView)v.findViewById(R.id.taskNote);
taskCheckBox.setChecked(cursor.getInt(cursor.getColumnIndex(NagTasksDatabaseHelper.CHECKED))==1);
//final long assignedID = v.getId();
//v.setClickable(true);
//LinearLayout holder = (LinearLayout)v.findViewById(R.id.container);
//holder.setClickable(true);
//IdFromDb = c.getInt(c.getColumnIndex(NagTasksDatabaseHelper.ID));
taskText.setText(cursor.getString(cursor.getColumnIndex(NagTasksDatabaseHelper.TASK)));
taskNote.setText(cursor.getString(cursor.getColumnIndex(NagTasksDatabaseHelper.NOTE)));
//isCheckedFromDb = c.getInt(c.getColumnIndex(NagTasksDatabaseHelper.CHECKED));
//cursor = c;
}
public class TaskListFragment extends SherlockListFragment implements ActionMode.Callback {
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(R.menu.context_menu_single_choice, menu);
mode.setTitle(R.string.activeActionMode);
return true;
}
#Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
public void onListItemClick(ListView l, View v, int position, long id){
//super.onListItemClick(l, v, position, id);
Log.i("NagTaskListFragment", "User clicked on "+id);
if (mActionMode == null) {
idForActionMode=id;
mActionMode = getSherlockActivity().startActionMode(this);
}
if (l.getSelectedView() == v) {
v.setSelected(false);
mActionMode.finish();
mActionMode = null;
idForActionMode = -1;
} else if (l.getSelectedView() != null) {
l.getSelectedView().setSelected(false);
v.setSelected(true);
idForActionMode = id;
//onPrepareActionMode(mActionMode, mActionMode.getMenu());
//Log.i("NagTaskListFragment", "selectedItemID = "+l.getSelectedItemId());
} else {
v.setSelected(true);
idForActionMode = id;
//Log.i("NagTaskListFragment", "selectedItemID = "+l.getSelectedItemId());
}
}
So, I moved the setChecked() method to newView and that seemed to have fixed the issue. I'm not sure if that's the correct fix, but it works for now.
Related
I have listview with custom adapter. Each row of item has button that activates popup menu with. When user click on one of the items it should display some data.
Here is the item holder class:
public class cNalog {
public String IDNalog;
public String NazivKlijenta;
public String OpisNaloga;
public String Napomena;
public int Hitnost;
public cNalog(String IDNalog, String nazivKlijenta, String opisNaloga, int hitnost) {
this.IDNalog = IDNalog;
NazivKlijenta = nazivKlijenta;
OpisNaloga = opisNaloga;
Hitnost = hitnost;
}
public cNalog() {}
public String getIDNalog() {
return IDNalog;
}
public void setIDNalog(String IDNalog) {
this.IDNalog = IDNalog;
}
public String getNazivKlijenta() {
return NazivKlijenta;
}
public void setNazivKlijenta(String nazivKlijenta) {
NazivKlijenta = nazivKlijenta;
}
public String getOpisNaloga() {
return OpisNaloga;
}
public void setOpisNaloga(String opisNaloga) {
OpisNaloga = opisNaloga;
}
public String getNapomena() {
return Napomena;
}
public void setNapomena(String napomena) {
Napomena = napomena;
}
public int getHitnost() {
return Hitnost;
}
public void setHitnost(int hitnost) {
Hitnost = hitnost;
}
}
And here is the getView method from CustomAdapter that extend BaseAdapter class:
#Override
public View getView(final int i, View view, ViewGroup viewGroup) {
final ViewHolder holder;
if (view == null)
{
holder = new ViewHolder();
LayoutInflater mInflater = (LayoutInflater) mContext.getSystemService(mContext.LAYOUT_INFLATER_SERVICE);
view = mInflater.inflate(R.layout.popisnaloga_red, null);
holder.btnPopUpMenu = (Button) view.findViewById(R.id.btnPopUpNalog);
holder.btnPopUpMenu.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
PopupMenu popup = new PopupMenu(mContext, view);
popup.getMenuInflater().inflate(R.menu.popup_nalog, popup.getMenu());
//holder.uidNalog = mData.get(_i).getIDNalog();
popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
#Override
public boolean onMenuItemClick(MenuItem menuItem) {
Toast.makeText(mContext,
"Your Message", Toast.LENGTH_LONG).show();
switch (menuItem.getItemId()) {
case R.id.mnuActionInfo:
Log.i("Selekcija", mData.get(i).getIDNalog()); //Info(mData.get(i).getOpisNaloga());
default:
return false;
}
}
});
popup.show();
}
});
}
TextView tvOpisRada = (TextView)view.findViewById(R.id.viewNazivNaloga);
tvOpisRada.setText(mData.get(i).getOpisNaloga());
return view;
}
private class ViewHolder {
protected Button btnPopUpMenu;
}
When user select some item it should print out UID. This works fine when I have 3-4 items... but if I scrool down and let's say he select 12th item he get the same UID as the one of the first fours item. It seems that when I scrool down the listview acts like there is always 4 items in list not 12 or more... How to solve this?
you did wrong when use ViewHolder. Your code only check if view = null then inflate row for that position. So you can not store view's value.
You need to store row as a tag of viewholder, when check when view != null and get that view.
You can check that link to know how Viewholder work in listview: Implements ViewHolder on a ListView AndroidStudio
I know someone suggest you to use RecyclerView, but I want to suggest you to know about viewHolder/listview first. The most dangerous thing is just copy code but dont know how it work.
I think the best way for what you want is to use a recyclerview rather than a listview
For any help take a look here
I was surprised that I couldn't find an existing answer on Stack that I could use for this, so here I am.
I have a ListFragment with a list attached to a SimpleCursorAdapter comprised of the rows defined by the following row.xml file:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="6dip" >
<CheckBox
android:id="#+id/story_check_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:focusable="false"
android:focusableInTouchMode="false" />
<TextView
android:id="#+id/story"
android:layout_width="wrap_content"
android:layout_height="24sp"
android:lines="1"
android:scrollHorizontally="true"
android:singleLine="true"
android:layout_alignBaseline="#+id/story_check_box"
android:layout_alignBottom="#+id/story_check_box"
android:layout_toRightOf="#+id/story_check_box" />
</RelativeLayout>
I connect the list with the adapter with the following code in my ListFragment:
adapter = new SimpleCursorAdapter(getActivity(), R.layout.row, null, new String[] { CProvider.Stories.TITLE }, new int[] { R.id.story }, 0);
setListAdapter(adapter);
I then try to use a CheckBox in my fragment to toggle all the list checkboxes as follows:
CheckBox selectAll = (CheckBox) rootView.findViewById(R.id.select_check_box);
selectAll.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
final ListView listView = getListView();
for(int i=0; i < getListAdapter().getCount(); i++){
View view = getViewByPosition(i, listView);
CheckBox cb = (CheckBox)view.findViewById(R.id.story_check_box);
if (isChecked) {
cb.setChecked(true);
}
else {
cb.setChecked(false);
}
}
}
});
I got getViewByPositionfrom here: Get ListView children that are not in view, and that almost works, but a few of the checkboxes don't get checked (and there is a pattern to it, but I can't seem to figure it out). It also seems a bit kludgier than I would think is necessary.
I want the checkboxes on the left, so I don't want to use checkedtextviews. Maybe I need to extend CursorAdapter and override getView?
Thanks in advance.
Maybe I'm not correctly understanding your question but what I understood was that you wanted to check and uncheck all the checkboxes thanks to one "Select All checkbox".
Then, what I would do is to put the state of the "select all checkbox" as a variable of the class (as a boolean) which is overwritten by your selectAll.setOnCheckedChangeListener and say to the adapter "Hey, my state changed!" every time the checkbox changed its state.
Something like this:
class Dummy{
boolean isAllSelected = false;
Checkbox selectAll = (find or create your CheckBox)
selectAll.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) isAllSelected = true;
else isAllSelected = false;
listView.getAdapter().notifyDataSetChanged();
}
}
And then, you just have to override the getView() of this adapter (like you suggested) adding a "if (isAllSlected)" condition.
To me, it sounds the easiest to do but it's maybe not that good to call the notifyDataSetChanged() method every time the user clicks on a checkbox (it's not that efficient for so minor changes). Anyway, hope it helps (the code I wrote is maybe not with the correct syntax: I wrote it directly on the website form)!
Below is what I wound up doing. In addition to taking care of the "select all/ unselect all" functionality, it handles checking/unchecking a checkbox when the text of a list item is selected/unselected, and vice versa. I was concerned about getView being called frequently, but setItemChecked causes getView to be called no matter what, so there's a limit to how much calls to getView can be avoided. As ataulm mentioned in a comment, maybe a composite view would a solution with less fuss.
In onCreateView:
selectAllCheckBox = (CheckBox) rootView.findViewById(R.id.select_all_check_box);
selectAllCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
final ListView listView = getListView();
for(int i=0; i < getListAdapter().getCount(); i++){
listView.setItemChecked(i, isChecked);
}
}
});
I also created a custom SimpleCursorAdapter with the following code, which also uses a simple ViewHolder class. In getView I check which items in the list are selected and check the checkboxes corresponding to those items. There's also code that sets a list item as selected or not if its corresponding checkbox has been clicked (i.e., checked or unchecked).
class AvailableCursorAdapter extends SimpleCursorAdapter {
AvailableCursorAdapter(Context context, int layout, Cursor c, String[] from, int[] to, int flags) {
super(context, layout, c, from, to, flags);
}
#Override
public View getView(final int position, View convertView, ViewGroup parent) {
View row = super.getView(position, convertView, parent);
ViewHolder holder = (ViewHolder)row.getTag();
if (holder == null) {
holder = new ViewHolder(row);
row.setTag(holder);
}
holder.storyCheckBox.setChecked(false);
holder.story.setTextColor(Color.LTGRAY);
long [] checkedIds = getListView().getCheckedItemIds();
if (checkedIds != null) {
for (int i = 0; i < checkedIds.length; i++) {
if (checkedIds[i] == getListAdapter().getItemId(position)) {
holder.storyCheckBox.setChecked(true);
holder.story.setTextColor(Color.WHITE);
break;
}
}
}
final boolean isChecked = holder.storyCheckBox.isChecked();
holder.storyCheckBox.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
getListView().setItemChecked(position, !isChecked);
}
});
return(row);
}
}
.
class ViewHolder {
CheckBox storyCheckBox;
TextView story = null;
ViewHolder(final View row) {
storyCheckBox = (CheckBox) row.findViewById(R.id.story_check_box);
story = (TextView) row.findViewById(R.id.story);
}
}
Finally, the following code causes getView to be called when a single ListItem is clicked, so that its corresponding checkbox gets selected or unselected, as appropriate:
#Override
public void onListItemClick(ListView l, View v, int position, long id) {
ViewHolder holder = (ViewHolder) v.getTag();
holder.storyCheckBox.setChecked(false);
holder.story.setTextColor(Color.LTGRAY);
long [] checkedIds = l.getCheckedItemIds();
if (checkedIds != null) {
for (int i = 0; i < checkedIds.length; i++) {
if (checkedIds[i] == getListAdapter().getItemId(position)) {
holder.storyCheckBox.setChecked(true);
holder.story.setTextColor(Color.WHITE);
break;
}
}
}
}
I am having trouble figuring this one out.
I have a ListView with CursorAdapter and if item goes offscreen the switch resets it self. I have a listener onCheckedChangeListener set on the switch, which triggers the listener implemented by hosting fragment that updates a field in database table and requeries the whole thing (contentResolver.notifyChange()).
I know this is kinda usual issue with check boxes, but I still cant get it working.
public class FilterCursorAdapter extends CursorAdapter {
public interface OnSwitchToggledListener {
public void onSwitchToggled(int id, boolean isActivated);
}
private OnSwitchToggledListener listener;
#Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
View view = layoutInflater.inflate(R.layout.filter_item, parent, false);
ViewHolder holder = new ViewHolder();
holder.keywordTextView = (TextView) view.findViewById(R.id.keyword_text_view);
holder.cathegoryTextView = (TextView) view.findViewById(R.id.cathegory_text_view);
holder.activateSwitch = (Switch) view.findViewById(R.id.switch_activate);
view.setTag(holder);
return view;
}
public void bindView(View row, final Context context, final Cursor cursor) {
final ViewHolder holder = (ViewHolder) row.getTag();
holder.keywordTextView.setText(cursor.getString(cursor.getColumnIndex(ArticleProvider.COLUMN_KEYOWRD)));
final int id = cursor.getInt(cursor.getColumnIndex(ArticleProvider.COLUMN_FILTER_ID));
String cathegory = cursor.getString(cursor.getColumnIndex(ArticleProvider.COLUMN_CATHEGORY));
int isActivated = cursor.getInt(cursor.getColumnIndex(ArticleProvider.COLUMN_ACTIVATED));
holder.cathegoryTextView.setText(string);
holder.activateSwitch.setChecked(isActivated == 1);
holder.activateSwitch.setOnCheckedChangeListener(new OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
listener.onSwitchToggled(id, isChecked);
}
});
}
private class ViewHolder {
public TextView keywordTextView;
public TextView cathegoryTextView;
public Switch activateSwitch;
}
}
public class FilterListFragment extends SherlockListFragment implements LoaderCallbacks<Cursor>, OnSwitchToggledListener {
#Override
public void onSwitchToggled(int id, boolean isActivated) {
Log.d(MainActivity.TAG, "ID: " + id + " isActivated: " + isActivated);
ContentValues values = new ContentValues();
values.put(ArticleProvider.COLUMN_ACTIVATED, isActivated);
resolver.update(ArticleProvider.FILTERS_URI, values, ArticleProvider.COLUMN_FILTER_ID + "=" + id, null);
}
}
From the logs it seems that the setOnCheckedChangeListener listener inside bindView gets triggered on it self.
How do I fix this one? Also I would like to note that I am using SwitchCompatLibrary (https://github.com/ankri/SwitchCompatLibrary)
Thanks!
If you face this problem, you need to add remove the listener before setting it again
holder.activateSwitch.setOnCheckedChangeListener(null)
I was having the same problem with this for about three hours today. As soon as it moved off screen, it changed. So, I ended up abandoning theonCheckedChanged listener and instead used:
mySwitch.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Log.i(TAG, "isChecked:");
}
});
Now it works perfect.
I have a list view which has been registered for a context menu. For some items within the list a context menu is not applicable. In these cases I just don't inflate a menu in the onCreateContextMenu method.
Unfortunately this means that when items that don't display a context menu are long-clicked Android then handles this as a short-click (presumably because the context menu would normally return true to say that the long-click event has been handled).
This results in an inconsistent behaviour in the listview - some items show a context menu when you long click them - others don't and then perform the default click behaviour. How can I ensure that even items that don't display a context menu consume the long click so that the onItemClick method isn't called?
#Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
Playable playable = (Playable) info.targetView.getTag(R.id.playable);
if (playable != null && !(playable instanceof AutoRadioStation) && !(playable.getId().equals(Playlist.AUTOMATIC_PLAYLIST))) {
v.setTag(R.id.playable, playable); // This copies the tag so that it is contained within the view used for the menu.
Drawable stationImage = (Drawable) ((ImageView) info.targetView.findViewById(R.id.artwork)).getDrawable().getConstantState().newDrawable();
menu.setHeaderTitle(playable.getName());
menu.setHeaderIcon(stationImage);
MenuInflater inflater = getActivity().getMenuInflater();
inflater.inflate(R.menu.saved_context_menu, menu);
}
}
I had a similar issue and I ended up using a Dialog instead of a context menu.
My activity implements OnItemLongClickListener, and I show it from onLongItemClick() if the condition is satisfied.
I finally got around to implementing a version of NathanZ solution. There didn't seem to be much stuff out there about turning a contextMenu into a DialogFragment so I'll paste most of my solution here.
Implementing an onLongItemClick listener also meant that I was able to have a long-click event that didn't require a menu within the listview. Unfortunately because you can't pass menus around to a dialog I had to reuse an existing ListViewElement type to store an id and a text string for each "menu" item in my listview.
#Override
public boolean onItemLongClick(AdapterView<?> parent, View view, int item, long position) {
Playable playable = (Playable) view.getTag(R.id.playable);
//Switch haptic feedback off by default so if we don't handle the long click we don't vibrate
parent.setHapticFeedbackEnabled(false);
if (playable == null) {
// This must be a message bar so the only option is to update all saved content
updateAll();
parent.setHapticFeedbackEnabled(true);
} else {
if (!(playable instanceof AutoRadioStation) && !(playable.getId().equals(Playlist.AUTOMATIC_PLAYLIST))) {
Drawable drawable = (Drawable) ((ImageView) view.findViewById(R.id.artwork)).getDrawable().getConstantState().newDrawable();
showContextDialog(playable, drawable);
parent.setHapticFeedbackEnabled(true);
}
}
return true;
}
private void showContextDialog(Playable playable, Drawable drawable) {
FragmentManager fm = getActivity().getSupportFragmentManager();
final List<ListViewElement> array = new ArrayList<ListViewElement>();
array.add(new ListViewElement(R.id.menu_share, null, getString(R.string.share), true));
array.add(new ListViewElement(R.id.menu_delete, null, getString(R.string.delete), true));
ContextMenuDialog dialog = new ContextMenuDialog(drawable, playable.getName(), array, playable);
dialog.setOnItemClickListener(this);
dialog.show(fm, "Context Menu");
}
//Callback from the ContextMenuDialog class
#Override
public void onItemClickDialogFragment(int option, Playable playable) {
switch (option) {
case R.id.menu_delete :
// Perform delete actions
break;
case R.id.menu_share :
// Perform share actions
break;
}
}
public class ContextMenuDialog extends DialogFragment implements OnItemClickListener {
private Drawable drawableIcon;
private String title;
private List<ListViewElement> values;
private Playable playable;
private DialogFragmentOnItemClickListener listener;
public interface DialogFragmentOnItemClickListener {
void onItemClickDialogFragment(int option, Playable playable);
}
public void setOnItemClickListener(DialogFragmentOnItemClickListener listener) {
this.listener = listener;
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//Create the dialog without a title since the layout includes a customized title
setStyle(DialogFragment.STYLE_NO_TITLE, R.style.MyDialogStyle);
}
public ContextMenuDialog(Drawable drawableIcon, String title, List<ListViewElement> values, Playable playable) {
this.drawableIcon = drawableIcon;
this.title = title;
this.values = values;
this.playable = playable;
}
public ContextMenuDialog(int drawableResource, String title, List<ListViewElement> values, Playable playable) {
this.drawableIcon = getResources().getDrawable(drawableResource);
this.title = title;
this.values = values;
this.playable = playable;
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.context_menu, container, true);
TextView titleView = (TextView) view.findViewById(R.id.context_menu_title);
titleView.setText(title);
ImageView icon = (ImageView) view.findViewById(R.id.context_menu_artwork);
icon.setImageDrawable(drawableIcon);
ListView listView = (ListView) view.findViewById(R.id.context_menu_listview);
ContextMenuAdapter adapter = new ContextMenuAdapter(getActivity(), R.layout.context_menu_list_item, values);
listView.setAdapter(adapter);
listView.setOnItemClickListener(this);
return view;
}
I'm trying to recreate what Google did with the ListView in the Gmail app. In particular, I would like to have each list item include a CheckBox and two TextViews (one on top of the other). I need listeners for when the CheckBox is checked (or clicked) and when anywhere else on the list item is clicked. Lastly, I'd like the ActionBar to reflect that items are selected and provide options like Select All, Select None, etc (see this screenshot).
So far, here's the layout I came up with.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal" >
<CheckBox android:id="#+id/checkBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|center_horizontal" />
<LinearLayout android:id="#+id/linearLayout1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="6dp"
android:focusable="true"
android:clickable="true" >
<TextView android:id="#+id/titleTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp" />
<TextView android:id="#+id/dateTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
This displays everything properly, but I need pointers on how to set up the listeners for the two Views (#+id/checkBox and #+id/linearLayout1). I have looked at the List16 API demo, but they're using the simple_list_item_activated_1 layout and I'm not sure what the XML for that looks like. As their code suggests, I created a ModeCallback class that implements ListView.MultiChoiceModeListener and I set the ListView's choice mode to CHOICE_MODE_MULTIPLE_MODAL, but I don't know how to get the CheckBox in my layout to work with this.
Has anyone successfully copied the Gmail app's ListView behavior? I've searched quite a bit and could not come up with anything (despite several others asking similar questions, like this one - most answers just point back to this same API demo).
Also, for context, I'm loading data from a SQLite database into the list and I've created my own Cursor adapter (which works fine). I have a feeling I need to set up listeners in this class in the newView() and bindView() methods, but everything I've tried hasn't worked.
Any ideas?
This will work in SDK 7 (android 2.1) and higher. (together with SherlockActionBar)
Totaly mimic gmail listbox experience.
I override onTouchEvent so press in the left corner side will activate selection mode;
mutch better then trying to click on the tiny cheackbox.
I override performItemClick so pressing not in the left will act as a Regular press action.
I override setItemChecked so it will update mActionMode as needed.
public class SelectListView extends ListView {
private SherlockFragmentActivity mActivity;
ActionMode mActionMode;
public SelectListView(Context context) {
this( context, null, 0);
}
public SelectListView(Context context, AttributeSet attrs) {
this( context, attrs, 0);
}
public SelectListView(Context context, AttributeSet attrs, int defStyle) {
super( context, attrs, defStyle );
setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
mActivity = (SherlockFragmentActivity) context;
}
#Override
public boolean performItemClick(View view, int position, long id) {
OnItemClickListener mOnItemClickListener = getOnItemClickListener();
if (mOnItemClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
if (view != null)
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
mOnItemClickListener.onItemClick(this, view, position, id);
return true;
}
return false;
}
boolean mSelectionMode = false;
int mStartPosition;
#Override
public boolean onTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
final int x = (int) ev.getX();
final int y = (int) ev.getY();
if (action == MotionEvent.ACTION_DOWN && x < getWidth() / 7) {
mSelectionMode = true;
mStartPosition = pointToPosition(x, y);
}
if (!mSelectionMode)
return super.onTouchEvent(ev);
switch (action) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
if (pointToPosition(x, y) != mStartPosition)
mSelectionMode = false;
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
default:
mSelectionMode = false;
int mItemPosition = pointToPosition(x, y);
if (mStartPosition != ListView.INVALID_POSITION)
setItemChecked(mItemPosition, !isItemChecked(mItemPosition));
}
return true;
}
#Override
public void setItemChecked(int position, boolean value) {
super.setItemChecked(position, value);
// boolean r = getAdapter().hasStableIds();
int checkedCount = getCheckItemIds().length;
if (checkedCount == 0) {
if (mActionMode != null)
mActionMode.finish();
return;
}
if (mActionMode == null)
mActionMode = mActivity.startActionMode(new ModeCallback());
mActionMode.setTitle(checkedCount + " selected");
}
class ModeCallback implements ActionMode.Callback {
#Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
menu.add(getResources().getString(R.string.aBar_remove)).setIcon(R.drawable.ic_action_trash)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
return true;
}
#Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return true;
}
#Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
Toast.makeText(mActivity, "Delted items", Toast.LENGTH_SHORT).show();
mode.finish();
return true;
}
#Override
public void onDestroyActionMode(ActionMode mode) {
mActionMode = null;
clearChecked();
}
}
public void clearChecked() {
SparseBooleanArray CItem = getCheckedItemPositions();
for (int i = 0; i < CItem.size(); i++)
if (CItem.valueAt(i))
super.setItemChecked(CItem.keyAt(i), false);
}
}
you can use any listbox adapter you need.
if you have a checkbox on your list_item_layout tou need to extent your adapter like this:
ArrayAdapter<String> mAdapter = new ArrayAdapter<String>(this, R.layout.simple_list_item_multiple_choice,
R.id.text1, Cheeses.sCheeseStrings) {
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View v = super.getView(position, convertView, parent);
CheckBox checkBox = (CheckBox) v.findViewById(R.id.CheckBox);
checkBox.setChecked(((ListView)parent).isItemChecked(position));
return v;
}
};
Don't forget to use android:background="?attr/activatedBackgroundIndicator" on your list_item_layout.xml
The mode in the figure is called action mode. If you are using your custom row view and custom adapter, you don't have to use CHOICE_MODE_MULTIPLE_MODAL to start action mode. I tried it once and failed, and I suspect it is used for built-in adapter.
In order to call the action mode by yourself in your code, call method startActionMode in any listener method of any of your view, let it be checkbox, textview or something like that. You then pass the ModeCallback as parameters into it, and do whatever operation you want to do in ModeCallback class.
I don't think this one would work on pre-3.0 Android though.
Here is a brief example. I have a expandable list activity and would like to call out the action mode menu when user check/uncheck the checkbox, and here is my getChildView method in my custom adapter class:
public View getChildView(int groupPosition, int childPosition,
boolean isLastChild, View convertView, ViewGroup parent) {
if (convertView == null) {
LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(R.layout.childrow, null);
}
CheckBox cb = (CheckBox)convertView.findViewById(R.id.row_check);
cb.setChecked(false); // Initialize
cb.setTag(groupPosition + "," + childPosition);
cb.setFocusable(false); // To make the whole row selectable
cb.setOnCheckedChangeListener(new OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
String tag = (String)buttonView.getTag();
String[] pos = tag.split(",");
if (isChecked) {
if (mActionMode == null) mActionMode = startActionMode(mMultipleCallback);
}
else {
if (mActionMode != null) { // Only operate when mActionMode is available
mActionMode.finish();
mActionMode = null;
}
}
}
});
TextView tvTop = (TextView)convertView.findViewById(R.id.row_text_top);
TextView tvBottom = (TextView)convertView.findViewById(R.id.row_text_bottom);
tvTop.setText(mChildren.get(groupPosition).get(childPosition));
tvBottom.setText(mChildrenSize.get(groupPosition).get(childPosition));
return convertView;
}
In addition tocnbuff410's answer use the following code to update checked items count
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
++checkBoxCount;
} else {
--checkBoxCount;
}
...
if (mActionMode != null) {
mActionMode.setSubtitle(checkBoxCount + " item(s) selected.");
}
I can confirm that following code would not work on checkboxes of custom listviews. However, long press would still work:
listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
listView.setMultiChoiceModeListener(new ModeCallback());
Selecting checkbox on custom ListView would not trigger actionmode. Only inbuilt ListViews e.g. extending ListActivity would automatically do that.