2

I need to display a sort of visitor logs in a RecyclerView and the data to display comes from a SQLiteDatabase DESC-ordered by DateTime (column enter_time, UNIX value). I have already implemented a simple CursorAdapter and I'm able to display data in the RecyclerView.

Next step is to make different views so that the first entry of every day uses a layout with an additional header with the date:

enter image description here

Inside the CursorAdapter I implemented getItemViewType(int) method where I need to add my logic selecting the right item type.

The logic is simple:

  • If next item in Cursor has same date as the current, then select the simple layout
  • If next item in Cursor has different date as the current, then select the layout with header

Here comes the problem: I need to check next item in the Cursor of the CursorAdapter but the displayed result is wrong and I can't tell why. Code looks correct to me but layout are assigned to items randomly.

public static class VisitorsRecyclerAdapter extends RecyclerView.Adapter<VisitorsRecyclerAdapter.VisitorViewHolder> {

    CustomCursorAdapter mCursorAdapter;
    Context mContext;
    private static final int NO_HEADER_LAYOUT = 1;
    private static final int HEADER_LAYOUT = 0;

    public VisitorsRecyclerAdapter(Context context, Cursor c) {
        mContext = context;
        mCursorAdapter = new CustomCursorAdapter(mContext, c, 0);
    }

    public static class VisitorViewHolder extends RecyclerView.ViewHolder {

        public TextView textName;
        public TextView textCats;

        public VisitorViewHolder(View v) {
            super(v);
            textName = (TextView) v.findViewById(R.id.item_visitor_name);
            textCats = (TextView) v.findViewById(R.id.item_visitor_category);
        }
    }

    public static class HeaderViewHolder extends RecyclerView.ViewHolder {

        public TextView textName;
        public TextView textCats;

        public HeaderViewHolder(View v) {
            super(v);
            textName = (TextView) v.findViewById(R.id.item_visitor_name);
            textCats = (TextView) v.findViewById(R.id.item_visitor_category);
        }
    }

    private class CustomCursorAdapter extends CursorAdapter {


        public CustomCursorAdapter(Context context, Cursor c, int flags) {
            super(context, c, flags);
        }

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

        @Override
        public int getItemViewType(int position) {

            Cursor cursor = (Cursor) mCursorAdapter.getItem(position);
            cursor.moveToNext();

            // date of current item
            Date date0 = new Date(Long.parseLong(cursor.getString(cursor.getColumnIndex(DatabaseConst.VisitorEntry.COLUMN_NAME_ENTER))) * 1000);

            if(position == -1) return HEADER_LAYOUT;
            cursor = (Cursor) mCursorAdapter.getItem(position - 1);
            cursor.moveToNext();

            // date of item that temporary comes after
            Date date1 = new Date(Long.parseLong(cursor.getString(cursor.getColumnIndex(DatabaseConst.VisitorEntry.COLUMN_NAME_ENTER))) * 1000);

            SimpleDateFormat format = new SimpleDateFormat("ddMMyyyy", Locale.ENGLISH);

            return format.format(date0).equals(format.format(date1)) ? NO_HEADER_LAYOUT : HEADER_LAYOUT;
        }

        @Override
        public View newView(final Context context, Cursor cursor, ViewGroup parent) {
            View v = null;
            int position = cursor.getPosition();
            int type = getItemViewType(position);
            RecyclerView.ViewHolder viewHolder = null;

            switch (type) {
                case HEADER_LAYOUT:
                    v = LayoutInflater.from(parent.getContext())
                            .inflate(R.layout.item_day, parent, false);
                    viewHolder = new HeaderViewHolder(v);
                    break;
                case NO_HEADER_LAYOUT:
                    v = LayoutInflater.from(parent.getContext())
                            .inflate(R.layout.item_visitor, parent, false);
                    viewHolder = new VisitorViewHolder(v);
                    break;
            }
            assert v != null;
            v.setTag(viewHolder);
            return v;
        }

        @Override
        public void bindView(View view, Context context, Cursor cursor) {
            final int viewType = getItemViewType(cursor.getPosition());

            switch (viewType) {
                case HEADER_LAYOUT:
                    HeaderViewHolder holder = (HeaderViewHolder) view.getTag();
                    holder.textName.setText(DateFormat.getDateTimeInstance().format(new Date(Long.parseLong(cursor.getString(cursor.getColumnIndex(DatabaseConst.VisitorEntry.COLUMN_NAME_ENTER))) * 1000)));
                    holder.textCats.setText(cursor.getString(cursor.getColumnIndex(DatabaseConst.VisitorEntry.COLUMN_NAME_FIRST_NAME)));
                    break;
                case NO_HEADER_LAYOUT:
                    VisitorViewHolder holder0 = (VisitorViewHolder) view.getTag();
                    holder0.textName.setText(DateFormat.getDateTimeInstance().format(new Date(Long.parseLong(cursor.getString(cursor.getColumnIndex(DatabaseConst.VisitorEntry.COLUMN_NAME_ENTER))) * 1000)));
                    holder0.textCats.setText(cursor.getString(cursor.getColumnIndex(DatabaseConst.VisitorEntry.COLUMN_NAME_FIRST_NAME)));
                    break;
            }

        }

    }

