ExpandableListView - keep selected child in a “pre

2019-02-02 09:18发布

问题:

I'm using an ExpandableListView in a left nav for a tablet screen.

When a user presses a child of a group in my expandable list, I'd like to keep the child in the pressed state so that the user knows for which child the right hand content is being shown for.

For a ListView, I was able to accomplish this effect with this line in the code:

getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);

and then applying this selector:

<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_activated="true" android:drawable="@drawable/menu_item_pressed" />
<item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/menu_item_normal" />
<item android:state_pressed="true" android:drawable="@drawable/menu_item_pressed" />
<item android:state_focused="true" android:state_pressed="false" android:drawable="@drawable/menu_item_pressed" />

The "state_activiated" is true on the listView item after the listView item is pressed.

I was hoping this same technique would work for my expandableListView but it hasn't. I used:

getExpandableListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);

and then used the same selector above, but it doesn't work. I've also tried other states such as state_selected and state_checked and those don't work either.

The selector above correctly applies the pressed and not pressed state. It looks like however with an ExpandableListView, the state is not "activated" after pressing a child.

Any help is appreciated. Thanks!

回答1:

Do the following on ExpandableListView:

Step 1. Set choice mode to single (Can be done in xml as android:choiceMode = "singleChoice")

Step 2. Use a selector xml as background (android:listSelector = "@drawable/selector_list_item")

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"
   android:exitFadeDuration="@android:integer/config_mediumAnimTime">

   <item android:drawable="@android:color/holo_blue_bright" android:state_pressed="true"/>
   <item android:drawable="@android:color/holo_blue_light" android:state_selected="true"/>
   <item android:drawable="@android:color/holo_blue_dark" android:state_activated="true"/>

</selector>

Step 3. Call expandableListView.setItemChecked(index,true) in onChildClick() callback.

index is a 0 based index of the child item calculated as follows

Group 1 [index = 0]

  • Child item 1
  • Child item 2
  • Child item 3 [index = 3]

Group 2 [index = 4]

  • Child item 1
  • Child item 2
  • Child item 3 [index = 7]

Group 3 [index = 8]

  • Child item 1
  • Child item 2
  • Child item 3 [index = 11]

If we have list headers as well, they would also account for index values.

Here is a working example:

@Override
public boolean onChildClick(ExpandableListView parent, View v,
        int groupPosition, int childPosition, long id) {
    ...

    int index = parent.getFlatListPosition(ExpandableListView.getPackedPositionForChild(groupPosition, childPosition));
    parent.setItemChecked(index, true);


    return true;
}


回答2:

I ended up solving this problem without using a selector on the child view. The adapter for the expandableListView takes data for each group and child. I added a "selected" boolean for the child data. The fragment takes care of correctly setting each child's selected boolean to either true or false. Then, in the adapter's "getChildView" method, if the child's "selected" boolean is true, I set the view's background resource to my "pressed" background. Otherwise I set it to the "not pressed" background. Doing this in addition to using a selector for the textView on the child view to change the text color when pressed achieves the desired effect.

Below are the things my Fragment does to maintain the child's "selected" boolean:

  • keep your group/child data as a class variable in your fragment
  • onChildClick - loop through the child data, setting all "selected" = false. Then set the child of the current group/child to true. Refresh the expandableListView by calling setEmptyView, reset the listeners, and set the list adapter.
  • onGroupClick - loop through the child data, setting all "selected" = false. Then set the child of the current group/child to true.
  • wherever in your fragment where you show your list - Populate your adapter data, set the list adapter, call notifyDataSetChanged on the adapter. Refresh the expandableListView by calling setEmptyView, reset the listeners, and set the list adapter.

In addition to the above I maintain a currentGroupPosition and currentChildPosition as class variables. Using setRetainInstance = true allows this to work properly on things like screen rotates.



回答3:

I don't think there is any built-in way to do what you're attempting with built-in Android functionality. To do some testing, I started with code from the Android API examples (C:\<android-sdk-directory>\samples\android-15\ApiDemos\src\com\example\android\apis\view\ExpandableList3.java).

