WhiteTigerK
WhiteTigerK

Reputation: 7051

Android ListView Selection Problem

Hi All,

I apologize for the following long question...

I have a LinearLayout which contains a ListView and some other items. As for the ListView, each on of its rows is a LinearLayout that contains 3 views - Checkbox, ImageView and TextView (from left to right - horizontal). Since I wanted the whole row to be selected when using the trackball (to be highlighted with a background color), I set the all three views inside the LinearLayout row as not focusable, and it worked.

Now I'm having 2 problems regarding this ListView. First, I want that whenever I touch a row in the ListView (with my finger), to get the same behavior as when using the trackball - means that I want the row to be selected (highlighted). What's happening right now is that when I touch the row it really becomes selected, but when I release my finger the selection is gone (much like happens in device's contact list).

Second - from a Menu, I can display a new LinearLayout instead the one that contains the ListView (different application's screen). When this happens, I still stores the object of the LinearLayout that contains the ListView, because I want to be able to re-display it later without creating it from scratch. The problem is that when I re-disaply the LinearLayout with the ListView, none of the ListView's rows are selected, even if a ceratin row was selected when the the LinearLayout with the ListView "left" the screen.

Sorry again for the long post.

Thanks!

Upvotes: 9

Views: 30221

Answers (8)

Yunis Rasulzade
Yunis Rasulzade

Reputation: 385

Just paste below code line to ListView in XML. Hope it helps someone :)

android:listSelector="@android:color/ANY_COLOR"

Upvotes: 0

David Berry
David Berry

Reputation: 41246

A much cleaner solution for me centered around the fact that if you set either android:choiceMode="singleChoice" or android:choiceMode="multipleChoice" in your ListView, then the ListView will attempt to maintain the checked state for either a single or multiple selection ListView. The gotcha is that it depends on the list cell implementing Checkable

So, you start by implementing a CheckableLinearLayout (or FrameLayout might be better) so that android will maintain the checked state for you and you can keep the Drawable's in sync. The key here is refreshDrawableState and onCreateDrawableState They update the Drawable so the background will be drawn highlighted or not.

public class CheckableLinearLayout extends LinearLayout implements Checkable {
    boolean checked;

    public CheckableLinearLayout(Context context) {
        super(context);
    }

    public CheckableLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public CheckableLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public void setChecked(boolean checked) {
        this.checked = checked;
        refreshDrawableState();
    }

    @Override
    public boolean isChecked() {
        return checked;
    }

    @Override
    public void toggle() {
        setChecked(!isChecked());
    }

    private static final int[] CheckedStateSet = { android.R.attr.state_checked };

    @Override
    protected int[] onCreateDrawableState(int extraSpace) {
        final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
        if (isChecked()) {
            mergeDrawableStates(drawableState, CheckedStateSet);
        }
        return drawableState;
    }
}

Then, a simple selector for the background drawable that will show the checked state:

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="#ccc" android:state_checked="true" />
    <item android:drawable="@android:color/transparent" />
</selector>

Now I can use CheckableLinearLayout in any list that I want to show persistent selection state (multiple or single):

<CheckableLinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@drawable/checkable_cell"
    >

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/text"
        android:layout_margin="10dp"
        android:maxLines="8"
        />
</CheckableLinearLayout>

I don't need any special code in the Fragment to update the checked state, and I can maintain the selected position around rotation with:

state.putInt("selection", listView.getCheckedItemPosition());

and

listView.setItemChecked(state.getInt("selection"), true);

likewise, I can use setItemChecked to set an initially selected item, or use getCheckedItemPosition (or getCheckedItemPositions() for multiple selection) to get the current selection.

Upvotes: 1

Dacker
Dacker

Reputation: 892

I am using an Adapter and didn't want to set custom background colors, but use the android:state_selected in drawable xml. SetSelection didn't work for me, but maybe that's also since I needed SetNotifyDataChanged which shows that the Selected State is not persistent.

I also found that the Selected state for an item in a ListView is not persistent, since SetNotifyDataChanged results in updating the ListView layout which clears them all. Setting the item to Selected in the Adapter's GetView is too soon too.

Eventually I set the Selected state for the view of the selected item after the layout of the listview has been changed, which is when LayoutChange event is being triggered (in Java it's probably attaching a to OnLayoutChangeListener of the ListView).

To make it really easy I store the view of the selected item as Adapter's SelectedItemView. In the ListView's LayoutChange eventhandler I just set the adapter's SelectedItemView.Selected to true.

Here's the code from my Activity where I set the Adapter for the ListView and also subscribe to LayoutChange (or in Java attach an OnLayoutChangeListener)

        ringTonesListView.Adapter = ringTonesListAdapter;
        ringTonesListView.LayoutChange += (s, layoutChangeArgs) => {
            //At this stage the layout has been updated and the Selected can be set to true for the view of the selected item. This will result in android:state_selected logic to be applied as desired and styling can be completely done per layout in Resources.
            ringTonesListAdapter.SelectedItemView.Selected = true;
        };

Here's my code for the Adapter:

public class RingTonesListAdapter : BaseAdapter<RingToneItem>
{
    List<RingTone> Items { get; set; }

    public override View GetView(int position, View convertView, ViewGroup parent)
    {
        View view = convertView;

        // re-use an existing view, if one is available
        // otherwise create a new one
        if (view == null)
        {
            view = Context.LayoutInflater.Inflate(Resource.Layout.AlertSoundItem, parent, false);
            view.Click += SelectRingTone;
        }

        RingTone ringTone = this[position];
        if (ringTone.Selected)
        {
            //==> Important
            //Store this view since it's the view for the Selected Item
            SelectedItemView = view;
            //Setting view.Selected to true here doesn't help either, since Selected will be cleared after.
        }

        return view;
    }

    private void SelectRingTone(object sender, EventArgs args)
    {
        View view = (View)sender;
        string title = view.FindViewById<TextView>(Resource.Id.ringToneTitle).Text;
        RingToneItem ringToneItem = Items.First(rt => rt.Title == title);
        if (!ringToneItem.Selected)
        {
            //The RingTone was not selected and is selected now
            //Deselect Old and Select new
            foreach (RingToneItem oldItem in Items.Where(rt => rt.Selected))
            {
                oldItem.Selected = false;
            }

            // Select New RingTone
            ringToneItem.Selected = true;
            //Update the ListView. 
            //This will result in removal of Selected state for all Items when the ListView updates it's layout
            NotifyDataSetChanged();
        }

        //Now play the test sound
        NotifierService.TestSound(Context, ringToneItem);
    }

    public View SelectedItemView { get; set; }
}

Upvotes: 1

vovkab
vovkab

Reputation: 1292

This should help:

private void setListviewSelection(final ListView list, final int pos) {
    list.post(new Runnable() {
        @Override
        public void run() {
            list.setSelection(pos);
            View v = list.getChildAt(pos);
            if (v != null) {
                v.requestFocus();
            }
        }
    });
}

Upvotes: 0

Tuo
Tuo

Reputation: 209

Yeah, From an iOS developer's perspective, I find that it is extremely hard to apply features like "set default selection when starting" and "remember selection status after user clicked row" to ListView.

So let's start with "remember selection" first.The problem is that even if you know that you can use selector xml to define highlight/pressed/focus style.But that style will not be kept after user clicked that row. For instance, I have a highlighting selector xml (list_selector.xml under res/drawable folder) like this (but you may have other fields need to highlight like text color of textview in row):

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/list_selector_pressed" android:state_pressed="true" />
    <item android:drawable="@drawable/list_selector_pressed" android:state_selected="true" />
</selector>    

and list_selector_pressed.xml which defined the highlighting style--set the background color to a gray color :

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <shape xmlns:android="http://schemas.android.com/apk/res/android">
            <solid android:color="@color/dark_gray" />
        </shape>
    </item>
</layer-list>

So as @David Hedlund suggested:

Rather, assign an OnItemClickListener, and have it store away the id of the selected item into some variable.

you need to create a instance variable on top of your class:

    private View currentSelectedView;

then go to

@Override
public void onListItemClick(ListView l, View v, int position, long id) {
    if (currentSelectedView != null && currentSelectedView != v) {
        unhighlightCurrentRow(currentSelectedView);
    }

    currentSelectedView = v;
    highlightCurrentRow(currentSelectedView);
    //other codes 
    }

Pretty simple: we check if currentSelectedView is null or current clicked view or not. we first to unhighlight any style by calling method unhighlightCurrentRow(currentSelectedView)---you may wonder why we pass instant variable currentSelectedView as parameter, I will explain it later. Then we assign view to currentSelectedView and highlight current row; so that the style will persist after user's clicking is done.

private void unhighlightCurrentRow(View rowView) {
    rowView.setBackgroundColor(Color.TRANSPARENT);
    TextView textView = (TextView) rowView.findViewById(R.id.menuTitle);
    textView.setTextColor(getResources().getColor(R.color.white));
}

private void highlightCurrentRow(View rowView) {
    rowView.setBackgroundColor(getResources().getColor(
            R.color.dark_gray));
    TextView textView = (TextView) rowView.findViewById(R.id.menuTitle);
    textView.setTextColor(getResources().getColor(R.color.yellow));

} 

Aha, that's it. That is how we implement "remember selection" for list view. As you see, we have to duplicate the codes for styling both in xml and java code--pretty stupid :(

Next about "set default selection". You may think that you can do this

listView.setAdapter(adatper)
listView.setSelection(0);
currentSelectedView = listView.getChildAt(0);
highlightCurrentRow(currentSelectedView);

in onCreate() in activity or onActivityCreated() in fragment.
But if you run it , you will get NullPointer exception and why ? because at this time, the listview is not rendered yet and Android doesn't like iOS which have viewWillAppear. SO you have to create an instant variable to remember whether it is first time to render listview cell and in onListItemClick to unset that variable:

So under currentSelectedView declaration:

private Boolean firstTimeStartup = true;

then add methods : suppose we want to highlight the first row in list view:

public class HomeAdapter extends ArrayAdapter<String> {
    int layoutResourceId;

    public HomeAdapter(Context context, int textViewResourceId,
            ArrayList<String> objects) {
        super(context, textViewResourceId, objects);
        layoutResourceId = textViewResourceId;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if (convertView == null) {
            convertView = LayoutInflater.from(getContext()).inflate(
                    layoutResourceId, null);
        }

        if (firstTimeStartup && postion == 0) {
            highlightCurrentRow(convertView);
        } else {
            unhighlightCurrentRow(convertView);
        }

        TextView title = (TextView) convertView
                .findViewById(R.id.menuTitle);
        title.setText(getItem(position));
        return convertView;
    }
}

Pretty simple. But you need to make some changes in onListItemClick method:

@Override
public void onListItemClick(ListView l, View v, int position, long id) {

    if (firstTimeStartup) {// first time  highlight first row
        currentSelectedView = l.getChildAt(0);
    }
    firstTimeStartup = false; 
    if (currentSelectedView != null && currentSelectedView != v) {
        unhighlightCurrentRow(currentSelectedView);
    }

    currentSelectedView = v;
    highlightCurrentRow(currentSelectedView);

     //other codes
}

There you go! Enjoy Android :)

Upvotes: 19

Irek Wisniowski
Irek Wisniowski

Reputation: 11

I've solved that issue sending a message after notifyDataSetChanged() ; and calling setSelection(0) in my handleMessage function. It Seems ugly, but helped me several times when SDK behaves oddly.

Upvotes: 1

carox
carox

Reputation: 59

private void setListviewSelection(final ListView list, final int pos) {
list.post(new Runnable() 
   {
    @Override
    public void run() 
      {
        list.setSelection(pos);
        View v = list.getChildAt(pos);
        if (v != null) 
        {
            v.requestFocus();
        }
    }
});
}

Above helps me setting a row focussed in the list.

Upvotes: 5

David Hedlund
David Hedlund

Reputation: 129832

  1. This is he default and expected behavior. Tampering with this is strongly suggested against.
  2. This follows from 1. to some extent. The selected-state is not persistent. Rather, assign an OnItemClickListener, and have it store away the id of the selected item into some variable. If you need to re-select the item when you come back, you can use setSelection()

Upvotes: 3

Related Questions