androidandroid-listviewandroid-cursoradapter

Date separators while scrolling in ListView


In a ListView (that uses a custom CursorAdapter), I am trying to display a separator solely on the basis of whether the date between two ListView items changes, as so: as so .

Unfortunately, the way I have my code set up, as I scroll down, pause for a second, and then start scrolling again, I have the separator turning up at the wrong location (such as here, see the disappearance of the separator in the second item in the ListView): see the disappearance of the second item

Or, it might appear in the third item.

I have encountered two main issues:

  1. The appearance and disappearance of the date separator at various times depending on a pause in scrolling, and
    1. the appearance of the date separator as one is scrolling upwards continuously at the wrong points.

I understand in principle that these issues arise because the behaviour of lastDate in my adapter really only takes into account unidirectional downward scrolling, and in addition are caused by how ListView recycles views and containers.

My question, essentially: how can I rework my custom CursorAdapter in MyAdapter.java or how I store my data in MainFragment.java so that I can display the separater appropriately regardless of the direction of the scroll, or momentary pauses and then rescrolls?

Thank you so much in advance!

Below is the MainFragment's code, the adapter's code, as well as the XML for the ListView items' layout.

MyAdapter.java

package com.cpsashta.seplistview;

import android.content.Context;
import android.database.Cursor;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import android.widget.ImageView;
import android.widget.TextView;

import com.cpsashta.seplistview.DbContract.DbEntry;

public class MyAdapter extends CursorAdapter {

    private LayoutInflater cursorInflater;
    private Context mContext;
    private Cursor mCursor;

    private String lastDate;

    TextView tvSep;
    TextView tvName;
    TextView tvTitle;
    TextView tvDate;
    TextView tvExp;

    public MyAdapter(Context context, Cursor c, int flags) {
        super(context,c,flags);
        this.mContext = context;
        this.mCursor = c;
        cursorInflater = LayoutInflater.from(context);
    }

    @Override
    public View newView(Context context, Cursor cursor, ViewGroup parent) {
        return cursorInflater.inflate(R.layout.list_layout_1, parent, false);

    }

    @Override
    public void bindView(View view, Context context, Cursor c) {
        tvTitle = (TextView) view.findViewById(R.id.tv_title);
        tvName = (TextView) view.findViewById(R.id.tv_name);
        tvSep = (TextView) view.findViewById(R.id.separator);
        tvDate = (TextView) view.findViewById(R.id.tv_date);
        tvExp = (TextView) view.findViewById(R.id.is_expanded_view);

        String title = c.getString(c.getColumnIndex(DbEntry.COLNAME_TITLE));
        String name = c.getString(c.getColumnIndex(DbEntry.COLNAME_NAME));
        String date = c.getString(c.getColumnIndex(DbEntry.COLNAME_DATE));

        tvTitle.setText(title);
        tvName.setText(name);
        tvDate.setText(date);
        if (!date.equals(lastDate) || c.getPosition() == 0) {
            tvSep.setVisibility(View.VISIBLE);
            tvSep.setText(date);
            tvExp.setText("true");
            lastDate = date;
        } else {
            tvSep.setVisibility(View.GONE);
            tvExp.setText("false");
        }
    }
}

list_layout_1.xml

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

    <TextView
        android:id="@+id/separator"
        style="?android:attr/listSeparatorTextViewStyle"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:textColor="@android:color/white"
        android:background="#FF0000"
        android:visibility="gone"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="gone"
        android:id="@+id/is_expanded_view"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceMedium"
        android:text="Medium Text"
        android:id="@+id/tv_title"
        android:layout_gravity="center_horizontal" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/tv_date"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceSmall"
        android:text="Small Text"
        android:id="@+id/tv_name"
        android:layout_gravity="right" />
</LinearLayout>

MainFragment.java

package com.cpsashta.seplistview;

import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.support.v4.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ListView;

import com.cpsashta.seplistview.DbContract.*;




public class MainFragment extends Fragment {

