Search code examples
androidandroid-listviewandroid-cursoradapter

Row background changes due recyclation. getView() implementation doesn't fix this


I've been looking a lot for guides, tutorials and answers here on stack, but I still can't figure this one out.

I have a custom drawable state. If I click on a row in a ListView, it gets highlighted and updates this information in database. (That a task (habit) has been performed). Everything works great until I start scrolling. I've learnt, that it is required to override the getView() method and probably the getItemViewType() with getViewTypeCount().

How to properly do this? My code for HabitsCursorAdapter with the getView() and other mentioned methods at the moment looks like this.

public class HabitsCursorAdapter extends CursorAdapter {

    private LayoutInflater inflater;
    private Context context;

public static final int PERFORMED = 1;
public static final int NOT_PERFORMED = 0;

    public HabitsCursorAdapter(Context context, Cursor c) {
        super(context, c, 0);
        inflater = LayoutInflater.from(context);
        this.context = context;
    }

    @Override
    public void bindView(View view, Context context, Cursor cursor) {

        String habitName = cursor.getString(cursor
                .getColumnIndexOrThrow(SQLiteHabitHelper.COLUMN_NAME));
        TextView habitNameTV = (TextView) view.findViewById(R.id.habit_name);
        habitNameTV.setText(habitName);


        /*******************   OK   ***********************/
        ImageButton editButton = (ImageButton) view.findViewById(R.id.habit_edit_button);
        Integer _id = cursor.getInt(cursor.getColumnIndexOrThrow(SQLiteHabitHelper.COLUMN_ID));
        editButton.setTag(R.id.EDITED_HABIT_ID, _id);
        view.setTag(_id);

        editButton.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                Intent i = new Intent(HabitsCursorAdapter.this.context,
                        HabitDetailActivity.class);
                i.putExtra("_id", (Integer) v.getTag(R.id.EDITED_HABIT_ID));
                HabitsCursorAdapter.this.context.startActivity(i);
            }
        });

        ImageButton deleteButton = (ImageButton) view
                .findViewById(R.id.habit_history_button); // NEED TO REWRITE TO DELETING BUTTON
        deleteButton.setTag(R.id.EDITED_HABIT_ID, _id);
        deleteButton.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                //itemized due debugging issue
                HabitListActivity hla = ((HabitListActivity) HabitsCursorAdapter.this.context);
                Object o = v.getTag(R.id.EDITED_HABIT_ID);
                Integer i = (Integer) o;
                long l = i.longValue();
                hla.delete(l);
            }
        });
        /*******************   OK   **********************/

        HabitListItemView hv = (HabitListItemView) view;
        hv.setHabitPerformed(
                (1 == cursor.getInt(cursor.getColumnIndexOrThrow(SQLiteHabitHelper.COLUMN_PERFORMED))) ? true : false
                    );
        view.refreshDrawableState();
    }

    @Override
    public View newView(Context context, Cursor c, ViewGroup container) {
        HabitListItemView v = (HabitListItemView) inflater.inflate(
                R.layout.habit_item_layout, container, false);
        bindView(v, context, c);        
        return v;
    }

    @Override
    public int getViewTypeCount() {
        return 2;
    }

    @Override
    public int getItemViewType(int position) {

        Cursor c = (Cursor) getItem(position);

        c.moveToPosition(position);
        int value = c.getInt(c.getColumnIndexOrThrow(SQLiteHabitHelper.COLUMN_PERFORMED));
        if(0 != value){
            return PERFORMED;
        }
        return NOT_PERFORMED;

    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if (!mDataValid) {
            throw new IllegalStateException("this should only be called when the cursor is valid");
        }
        if (!mCursor.moveToPosition(position)) {
            throw new IllegalStateException("couldn't move cursor to position " + position);
        }
        View v;
        if (convertView == null) {
            v = newView(mContext, mCursor, parent);
            return v;
        } else {

            mCursor.moveToPosition(position);

            HabitListItemView hv = (HabitListItemView) convertView;
            int convertViewIsPerformed = hv.getHabitPerformed() ? 1 : 0;
            int actualItemPerformed = getItemViewType(position); 

            if(actualItemPerformed == convertViewIsPerformed){              
                v = convertView;
                bindView(v, mContext, mCursor);
                return v;
            }else{
                v = newView(mContext, mCursor, parent);
                return v;
            }                       
        }
    }
}

