Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Setting the background color of ListView items on Button click

I have a FragmentActivity that can contain as many ListFragments as a user wants. The ListFragments are laid out side-by-side (upon choosing an option in the ActionBar) inside a HorizontalScrollView. Each ListFragment item contains a TextView and a Button. Clicking on the Button changes the background color of the Button's ViewParent - a LinearLayout containing the Button and the TextView.

Now, clicking the Button in each ListFragment changes the background color of its corresponding list item, however upon scrolling buttons in list items that were not clicked also have their background color changed. This behavior is unexpected as only the items that were clicked are required to have their background color changed by a corresponding click on its Button.

The data for the ListFragment comes from a custom SimpleAdapter that overrides the getView(int position, View convertView, ViewGroup parent) method where I attach a setOnClickListener to a Button, which changes the background color of its ViewParent when clicked.

I have spent many hours on this issue however I am unable to arrive at a solution. An explanation that helps understand the root cause of this behaviour would be really useful. Also any guidance to help set the background color of just one item when clicked is welcome.

Here is the code for my custom Adapter MyCustomAdapter that extends SimpleAdapter:

package com.example.androidlistfragmenttest;

import java.util.List;
import java.util.Map;
import android.content.Context;
import android.graphics.Color;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.SimpleAdapter;

public class MyAdapter extends SimpleAdapter{

Context context;

public MyAdapter(Context context, List<? extends Map<String, ?>> data,
        int resource, String[] from, int[] to) {
    super(context, data, resource, from, to);

    this.context = context;
}

@Override
public boolean areAllItemsEnabled() {
    return false;
}

@Override
public boolean isEnabled(int position) {
   return false;
}

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

    View view = convertView;
    if(view == null){

        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        view = inflater.inflate(R.layout.fragment_layout, null);
        Button button = (Button) view.findViewById(R.id.button);

        button.setOnClickListener(new View.OnClickListener(){

            @Override
            public void onClick(View v) {
                Log.v("GOJIRA","ATOMIC BREATH");
                // Changes parent view's background color
                View parent = (View) v.getParent();
                parent.setBackgroundColor(Color.GREEN);
            }   
        });

    }
    return view;
}


}

And the ListFragment:

package com.example.androidlistfragmenttest;

import java.util.ArrayList;
import java.util.HashMap;
import android.os.Bundle;
import android.support.v4.app.ListFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class MyFragment extends ListFragment {

private ArrayList<HashMap<String,String>> arraylist;


@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                         Bundle savedInstanceState) {

    // Inflate the layout for this fragment

    View view = inflater.inflate(R.layout.fragment_layout, container, false);
    return view;
}

@Override
public void onActivityCreated(Bundle savedInstanceState){

    super.onActivityCreated(savedInstanceState);
    arraylist = dataGenerator();
    MyAdapter adapter = new MyAdapter(getActivity().getApplicationContext(), arraylist, R.layout.fragment_layout,new String[]{"KEY"},new int[]{R.id.text_id});
    setListAdapter(adapter);

}
/*
 * Method to populate an adapter's data list.
 */
public ArrayList<HashMap<String,String>> dataGenerator(){

    HashMap<String,String> hashMap1 = new HashMap<String,String>();
    hashMap1.put("KEY", "A");

    HashMap<String,String> hashMap2 = new HashMap<String,String>();
    hashMap2.put("KEY", "B");

    HashMap<String,String> hashMap3 = new HashMap<String,String>();
    hashMap3.put("KEY", "C");

    HashMap<String,String> hashMap4 = new HashMap<String,String>();
    hashMap4.put("KEY", "D");

    HashMap<String,String> hashMap5 = new HashMap<String,String>();
    hashMap5.put("KEY", "E");

    ArrayList<HashMap<String,String>> arraylist = new ArrayList<HashMap<String,String>>();
    arraylist.add(hashMap1);
    arraylist.add(hashMap2);
    arraylist.add(hashMap3);
    arraylist.add(hashMap4);
    arraylist.add(hashMap5);


    return arraylist;
}

}

As well as the ListActivity:

package com.example.androidlistfragmenttest;

import java.util.Stack;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction; 
import android.support.v4.app.NavUtils;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;

public class MainActivity extends FragmentActivity {


private Stack<String> tagStack;
private Integer last_tag_number;

public MainActivity(){

    last_tag_number = new Integer("0");
    tagStack = new Stack<String>();
}

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the menu; this adds items to the action bar if it is present.
    getMenuInflater().inflate(R.menu.main, menu);
    return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {

    case R.id.add_fragment:
        addColumn();
        return true;


    case R.id.remove_column:
        removeColumn();
        return true;


    case android.R.id.home:
        // This ID represents the Home or Up button. In the case of this
        // activity, the Up button is shown. Use NavUtils to allow users
        // to navigate up one level in the application structure. For
        // more details, see the Navigation pattern on Android Design:
        //
        // http://developer.android.com/design/patterns/navigation.html#up-vs-back
        //
        NavUtils.navigateUpFromSameTask(this);
        return true;

    }
    return super.onOptionsItemSelected(item);
}

/*
 * This function adds a column pane to the screen
 *  
 */
public void addColumn(){

    FragmentManager fragmentManager = getSupportFragmentManager();
    FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

    MyFragment fragment = new MyFragment();
    fragmentTransaction.add(R.id.fragment_activity, fragment,tagGenerator());
    fragmentTransaction.commit();

}