    String[] names = {"Dick", "Sammy", "Manuel", "Jenny", "Kirk", "Shimmy"};
    public static String titleSmile = "Damned crazy folks, Smile, you're on camera!";
    String[] titles = {"Here we go again!", titleSmile};
    String dateMonth = "Aug";

    DbHelper mDbHelper;
    SQLiteDatabase db;
    Button button;

    public MainFragment() {
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View rootView = inflater.inflate(R.layout.fragment_main, container, false);

        button = (Button) rootView.findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                LinearLayout ll = (LinearLayout) v.getParent();
                addOneItem(ll);
            }
        });

        return rootView;

    }


    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        ListView lv = (ListView) view.findViewById(R.id.listview);
        Cursor c = getAll();
        MyAdapter adapter = new MyAdapter(getActivity(),c,0);
        lv.setAdapter(adapter);

    }

    //Occurs on button press, the view pressed is intended to be the rootView (a.k.a. the LinearLayout housing the ListView and button)
    public void addOneItem(View view) {
        newItemToDb();
        ListView lv = (ListView) view.findViewById(R.id.listview);
        MyAdapter adapter = (MyAdapter) lv.getAdapter();
        adapter.notifyDataSetChanged();
    }

    //Adds random name, title, and date to the database
    public void newItemToDb() {
        if (db == null || !db.isOpen()) {
            if (mDbHelper == null) {
                mDbHelper = new DbHelper(getActivity());
            }
            db = mDbHelper.getWritableDatabase();
        }

        ContentValues values = new ContentValues();
        int nameVal = (int)(Math.random() * names.length);
        values.put(DbEntry.COLNAME_NAME, names[nameVal]);

        int titleVal = (int) (Math.random() * titles.length);
        values.put(DbEntry.COLNAME_TITLE, titles[titleVal]);

        //Chooses a date from August 1 to August 10
        //int dateDay = (int)(Math.random() * 10 + 1);
        //FOR NOW choosing the date of Aug 1 or 2 only
        int dateDay = (int)(Math.random() * 2 + 1);
        String date = dateMonth + " " + dateDay;
        values.put(DbEntry.COLNAME_DATE, date);


        db.insert(DbEntry.TABLE_NAME, null, values);
        db.close();
    }

    public Cursor getAll() {
        if (db == null || !db.isOpen()) {
            if (mDbHelper == null) {
                mDbHelper = new DbHelper(getActivity());
            }
            db = mDbHelper.getWritableDatabase();
        }

       String[] projection = { DbEntry._ID,
               DbEntry.COLNAME_TITLE,
               DbEntry.COLNAME_NAME,
               DbEntry.COLNAME_DATE
       };

        String sortOrder = null;

        //TODO delete the sortOrder below
        //String sortOrder = RecordingEntry.COLNAME_TAG_FULL
        //        + " DESC";
        String[] selectionArgs = null;
        String selection = null; // Setting this to null

        Cursor c = db.query(DbEntry.TABLE_NAME,
                projection,
                selection,
                selectionArgs,
                null, // don't group the rows
                null, // don't filter by row groups
                sortOrder
        );

        return c;
    }

}

EDIT: The solution I've put into use, based on @N.T.'s suggestion, replaces the end of the bindView section of MainFragment with the following:

        String lastDate = "";
        int position = c.getPosition();
        if (position != 0 ) {
            c.moveToPosition(position - 1);
            lastDate = c.getString(c.getColumnIndex(DbEntry.COLNAME_DATE));
            c.moveToPosition(position);
        }
        if (position == 0 || !date.equals(lastDate)) {
            separator.setVisibility(View.VISIBLE);
            separator.setText(date);
        } else {
            separator.setVisibility(View.GONE);
        }

Solution

  • You cannot control the order in which the newView/bindView are called. Let's say you are displaying items 10-20 on screen and the last one drawn is the one form position 20, therefore your lastDate will point to that. Now you scroll up and the system will ask you to draw item#9. At this point you do comparisons between item#9 and lastDate that points to item#20, which is not what you wanted.

    To fix things you need to move your cursor to the previous position (mCursor.moveToPosition(position - 1)) and check your current item against the previous item in the list, not against the previously requested one to be drawn.