And the custom row view is:

public class HabitListItemView extends RelativeLayout {

    private static final int[] STATE_HABIT_PERFORMED = { R.attr.state_habit_performed };

    private boolean performed;

    public HabitListItemView(Context context) {
        this(context, null);
    }

    public HabitListItemView(Context context, AttributeSet attributeSet) {
        super(context, attributeSet);
        loadViews();
    }

    public HabitListItemView(Context context, AttributeSet attributeSet,
            int defStyle) {
        super(context, attributeSet, defStyle);

        loadViews();
    }

    private void loadViews() {

        setBackgroundResource(R.drawable.habit_list_item_background);   

    }

    @Override
    protected int[] onCreateDrawableState(int extraSpace) {

        if (performed) {

            final int[] drawableState = super
                    .onCreateDrawableState(extraSpace + 1);

            mergeDrawableStates(drawableState, STATE_HABIT_PERFORMED);

            return drawableState;
        } else {
            return super.onCreateDrawableState(extraSpace);
        }
    }

    public void setHabitPerformed(boolean performed) {
        this.performed = performed;
        refreshDrawableState();
}

public boolean getHabitPerformed() {
    return performed;
}

}

and the onListItemClick() in ListFragment

    @Override
    public void onListItemClick(ListView l, View v, int position, long id) {

        super.onListItemClick(l, v, position, id);

        HabitListItemView hv = (HabitListItemView) v;

        ContentValues cv = new ContentValues();



        String habitId = v.getTag().toString(); 
        SQLiteDatabase db = dh.getWritableDatabase();
        Cursor cPerfomed = db.query(SQLiteHabitHelper.TABLE_HABIT, 
                new String[]{SQLiteHabitHelper.COLUMN_ID, SQLiteHabitHelper.COLUMN_PERFORMED}, 
                "_id = ?", new String[]{habitId}, null, null, null);
        cPerfomed.moveToFirst();

        int performed = cPerfomed.getInt(cPerfomed.getColumnIndexOrThrow(SQLiteHabitHelper.COLUMN_PERFORMED));
        cPerfomed.close();

        cv.put(SQLiteHabitHelper.COLUMN_ID, (Integer) v.getTag());
        cv.put(SQLiteHabitHelper.COLUMN_PERFORMED, (performed == 0) ? 1 : 0);

        db.update(SQLiteHabitHelper.TABLE_HABIT, cv, "_id = ?", new String[]{habitId});
        db.close();
        hv.setHabitPerformed((performed == 0) ? true : false);


    }

A good thorough explanation on how and when are the overriden methods called would maybe help me resolve the issue. I presume, that newView and bindView are called only by getView, thus I believe I can control that by my overriden getView. However the getItemViewType and getViewTypeCount are called by methods which I did not implement and I do not understand why and how the results are used. Maybe they cause the problems.

If more code is required, I'll edit my post according to requirements.

Any help will be greatly appreciated!


Solution

  • A friend of mine helped me by sending me this link

    Android: CursorAdapter, ListView and CheckBox

    I tried the solution, however, I did not work it out with checkboxes, but with the background color.

    I had to modify getView() method accordingly:

    public View getView(int position, View inView, ViewGroup parent) {
        if (inView == null) {
            LayoutInflater inflater = (LayoutInflater) context
                    .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            inView = inflater.inflate(R.layout.habit_item_layout, null);
        }
    
        HabitListItemView hliv = (HabitListItemView) inView;
    
            mCursor.moveToPosition(position);
        bindView(hliv, mContext, mCursor);
        hliv.setHabitPerformed(habitPerformed.get(position)); //line added
    
        return inView;
    }
    

    and add to the adapter an array, where I store, if it should have a background green or not.

    public ArrayList<Boolean> habitPerformed = new ArrayList<Boolean>();
    

    and in the fragment with the list in the method onListItemClick() I added code which modified the array accordingly.

        if (hv.getHabitPerformed()) {
            adapter.habitPerformed.set(position, true);
        } else if (!hv.getHabitPerformed()) {
            adapter.habitPerformed.set(position, false);
        }
    

    This solved my problem.