How do I clear ListView selection?

2020-01-28 09:54发布

问题:

TL;DR: You choose an option from (a) my listview. Then, you change your mind and type something in (b) my edit text. How do I clear your listview selection and only show your edittext? (and vice versa)

I have an application with a listview of options as well as an edittext to create an own option. I need the user to either choose or create an option, but not both. Here's a drawing of my layout:

Whenever the user selects an option from the listview, I set it as "selected" by making it green, like so:

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

(this is set as the background of my listview)

Problem: I want to unselect the listview option if the user decides to type in their own option since they can only have one option.

  1. User selects an option from the listview
  2. User decides they want to create their own option using the edittext
  3. The listview option is unselected when they start typing their own

I've tried doing the following, but nothing unselects.

e.setOnEditorActionListener(new TextView.OnEditorActionListener()
        {
            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {

                for(int i=0; i<=5; i++){
                                listView.setItemChecked(i, false);
                }
                listView.clearChoices();
                listView.requestLayout()
                adapter.notifyDataSetChanged()
             }
        }

A very puzzling predicament, any help is appreciated!

Edit: here is the layout of the edittext:

<EditText
                android:id="@+id/editText"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_alignBaseline="@+id/textView4"
                android:layout_alignBottom="@+id/textView4"
                android:layout_toEndOf="@+id/textView4"
                android:layout_toRightOf="@+id/textView4"
                android:color="@color/colorPrimary"
                android:imeOptions="actionDone"
                android:inputType="text"
                android:textColor="@color/textColorPrimary"
                android:textColorHint="@color/colorPrimary" />

Edit: here is the layout of the listview:

    <ListView
        android:id="@+id/listview"
        android:layout_width="wrap_content"
        android:layout_height="250dp"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_below="@+id/toolbar"
        android:background="@drawable/bg_key"
        android:choiceMode="singleChoice"
        android:listSelector="@color/colorPrimary">

    </ListView>

回答1:

Long Story Short

  • ListView selector (android:listSelector) is designed to indicate a click event, but not selected items.
  • If a ListView selector is drawn (after first click) it won't dissapear without drastic changes in the ListView
  • Hence use only drawables with transparent background if no state is applied to it as a ListView selector. Don't use a plain color resource for it, don't confuse yourself.
  • Use ListView choice mode (android:choiceMode) to indicate selected items.
  • ListView tells which row is selected by setting android:state_activated on their root view. Provide your adapter with corresponding layout/views to represent selected items correctly.

Theory

Well, the built-in selection in ListView is utterly tricky at a first glance. However there are two main distinctions you should keep in mind to avoid confusing like this - list view selector and choice mode.

ListView selector

ListView selector is a drawable resource that is assumed to indicate an event of clicking a list item. You can specify it either by XML-property android:listSelector or using method setSelector(). I couldn't find it in docs, but my understanding is that this resource should not be a plain color, because after it's being drawn, it won't vanish without drastic changes in the view (like setting an adapter, that in turn may cause some glitches to appear), hence such drawable should be visible only while particular state (e.g. android:state_pressed) is applied. Here is a simple example of the drawable that can be used as a List View selector

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

For whatever reason you cannot use a Color State List as List View selector, but still can use plain colors (that are mostly inappropriate) and State List drawables. It makes things somewhat confusing. After the first click on a List View happens, you will not be able to remove List View selector from the List View easily.

The main idea here is that List View selector is not designed to indicate selected item.

ListView choice mode

ListView choice mode is assumed to indicate selected items. As you might know, primarily there are two choice modes we can use in ListView - Single Choice and Multiple Choice. They allow to track a single or multiple rows selected respectively. You can set them via android:choiceMode XML-property or setChoiceMode() method. The ListView itself keeps selected rows in it and let them know which one is selected at any given moment by setting android:state_activated property of the row root view. In order to make your rows reflect this state, their root view must have a corresponding drawable set, e.g. as a background. Here is an example of such drawable:

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

You can make rows selected/deselected programmatically using the setItemChecked() method. If you want a ListView to clear all selected items, you can use the clearChoices() method. You also can check selected items using the family of the methods: getCheckedItemCount(), getCheckedItemIds(), getCheckedItemPosition() (for single choice mode), getCheckedItemPositions() (for multiple choice mode)

Conclusion

If you want to keep things simple, do not use the List View selector to indicate selected items.


Solving the issue

Option 1. Dirty fix - hide selector

Instead of actually removing selector, changing layouts and implementing a robust approach, we can hide the selector drawable when it's needed and show it later when clicking a ListView item:

    public void hideListViewSelector() {
        mListView.getSelector().setAlpha(0);
    }

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        if (mListView.getSelector().getAlpha() == 0) {
            mListView.getSelector().setAlpha(255);
        }
    }

Option 2. Thoughtful way

Let's go through your code and make it comply the rules i described step by step.

Fix ListView layout

In your ListView layout the selector is set to a plain color, and therefore your items are colored by it when they are clicked. The drawable you use as the ListView background have no impact, because ListView state doesn't change when its rows are clicked, hence your ListView always has just @color/windowBackground background.

To solve your problem you need at first remove the selector from the ListView layout:

<ListView
    android:id="@+id/listview"
    android:layout_width="wrap_content"
    android:layout_height="250dp"
    android:layout_alignParentLeft="true"
    android:layout_alignParentStart="true"
    android:layout_below="@+id/toolbar"
    android:listSelector="@color/colorPrimary"
    android:background="@color/windowBackground"
    android:choiceMode="singleChoice"/> 

Make your rows reflect activated state

In the comments you give your adapter as follows:

final ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, text1, listOfThings);

