ListView with Spinner Inside: 1st Item of the list

2019-07-20 03:15发布

问题:

I had this weird scenario where every time I select a value from the spinner in the ListView's 1st Item, the last ListView'sitem its spinner value is the same as the first item. This will only happen when the total number of ListView items is 5 and above. I commented out the codes and retain just the declarations and it's still happening. Is this a bug in Android?

Clarifications:

  • My ListView's Scroll Listener is empty
  • My spinner's setOnItemSelectedListener is commented out.
  • Android SDK Tool version is 22.6.2
  • Android SDK Platform-Tools is 19.0.1

Here's the adapter code:

@Override
public View getView(final int position, View convertView, final ViewGroup parent) {

    Viewholder v = new Viewholder();
    v.rowView = convertView;
    LayoutInflater inflater = (LayoutInflater) context
            .getSystemService(Context.LAYOUT_INFLATER_SERVICE);

    if (v.rowView == null) {
        v.rowView = inflater.inflate(R.layout.inner_base_header_cutom, parent, false);
        v.spinner = (Spinner) v.rowView.findViewById(R.id.spinner1);
        v.rowView.setTag(v);
    } else {
        v = (Viewholder) v.rowView.getTag();
    }

    return v.rowView;
}

ViewHolder:

class Viewholder{
    View rowView;
    TextView itemPID;
    TextView itemPrice;
    TextView itemItemId;
    Spinner spinner;
    TextView subtotal;
}

XML:

<Spinner
    android:id="@+id/spinner1"
    android:layout_width="100dip"
    android:layout_height="wrap_content"
    android:layout_margin="20dip"
    android:entries="@array/quanitiy"
    android:layout_alignTop="@+id/itemPrice"
    android:layout_toRightOf="@+id/imageDisplay" />

Array:

   <string-array name="quanitiy">
        <item>Quantity</item>
        <item>1</item>
        <item>2</item>
        <item>3</item>
        <item>4</item>
        <item>5</item>
        <item>6</item>
        <item>7</item>
        <item>8</item>
        <item>9</item>
        <item>10</item>
    </string-array>

UPDATE I commented out the code for OnItemClickListner. I updated the code above, still the problem exists. The only thing left is the declarations.

Scenario: If I select 1 at the first item of the spinner [index 0] of the ListView, the last item of ListView's spinner gets also 1 without interaction. When down the listview's items until the last part, that's where I found out that they are both the same. I commented out the codes and just retained the declarations.

回答1:

Ok now I got it. Thanks for the clarifications. Your problem comes from views being recycled. You should take a look at this post to understand recycling better.

The short answer

Basically, when you use the convertView in getView(), you are reusing the view of an item that is no longer visible. That's why its fields (including the spinner, the textview for the price...) are already set to something, and you should set them yourself in getView(). The values to give these textfields and the spinner should depend on the item at the specified position.

The detailed answer

Here's what recycling does (picture taken from the post previously mentioned):

Your problem

Here when you scroll to display a new item (let's say item 8, like the picture), the view used to display it is the same as the one for the first item (item 1). Therefore, when you select something on the spinner of the first item, and then scroll down, you will see the change in the last item too because your getView doesn't change the values of the views that were used for item 1 (but it is supposed to update them!).

Note: The correspondance of the first and the last item in your case is totally by chance. Usually, the view is reused for 2 items that are approximately separated by the number of items in one screen height, as you can see in the picture.

Inline explanation in YOUR code for getView

Viewholder v = new Viewholder(); // why create a new one, while you might reuse the former one?

// here rowView becomes the same as the former item's view, with all the former values
v.rowView = convertView; 

// you don't need this here but only in the case rowView is null, should be in your 'if'
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

if (v.rowView == null) {
    v.rowView = inflater.inflate(R.layout.inner_base_header_cutom, parent,false);
    v.spinner = (Spinner) v.rowView.findViewById(R.id.spinner1);
    v.rowView.setTag(v);
    // here you inflated a new view (nothing reused) 
    // but you didn't initialize anything
} else {
    v = (Viewholder) v.rowView.getTag();
    // here you're reusing the former View and ViewHolder 
    // and you don't change the values
}

The solution

Your not supposed to let the items behave as they please, but you're supposed to tell them what to display when you initialize the item view's content in getView(). For the picture above, this means you need to change what used to be item 1 into item 8.

You should keep the state of each item in your adapter. You probably have a backing list or something to display different elements in your ListView depending on the position, right? Then you should use a similar list (or the same) to store the value selected in the spinner, the value displayed by the text field for the price etc. These should probably correspond to fields of the items in your current list anyway, so you probably already have some place to store them.

On the other hand, when you reuse the convertView in getView(), ensure you initialize the spinner (and text field, and everything in this item view) with the correct values for the item at position.

Solution code (template) for getView

Viewholder v;

if (convertView == null) {
    // create a new holder for the new item view
    v = new Viewholder();
    LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    v.rowView = inflater.inflate(R.layout.inner_base_header_cutom, parent,false);
    v.rowView.setTag(v);

    // populate the new holder's fields
    v.spinner = (Spinner) v.rowView.findViewById(R.id.spinner1);
    v.itemPID = (TextView) v.rowView.findViewById(...); // put the right ID here
    v.itemPrice = (TextView) v.rowView.findViewById(...); // put the right ID here
    v.itemItemId = (TextView) v.rowView.findViewById(...); // put the right ID here
    v.subtotal =  (TextView) v.rowView.findViewById(...); // put the right ID here
} else {
    v = (Viewholder) convertView.getTag();
    // the views of v are already populated here (reused)     
}

/* 
 * Reused or not, the item view needs to be initialized here.
 * Initialize all views contained in v with values that are 
 * meaningful for the item at 'position'.
 */

// for instance:
MyItemClass theItemAtPosition = myBackingList.get(position);
v.subtotal.setText(String.valueOf(theItemAtPosition.getSubtotal()));
v.spinner.setSelection(theItemAtPosition.getQuantity());

// to be continued, you get the idea ;)