I used ExpandableListView.CHOICE_MODE_SINGLE. The choice mode applies to whatever View is checked or selected. (The ExpandableListView source code says it determines how many items can be selected at once, but in ListView, it seems to apply to what is checked.) Also note that the doc says you cannot get a valid result from getCheckedItemPosition for a ListView in single choice mode. This seems to be true.

My testing showed that ExpandableListActivity doesn't automatically make the clicked/touched item checked or selected, nor does it automatically give focus to the clicked child. Only the ExpandableListActivityitself and the group holding the child item are auotmatically focused after you click one of their children.

Here's some of my code (much of it taken from the Android examples):

@Override
public void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    final String NAME = "NAME";
    final String IS_EVEN = "IS_EVEN";

    List<Map<String, String>> groupData = new ArrayList<Map<String, String>>();
    List<List<Map<String, String>>> childData = new ArrayList<List<Map<String, String>>>();

    for (int i = 0; i < 12; i++) {

        Map<String, String> curGroupMap = new HashMap<String, String>();
        groupData.add(curGroupMap);

        curGroupMap.put(NAME, "Group " + i);
        curGroupMap.put(IS_EVEN, (i % 2 == 0) ? "This group is even" : "This group is odd");

        List<Map<String, String>> children = new ArrayList<Map<String, String>>();

        for (int j = 0; j < 12; j++) {

            Map<String, String> curChildMap = new HashMap<String, String>();
            children.add(curChildMap);

            curChildMap.put(NAME, "Child " + j);
            curChildMap.put(IS_EVEN, (j % 2 == 0) ? "This child is even" : "This child is odd");
        }

        childData.add(children);
    }

    SimpleExpandableListAdapter adapter = new SimpleExpandableListAdapter(

        this,
        groupData,
        android.R.layout.simple_expandable_list_item_1,
        new String[] {NAME, IS_EVEN},
        new int[] {android.R.id.text1, android.R.id.text2},
        childData,
        R.layout.child, 
        new String[] {NAME, IS_EVEN},
        new int[] {android.R.id.text1, android.R.id.text2}
        );

    ExpandableListView expListView = (ExpandableListView)findViewById(android.R.id.list);

    setListAdapter(adapter);
    expListView.setChoiceMode(ExpandableListView.CHOICE_MODE_SINGLE);
    expListView.setOnChildClickListener(new OnChildClickListener() {

        @Override
        public boolean onChildClick(ExpandableListView expListView, View viewClicked, int groupPosition, int childPosition, long childRowId) {

            viewClicked.setSelected(true); // the only way to force a selection of the clicked item      

Note the very last line, which forces the clicked child to have a selected state. You can then use an XML selector with a Drawable state list to highlight the selected item when it is selected. (This doesn't fully work--more on that below.)

Inside onChildClick in my ExpandableListActivity, I did some testing and found the following:

groupPosition; // index of group within the ExpandableListViewActivity           
childRowId; // for my particular ExpandableListView, this was the index of the child in the group
childPosition; // for my particularExpandableListView, this was also the index of the child in the group

expListView.getCheckedItemPosition(); // null - from ListView class
expListView.getSelectedView(); // null - from AbsListView class
expListView.getSelectedItemId(); // a long, based on the packed position - from AdapterView class
expListView.getSelectedId(); // -1 - from ExpandableListView class, calls getSelectedPosition()
expListView.getSelectedPosition(); // -1 - from ExpandableListView class, calls getSelectedItemPosition()
expListView.getSelectedItemPosition(); // -1 - from AdapterView class
expListView.getFocusedChild(); // null - from ViewGroup class
expListView.getItemIdAtPosition(1); // a long (View ID) - from AdapterView class
viewClicked.isFocused()); // false
viewClicked.isPressed()); // false
viewClicked.isSelected()); // true only if we explicitly set it to true
expListView.isFocused()); // true
expListView.isPressed()); // false
expListView.isSelected()); // false
expListView.getCheckedItemPosition())); // -1 - from ListView
expListView.getSelectedId())); // -1 - from ExpandableListView  

/* expListView.getChildAt(childPosition) is not helpful, because it looks at the rendered 
   LinearLayout with a bunch of TextViews inside it to get the index and find the child,
   and you won't generally know where to look for the child you want */