You also asked me if it's possible to keep using standard adapter to achieve desired behavior. We can for sure, but anyway a few changes are required. I can see 3 options for this case:

1. Using standard android checked layout

You can just specify a corresponding standard layout - either any of the layouts that use CheckedTextView without changed background drawable as the root component or of those that use activatedBackgroundIndicator as their background drawable. For your case the most appropriate option should be the simple_list_item_activated_1. Just set it as in your ArrayAdapter constructor like this:

final ArrayAdapter<String> adapter = new ArrayAdapter(this, android.R.layout.simple_list_item_activated_1, android.R.id.text1, listOfThings);

This option is the closest to what i understand by 'standard' adapter.

2. Customize your adapter

You can use standard layout and mostly standard adapter with a small exception of getting a view for your items. Just introduce an anonymous class and override the method getView(), providing row views with corresponding background drawable:

final ArrayAdapter<String> adapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1, android.R.id.text1, listOfThings) {

    @NonNull
    @Override
    public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
        final View view = super.getView(position, convertView, parent);
        if (convertView == null) {
            view.setBackgroundResource(R.drawable.list_item_bg);
        }
        return view;
    }
};

3. Customize your layout

The most common way of addressing this issue is of course introducing your own layout for the items view. Here is my simple example:

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:background="@drawable/list_item_bg"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:padding="16dp">

    <TextView
        android:id="@android:id/text1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</FrameLayout>

I saved it in a file /res/layout/list_view_item.xml Do not forget setting this layout in your adapter:

final ArrayAdapter<String> adapter = new ArrayAdapter(this, R.layout.list_view_item, android.R.id.text1, listOfThings);

Clear choices when needed

After that your rows will reflect selected state when they are clicked, and you can easily clear the selected state of your ListView by calling clearChoices() and consequence requestLayout() to ask the ListView to redraw itself.

One little comment here that if you want unselect the item when user start typing, but not when he actually clicks the return (done) button, you need to use a TextWatcher callback instead:

    mEditText.addTextChangedListener(new TextWatcher(){

        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
        }

        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
            if (mListView.getCheckedItemCount() > 0) {
                mListView.clearChoices();
                mListView.requestLayout();
            }
        }

        @Override
        public void afterTextChanged(Editable s) {}
    });

Hopefully, it helped.



回答2:

I have a good solution to do that. Add EditText to your layout which contains on your ListView as this layout:

    <RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:focusable="true"
    android:focusableInTouchMode="true"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <ListView
        android:id="@+id/list_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:scrollbars="vertical"
        android:choiceMode="singleChoice"
        />
    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Comment"
        android:layout_below="@id/list_view"
        android:id="@+id/editText"
        android:nextFocusUp="@id/editText"
        android:nextFocusLeft="@id/editText"/>
</RelativeLayout>

Then initialize Boolean variable to check whether editText if focused or not for example use this : boolean canBeSelected = true;

Then after setting adapter use this code:

listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
        @Override
        public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
            if (canBeSelected) {
                listView.setSelector(R.drawable.background);
                listView.setSelected(true);
                listView.setSelection(i);
            } else {
                if (!editText.isFocused()){
                    canBeSelected = true;
                    listView.setSelector(R.drawable.background);
                    listView.setSelected(true);
                    listView.setSelection(i);
                }
            }
        }
    });
    editText.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View view, MotionEvent motionEvent) {
            canBeSelected = false;
            Drawable transparentDrawable = new ColorDrawable(Color.TRANSPARENT);
            listView.setSelector(transparentDrawable);
            listView.clearChoices();
            listView.setSelected(false);
            return false;
        }
    });



    editText.addTextChangedListener(new TextWatcher() {
        @Override
        public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
            if (editText.isFocused()){
                Drawable transparentDrawable = new ColorDrawable(Color.TRANSPARENT);
                listView.setSelector(transparentDrawable);
                listView.clearChoices();
                listView.setSelected(false);
                canBeSelected = false;
            }
        }

        @Override
        public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
            Drawable transparentDrawable = new ColorDrawable(Color.TRANSPARENT);
            listView.setSelector(transparentDrawable);
            listView.clearChoices();
            listView.setSelected(false);
            canBeSelected = false;
        }

        @Override
        public void afterTextChanged(Editable editable) {
            if (editText.isFocused()) {
                Drawable transparentDrawable = new ColorDrawable(Color.TRANSPARENT);
                listView.setSelector(transparentDrawable);
                listView.clearChoices();
                listView.setSelected(false);
                canBeSelected = false;
            }
        }
    });
}

Hope it works with you :)



回答3:

Re-setting the adapter in the edittext listener worked for me:

editText.addTextChangedListener(new TextWatcher() {
        @Override
        public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {

        }
        @Override
        public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {

            listview.clearChoices();
            listview.setAdapter(adapter);
            Toast.makeText(getApplicationContext(),"Typing" + listview.getSelectedItemPosition(), Toast.LENGTH_SHORT).show();
        }
        @Override
        public void afterTextChanged(Editable editable) {

        }
    });

I put the selected index in a toast to check if the item was correctly deselected.

Hope this works!!



回答4:

Just call clear when you make the request for the second data set:

    arrayAdapter!!.clear()

You load your first data set
The user select one elements,
This action highlight your item
For any reason you launch the reload of your data set (because edittext's value changed),
at this moment call, clear() on your adapter.
Then you retrieved your dataset, you send it to the arrayAdapter and No one is selected .
This is because when you clear, it also clear the selected flag



回答5:

Have you tried setselected for each element of your list adapter?

mEditText.setSelected(false);

https://developer.android.com/reference/android/view/accessibility/AccessibilityNodeInfo.html#setSelected(boolean)