    @Override
    public int getItemCount() {
        if(mCursorAdapter == null) return 0;

        return mCursorAdapter.getCount();
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        mCursorAdapter.getCursor().moveToPosition(position);
        mCursorAdapter.bindView(holder.itemView, mContext, mCursorAdapter.getCursor());
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v = mCursorAdapter.newView(mContext, mCursorAdapter.getCursor(), parent);
        RecyclerView.ViewHolder viewHolder = null;
        switch (viewType){
            case HEADER_LAYOUT:
                viewHolder = new HeaderViewHolder(v);
                break;
            case NO_HEADER_LAYOUT:
                viewHolder = new VisitorViewHolder(v);
                break;
        }

        return viewHolder;
    }
}

This is the actual wrong result:

enter image description here

I looked over SO, but I can't find similar scenario where it's needed to recover data from adjacent items (I'm not event sure is this the problem). What would you suggest to fix this issue?

fillobotto
  • 3,698
  • 5
  • 34
  • 58

2 Answers2

1

It seems quite right till you returned value from getItemViewType. You have created only single ViewHolder that is VisitorViewHolder. But there is another better way to to accomplish this, Create multiple ViewHolder class and Switch them in onCreateViewHolder.

Modify like this,

@Override
    public VisitorViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

    switch (viewType){
       case HEADER_LAYOUT:
        View v = mCursorAdapter.newView(mContext, mCursorAdapter.getCursor(), parent);
        final VisitorViewHolder holder = new VisitorViewHolder(v);    
        return holder;

       case NO_HEADER_LAYOUT:
              //Return you another viewholder which contains view with no header
      }
 }

See here for more reference, That will make your task even more easier.

Community
  • 1
  • 1
Shree Krishna
  • 8,474
  • 6
  • 40
  • 68
  • Please look edited code, when I cast `view.getTag()` in `bindView()` it's trying to convert to the wrong ViewHolder type, like if the view is being recycled and its tag is now belonging to a different item – fillobotto Apr 20 '16 at 19:46
  • More: Happens only when rendering items out of view after a scroll down – fillobotto Apr 20 '16 at 19:53
  • @fillobotto when does it happens, After you start scrolling or in initialization phase too ? You have to cast it in `onBindViewHolder` not in `bindView`. And I think you can also try removing `newView` override method – Shree Krishna Apr 21 '16 at 00:27
  • Found out the problem. In `getItemViewType` method I'm shifting the position of the cursor and this makes happen items duplications and so on, just need to restore the correct position after my check. Going to post the solution as soon as possible. – fillobotto Apr 21 '16 at 00:38
  • 1
    Sure, I just can't post frommy mobile. I upvoted your post since using different ViewHolders is an elegant approach but I'll post the effective solution to my problem – fillobotto Apr 21 '16 at 08:46
1

Two different problems were occurring.

Item recycling

The adapter was recycling views that were inflated for a different item type, hence an exception occurred while binding since the view.getTag() contained a wrong ViewHolder type. To fix it, I set StableIds to false.

    @Override
    public void setHasStableIds(boolean hasStableIds) {
        super.setHasStableIds(false);
    }

Type assignment logic

Logically, was correct, but I was tweaking the position of the cursor without setting it to the previous value after having finished.

        @Override
        public int getItemViewType(int pos) {
            Integer position = pos;
            // with -1 position you can return whatever you want
            if(position.equals(-1)) { return HEADER_LAYOUT; }

            Cursor cursor = mCursorAdapter.getCursor();
            if(!position.equals(0)) {
                cursor.moveToPosition(position);
                Date date0 = new Date(Long.parseLong(cursor.getString(cursor.getColumnIndex(DatabaseConst.VisitorEntry.COLUMN_NAME_ENTER))) * 1000);

                cursor.moveToPosition(position - 1);
                Date date1 = new Date(Long.parseLong(cursor.getString(cursor.getColumnIndex(DatabaseConst.VisitorEntry.COLUMN_NAME_ENTER))) * 1000);

                SimpleDateFormat format = new SimpleDateFormat("ddMMyyyy", Locale.ENGLISH);

                cursor.moveToPosition(position);
                return ((format.format(date0).equals(format.format(date1))) ? NO_HEADER_LAYOUT : HEADER_LAYOUT);
            } else {
                // position = 0 -> first item in list always need header
                cursor.moveToPosition(position); // important!
                return HEADER_LAYOUT;
            }
        }

As you can see, before returning from the method, I'm again setting the cursor position.

fillobotto
  • 3,698
  • 5
  • 34
  • 58