I also tried creating a new OnItemClickListener and implementing its onItemClick method. Although the documentation suggests that onItemClickListener will work in some capacity on ExpandableListViews, I have never seen it called.

The first thing I tried to change the background color on the chosen child item and keep it highlighted didn't work well. I set a Drawable for android:state_window_focused="true" inside the selector XML file. It worked sort of but presented several problems.

The second thing I tried was explicitly selecting the item pressed in onChildClick by calling viewClicked.setSelected(true);. The problems here were fewer:

1) If I scrolled until the highlighted item was out of view and returned to it, the highlighting had disappeared, 2) The same would happen if I rotated the phone, and 3) The state of the highlighted item was no longer selected as soon as it was no longer visible on the display.

Android's ExpandableListView apparently was not meant to work like an accordion-style left navigation menu, even though that seems like a fairly obvious use for it. If you want to use it that way, I think you'll have to do a fair amount of customization and digging into how the Views are drawn.

Good luck.



回答4:

Declare globally:

View view_Group;

yourExpandableListView.setChoiceMode(ExpandableListView.CHOICE_MODE_SINGLE);
yourExpandableListView.setOnChildClickListener(new OnChildClickListener() {

    @Override
    public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) { 
    v.setSelected(true); 
    if (view_Group != null) {
        view_Group.setBackgroundColor(Default Row Color Here);
    }
    view_Group = v;
    view_Group.setBackgroundColor(Color.parseColor("#676767"));      
}


回答5:

This solution works for me (expandable list view used inside navigation view):

Create custom menu model, for example

public class DrawerExpandedMenuModel {

String iconName = "";
int id = -1;
int iconImg = -1;
boolean isSelected = false;
.
.
.

Override onChildClick of ExpandableListView

        expandableList.setOnChildClickListener(new ExpandableListView.OnChildClickListener() {
        @Override
        public boolean onChildClick(ExpandableListView expandableListView, View view, int groupPosition, int childPosition, long l) {
            for (int k = 0; k < listDataHeader.size(); k++){
                listDataHeader.get(k).isSelected(false);
                if (listDataChild.containsKey(listDataHeader.get(k))) {
                    List<DrawerExpandedMenuModel> childList = listDataChild.get(listDataHeader.get(k));
                    if (childList != null) {
                        for (int j = 0; j < childList.size(); j++) {
                            if (k == groupPosition && j == childPosition) {
                                childList.get(j).isSelected(true);
                            } else {
                                childList.get(j).isSelected(false);
                            }
                        }
                    }
                }
            }
            mMenuAdapter.notifyDataSetChanged();

Override onGroupClick of ExpandableListView

        expandableList.setOnGroupClickListener(new ExpandableListView.OnGroupClickListener() {
        @Override
        public boolean onGroupClick(ExpandableListView expandableListView, View view, int groupPosition, long l) {
            for (int k = 0; k < listDataHeader.size(); k++){
                if (k == groupPosition) {
                    listDataHeader.get(k).isSelected(true);
                } else {
                    listDataHeader.get(k).isSelected(false);
                }
                if (listDataChild.containsKey(listDataHeader.get(k))) {
                    List<DrawerExpandedMenuModel> childList = listDataChild.get(listDataHeader.get(k));
                    if (childList != null) {
                        for (int j = 0; j < childList.size(); j++) {
                            childList.get(j).isSelected(false);
                        }
                    }
                }
            }
            mMenuAdapter.notifyDataSetChanged();

Override getGroupView and getChildView

public class DrawerExpandableListAdapter extends BaseExpandableListAdapter {
.
.
.
  @Override
public View getGroupView(final int groupPosition, final boolean isExpanded, View convertView, final ViewGroup parent) {
    if (headerTitle.isSelected() == true) convertView.setBackgroundColor(mContext.getResources().getColor(R.color.gray));
    else convertView.setBackgroundColor(mContext.getResources().getColor(R.color.transparent));
.
.
.
@Override
public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
    if (childObj.isSelected() == true) convertView.setBackgroundColor(mContext.getResources().getColor(R.color.gray));
    else convertView.setBackgroundColor(mContext.getResources().getColor(R.color.transparent));

That's it!