0

I need to add custom button objects to each row in a ListView. Here's a simplified row layout:

<LinearLayout android:id="@+id/table_cell"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    >

    <TextView android:id="@+id/label"
        android:textSize="19dp"
        android:textStyle="bold"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:lines="1"
    />

    <LinearLayout android:id="@+id/button_wrapper"
        android:layout_width="100dp"
        android:layout_height="match_parent"
    />

</LinearLayout>

In my custom ArrayAdapter, I place the button into the cell in getView():

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    // recycle the cell if possible
    View cell = null;
    if (convertView == null) {
        LayoutInflater inflater = (LayoutInflater) this.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        cell = inflater.inflate(R.layout.table_cell, parent, false);
    } else {
        cell = convertView;
    }

    MyButton button = (MyButton) this.buttons.get(position);
    if (button != null) {
        // remove the button from the previous instance of this cell
        ViewGroup parent = (ViewGroup)button.getParent();
        if (parent != null) {
            parent.removeView(button);
        }

        // add the button to the new instance of this cell
        ViewGroup buttonWrapper = (ViewGroup)cell.findViewById(R.id.button_wrapper);
        buttonWrapper.addView(button);
    }
}

I know that getView() is called multiple times for each table row as I scroll the table or click buttons or do other things, so the code above removes the button from the previous view before adding it to the new view to avoid a "view already has a parent" exception.

The problem is that this assumes the latest view generated from getView is the one that's visible on the screen, but this is often not the case. Sometimes getView() generates new views, but an older view remains on the screen. In that situation, my button disappears because getView() moves it to a new view that is not visible. I discovered that behavior by initializing an int variable named repeatRowTest and then adding this code inside getView():

if (position == 0) {
    Log.d("getView", "repeat row count: " + repeatRowCountTest);
    TextView label = (TextView)cell.findViewById(R.id.label);
    label.setText(String.format("%d %s", repeatRowCountTest, label.getText()));
    repeatRowCountTest++;
}

This shows me how many times a given row has been generated, and which instance is currently displayed. I might see a row being generated 10 times, while only the 5th one is displayed. But my buttons will only be visible if the latest instance of the row is displayed.

So the question is, how can I tell whether a row generated in getView() is actually going to be displayed, so I know whether to move my button into it, or leave my button where it is? Or more generally, how can I add a button to a row and make sure it remains visible as getView is repeated for a given position?

I've inspected all the properties of a displayed row versus an extra, non-displayed row, and couldn't find any differences. I also tried calling notifyDataSetChanged on the array adapter after my buttons disappear, and that refreshes the list with all the latest views that contain the buttons -- but it's not clear which events trigger getView to repeat itself, so I wouldn't know when I need to call notifyDataSetChanged to make things right again. I suppose I could clone the button and add a new instance of the button to each new instance of the row, but that seems more resource-intensive than is necessary, and will create other problems since other objects have references to these buttons. I haven't found any code examples showing the best way to do this, but it seems like a common requirement, so hopefully I'm missing something simple!

UPDATE: Is there a method of the ArrayAdapter I can override that is called after the getView() methods are called? If so, I could check the parents of all the recently created rows to see if they are actually displayed in the ListView, and refresh the ListView at that point if they aren't.

arlomedia
  • 8,534
  • 5
  • 60
  • 108

2 Answers2

0

You don't need to create your custom button by code, you can insert it inside the row layout xml like a normal android button. In this way you can remove the button wrapper layout and the add/remove logic from getView.

<LinearLayout android:id="@+id/table_cell"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>

<TextView android:id="@+id/label"
    android:textSize="19dp"
    android:textStyle="bold"
    android:layout_width="100dp"
    android:layout_height="wrap_content"
    android:lines="1"
/>

<yourpackagename.MyButton 
    android:id="@+id/button"
    android:layout_width="100dp"
    android:layout_height="match_parent"
/>

</LinearLayout>

Is simpler to understand with code, but maybe you have to adapt it.

XML:

<LinearLayout android:id="@+id/table_cell"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>

<TextView android:id="@+id/label"
    android:textSize="19dp"
    android:textStyle="bold"
    android:layout_width="100dp"
    android:layout_height="wrap_content"
    android:lines="1"
/>

<yourpackagename.MyButton 
    android:id="@+id/button1"
    android:layout_width="12dp"
    android:layout_height="match_parent"
/>
<yourpackagename.MyButton 
    android:id="@+id/button2"
    android:layout_width="12dp"
    android:layout_height="match_parent"
/>
<yourpackagename.MyButton 
    android:id="@+id/button3"
    android:layout_width="12dp"
    android:layout_height="match_parent"
/>
<yourpackagename.MyButton 
    android:id="@+id/button4"
    android:layout_width="12dp"
    android:layout_height="match_parent"
/>
<yourpackagename.MyButton 
    android:id="@+id/button5"
    android:layout_width="12dp"
    android:layout_height="match_parent"
/>
<yourpackagename.MyButton 
    android:id="@+id/button6"
    android:layout_width="12dp"
    android:layout_height="match_parent"
/>
<yourpackagename.MyButton 
    android:id="@+id/button7"
    android:layout_width="12dp"
    android:layout_height="match_parent"
/>
<yourpackagename.MyButton 
    android:id="@+id/button8"
    android:layout_width="12dp"
    android:layout_height="match_parent"
/>
</LinearLayout>

Model class that you pass to the Adapter:

public class MyRowModel
{
    public boolean isButton1Visible;
    public boolean isButton2Visible;
    public boolean isButton3Visible;
    public boolean isButton4Visible;
    public boolean isButton5Visible;
    public boolean isButton6Visible;
    public boolean isButton7Visible;
    public boolean isButton8Visible;
}

ViewHolder:

private class ViewHolder {
    public MyButton b1;
    public MyButton b2;
    public MyButton b3;
    public MyButton b4;
    public MyButton b5;
    public MyButton b6;
    public MyButton b7;
    public MyButton b8;
}

getView method:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    ViewHolder viewHolder;
    if (convertView == null) {
        LayoutInflater inflater = (LayoutInflater) this.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        convertView = inflater.inflate(R.layout.table_cell, parent, false);
        viewHolder = new ViewHolder();
        viewHolder.b1 = (MyButton)convertView.findViewById(R.id.button1);
        viewHolder.b2 = (MyButton)convertView.findViewById(R.id.button2);
        viewHolder.b3 = (MyButton)convertView.findViewById(R.id.button3);
        viewHolder.b4 = (MyButton)convertView.findViewById(R.id.button4);
        viewHolder.b5 = (MyButton)convertView.findViewById(R.id.button5);
        viewHolder.b6 = (MyButton)convertView.findViewById(R.id.button6);
        viewHolder.b7 = (MyButton)convertView.findViewById(R.id.button7);
        viewHolder.b8 = (MyButton)convertView.findViewById(R.id.button8);
        convertView.setTag(viewHolder);
    } else {
        viewHolder = convertView.getTag();
    }
    MyRowModel myRowModel = getItem(position);
    if(myRowModel.isButton1Visible)
    {
        viewHolder.b1.setVisibility(View.VISIBLE);
    }
    else
    {
        viewHolder.b1.setVisibility(View.INVISIBLE);
    }
    if(myRowModel.isButton2Visible)
    {
        viewHolder.b2.setVisibility(View.VISIBLE);
    }
    else
    {
        viewHolder.b2.setVisibility(View.INVISIBLE);
    }
    //and so on
    return convertView;
}
Fabio Felici
  • 2,841
  • 15
  • 21
  • Are you suggesting this as a solution to my problem, or just a way to reorganize my code? I need some conditional logic in getView because not all table rows need the button. – arlomedia Nov 05 '14 at 09:23
  • In this case check this http://stackoverflow.com/questions/4777272/android-listview-with-different-layout-for-each-row – Fabio Felici Nov 05 '14 at 09:26
  • The rows actually contain up to eight buttons in different combinations, so it wouldn't be practical to set up a different XML layout for each combination of buttons. I guess I could include all the buttons in the XML and hide/show them from the code as needed. Are you suggesting that my problem would be solved by defining my buttons in XML? Would that create a new instance of the button for each new instance of the table row? – arlomedia Nov 05 '14 at 09:48
  • Yes it creates a new instance of button for each row. It's often easier than creating them and adding them programatically. – Groco Nov 05 '14 at 09:57
  • If you choose to use the xml with eight buttons and hide/show logic, every row will have eight instance of button. In this case you should use a ViewHolder to avoid calling findViewById every time. – Fabio Felici Nov 05 '14 at 09:59
  • If I follow an approach that creates a new button instance for each row instance, then I will still have a similar problem of knowing which buttons are visible so that I can update the references that other objects hold to these buttons. I'll work on that if creating new button instances for every new row instance is the only reliable solution. – arlomedia Nov 05 '14 at 10:28
  • Aren't you passing an array of objects to the Adapter? The information about buttons visibility should live in that object. – Fabio Felici Nov 05 '14 at 10:38
  • I was referring to my original problem of not knowing which of the many instances of each row is displayed. If I create a different instance of a button for each instance of its row, then one instance of the button will always be displayed, but I won't know which one. – arlomedia Nov 05 '14 at 12:03
  • To clarify, my problem is not with adding buttons to my layout or hiding/showing buttons from one row to another. The problem is when the ListView calls getView() multiple times for a given row position but doesn't display the most recent result from that method. I don't see any pattern to predict when that will happen, and I can't find a way to tell programmatically that it has happened. – arlomedia Nov 05 '14 at 12:19
  • What does "the most recent result" mean? Are you changing the data behind the listView and want to refresh the rows? – Fabio Felici Nov 05 '14 at 13:41
  • I mean that getView() runs multiple times for each row, which is a normal part of the ListView behavior, but sometimes the ListView doesn't display the latest instances of the rows it is creating. My question gives more detail and includes test code that demonstrates the problem. – arlomedia Nov 05 '14 at 17:13
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/64340/discussion-between-gonji-dev-and-arlomedia). – Fabio Felici Nov 05 '14 at 17:17
0

I noticed that if I scroll the ListView after the problem occurs, all the rows redraw with the buttons showing, so apparently Android intends to display the latest view for each row, but it isn't always refreshing the view.

Then I tried to figure out what is causing getView to run repeatedly for the currently visible rows (normally it would only run when new rows come into view). Unfortunately, lots of things that are happening elsewhere in this activity are triggering the ListView to regenerate its views, like a ProgressBar that moves as audio plays, an animation that shortens and lengthens the ListView to show another view next to it, and the buttons inside the table rows updating with different graphics to show the status of different things the app is tracking. I was able to eliminate some of this, for example by checking to see if a button is already in the desired state before updating its state, but I can't eliminate all of it.

Since the most frequent action that triggers getView is updating the audio ProgressBar, I added a line to call invalidateViews() on the ListView whenever I update the ProgressBar. That keeps the ListView refreshed so that the latest views always remain visible and therefore my views always remain visible. When running in the debugger, that slows the app down quite a bit, but when running on a standalone device, the performance change isn't noticeable.

Perhaps a better question to ask at this point is why a ProgressBar that isn't related to the ListView causes the ListView to constantly regenerate its views. If I have time or I run into more problems with this, I'll post that as a separate question.

arlomedia
  • 8,534
  • 5
  • 60
  • 108