/*This function removes a column pane from the screen
 * 
 * 
 */

public void removeColumn(){

    if(tagStack.size() != 0){
        FragmentManager fragmentManager = getSupportFragmentManager();
        Fragment fragment = fragmentManager.findFragmentByTag(tagStack.pop());
        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        fragmentTransaction.remove(fragment);
        fragmentTransaction.commit();
    }
}

/*
 * This function generates tags for each fragment that is displayed on the screen
 * The tags pose as unique identifiers for each fragment
 */
public String tagGenerator(){

    Integer tag_number; 

    if(last_tag_number.intValue() == 0){
        tag_number = last_tag_number;   
        int temp = last_tag_number.intValue();
        temp+=1;
        last_tag_number = Integer.valueOf(temp);
    }
    else{
        tag_number = new Integer(last_tag_number.intValue());
        int temp = last_tag_number.intValue();
        temp+=1;
        last_tag_number = Integer.valueOf(temp);
    }
    String tag = tag_number.toString();
    tagStack.push(tag);

    return tag;
}

}

And finally the layouts. For the ListFragment:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="300dp"
    android:layout_height="match_parent"
    android:orientation="vertical" 
    android:layout_weight="1"
    android:layout_margin="5dp" >

   <ListView android:id="@id/android:list"
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
   /> 

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/label" 
        android:layout_gravity="start"
    />

    <TextView 
        android:id="@+id/text_id"
        android:layout_width="0dp"
        android:layout_height="wrap_content" 
        android:layout_weight="1"
        android:layout_gravity="end"
    />

     <Button
         android:id="@+id/button"
         android:layout_height="wrap_content"
         android:layout_width="wrap_content"
         android:text="@string/button"
         android:layout_margin="2dp"
         android:layout_marginLeft="2dp"
         android:layout_marginRight="2dp"
         android:clickable="true"

      />


</LinearLayout>   

And for the FragmentActivity:

<?xml version="1.0" encoding="utf-8"?>

<HorizontalScrollView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

<LinearLayout  
     android:id="@+id/fragment_activity"
     android:layout_width="fill_parent" 
     android:layout_height = "fill_parent"
     android:orientation = "horizontal"
     android:gravity="center"       
 > 

</LinearLayout> 
</HorizontalScrollView>
like image 616
user1841702 Avatar asked Sep 30 '22 16:09

user1841702


2 Answers

Answering my question.

The devil is in the detail. convertView is the View that is recycled. Any code within the if(view == null) block of the overridden getView(int position, View convertView, ViewGroup parent) will be recycled as well. Moving the Button and its associated listener outside this block will provide access to absolute list item positions instead of recycled positions. These positions can then be stored and the corresponding list item background color / state can be maintained. Here is my custom adapter's getView method:

Edit: This solution is based on @matiash's suggestion who has also provided an answer to this question.

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

    //convertView is the recycled view.

    View view = convertView;
    final int pos = position;

    /*  Views are recycled within this block. 
     *  Only recycled relative list item positions accessible here.
     */

    if(view == null){

        final View viewClick = view;

        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        view = inflater.inflate(R.layout.fragment_layout, null);  

    }

   /*   
    *  Button moved out of if(view == null) block. 
    *  Views are not recycled here. Absolute list item positions accessible.
    *  Absolute list positions saved in an ArrayList (coloredItems).
    */  

    Button button = (Button) view.findViewById(R.id.button);
    button.setOnClickListener(new View.OnClickListener(){


        @Override
        public void onClick(View v) {

            //This is always a refreshed position and never an absolute list item position.
            Log.d("CLICKED POSITION",Integer.valueOf(pos).toString());
            View parent = (View) v.getParent();

            parent.setBackgroundColor(Color.GREEN);
            coloredItems.add(Integer.valueOf(pos));             
        }
    });

     /*
      * This ensures that only list item positions saved 
      * in the ArrayList 'coloredItems' have a green background.
      * All other list items have a transparent background.
      */

    if(coloredItems.contains(Integer.valueOf(position)))
        view.setBackgroundColor(Color.GREEN);
    else
        view.setBackgroundColor(Color.TRANSPARENT);

    return view;
}
like image 85
user1841702 Avatar answered Oct 09 '22 14:10

user1841702


This is a common pitfall with ListView view reuse.

When you scroll on a ListView or GridView, views what exit the screen are placed in a recycler. Whenever a new row needs to be created, a recycled view will be used if there is one available. This is intended to make scrolling faster, since creating views from scratch is expensive. However, it may lead to subtle bugs if you're not careful.

If you change the state (i.e. background color) of one of the rows, and that row is reused, it will keep the state (i.e. a green background). Also, when the original row is redrawn, it may recycle a different view, and it won't be green either.

What you need to do is to store the information about which rows should and should not have a green background. You can use the ViewHolder pattern for this, or some other method (such as a HashSet, &c).

Then, your getView() method should be something like this:

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

    View view = convertView;
    if(view == null)
    {
        <Initialize a new row, same as before>

        button.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View v) {
                <change background color>
                <Store that row = position must have a green background>
            }   
        });

    }

    if <this position should be green>
        parent.setBackgroundColor(Color.GREEN);
    else
        parent.setBackgroundColor(Color.TRANSPARENT); // or whatever the original color was.

    return view;
}
like image 30
matiash Avatar answered Oct 09 '22 13:10

matiash