Search code examples
androidlistviewandroid-cursoradapterandroid-viewholder

How to hide one view within a layout in an Android ListView backed by database cursor adapter


The goal of the sample application is to display items from a SQLite database, but hide the second text view if the database record has a hide flag active (otherwise display the second text view).

The problem is that it doesn't hide the right things. And as scroll actions cause items to go out of view, and back into view, the second text view gets hidden and and shown on various list items in an erratic manner.

The hidden flag has been set on items 5, 10, 15, 20, and here is how it comes-up:initial page 6 unexpectedly hiddenseveral unexpectedly hidden itemsmore unexpectedly hidden images Scrolling down, various other strange items are hidden, and it doesn't seem to be the same each time. Entry 14, Entry 16, are hidden, for instance.

After Scrolling to the top, we see the first set of items no longer has the same hidden second lines.

after scrolling to the top, different things are hidden

Then a whole new set of entries are hidden scrolling back and forth. Not quite random, but inexplicable. You've got to see it to believe it.

The 'real' application that this sample is based-upon (not shown here) actually is attempting to show and hide an ImageView, but the same kind of problem surrounds hiding a TextView, so that's what's I've shown here.

Below is the application. Everything you need should be included (including sample data), should you wish to run this crazy thing. Or you can find it on github: https://github.com/sengsational/LvCaApp

LvCaActivity.java:

public class LvCaActivity extends AppCompatActivity {

    private SimpleCursorAdapter dataAdapter;
    private DbAdapter dbHelper;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_lv_ca);

        dbHelper = new DbAdapter(this);
        dbHelper.open();
        dbHelper.deleteAll();
        dbHelper.insertSome();

        Cursor bCursor = dbHelper.fetchAll(DbAdapter.bColumns);

        dataAdapter = new MySimpleCursorAdapter(
                this, R.layout.b_item,
                bCursor,
                DbAdapter.bColumns,
                ViewHolder.viewsArray,
                0);

        ListView listView = (ListView) findViewById(R.id.listView1);

        listView.setAdapter(dataAdapter);

    }
}

activity_lv_ca.xml:

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

    <ListView android:id="@+id/listView1" android:layout_width="fill_parent"
              android:layout_height="fill_parent" />

</LinearLayout>

b_item.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                android:padding="6dip"
                android:id="@+id/b_item_layout">

        <TextView
            android:id="@+id/bName"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_alignParentTop="true"
            android:textAppearance="?android:attr/textAppearanceListItem"
            android:ellipsize="end"
            android:singleLine="true"
            android:paddingTop="30dp"/>

        <TextView
            android:id="@+id/bSecondLine"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@+id/bName"
            android:textAppearance="?android:attr/textAppearanceSmall" />

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

</RelativeLayout>

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.company.cpp.lvcaapp"
          xmlns:android="http://schemas.android.com/apk/res/android">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".LvCaActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>

</manifest>

DbAdapter.java:

public class DbAdapter {

    private static final String TAG = "DbAdapter";
    private DatabaseHelper mDbHelper;
    private SQLiteDatabase mDb;

    private static final String DATABASE_NAME = "adbname";
    private static final String SQLITE_TABLE = "atablename";
    private static final int DATABASE_VERSION = 1;

    private final Context mCtx;

    public static final String[] bColumns = new String[] {
            "_id",
            "NAME",
            "SECOND_LINE",
            "HIDDEN",
    };

    private static final String DATABASE_CREATE =
            "CREATE TABLE if not exists " + SQLITE_TABLE + " (" +
                    "_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
                    "NAME TEXT, " +
                    "SECOND_LINE, " +
                    "HIDDEN" +
                    ");";

    private static class DatabaseHelper extends SQLiteOpenHelper {
        DatabaseHelper(Context context) {
            super(context, DATABASE_NAME, null, DATABASE_VERSION);
        }

        @Override
        public void onCreate(SQLiteDatabase db) {
            Log.w(TAG, DATABASE_CREATE);
            db.execSQL(DATABASE_CREATE);
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
                    + newVersion + ", which will destroy all old data");
            db.execSQL("DROP TABLE IF EXISTS " + SQLITE_TABLE);
            onCreate(db);
        }
    }

    public DbAdapter(Context ctx) {
        this.mCtx = ctx;
    }

    public DbAdapter open() throws SQLException {
        mDbHelper = new DatabaseHelper(mCtx);
        mDb = mDbHelper.getWritableDatabase();
        return this;
    }

    public void close() {
        if (mDbHelper != null) {
            mDbHelper.close();
        }
    }
    public Cursor fetchAll(String[] fields) {
        Cursor mCursor = mDb.query(SQLITE_TABLE, fields, null, null, null, null, null);
        if (mCursor != null) {
            mCursor.moveToFirst();
        }
        return mCursor;
    }

    public void insertSome() {
        AListItem.getInstance();
        String sampleData = "[{\"name\":\"Entry 1\",\"second_line\":\"Second Line 1\",\"hidden\":\"F\"},{\"name\":\"Entry 2\",\"second_line\":\"Second Line 2\",\"hidden\":\"F\"},{\"name\":\"Entry 3\",\"second_line\":\"Second Line 3\",\"hidden\":\"F\"},{\"name\":\"Entry 4\",\"second_line\":\"Second Line 4\",\"hidden\":\"F\"},{\"name\":\"EntryH 5\",\"second_line\":\"Second Line 5\",\"hidden\":\"T\"},{\"name\":\"Entry 6\",\"second_line\":\"Second Line 6\",\"hidden\":\"F\"},{\"name\":\"Entry 7\",\"second_line\":\"Second Line 7\",\"hidden\":\"F\"},{\"name\":\"Entry 8\",\"second_line\":\"Second Line 8\",\"hidden\":\"F\"},{\"name\":\"Entry 9\",\"second_line\":\"Second Line 9\",\"hidden\":\"F\"},{\"name\":\"EntryH 10\",\"second_line\":\"Second Line 10\",\"hidden\":\"T\"},{\"name\":\"Entry 11\",\"second_line\":\"Second Line 11\",\"hidden\":\"F\"},{\"name\":\"Entry 12\",\"second_line\":\"Second Line 12\",\"hidden\":\"F\"},{\"name\":\"Entry 13\",\"second_line\":\"Second Line 13\",\"hidden\":\"F\"},{\"name\":\"Entry 14\",\"second_line\":\"Second Line 14\",\"hidden\":\"F\"},{\"name\":\"EntryH 15\",\"second_line\":\"Second Line 15\",\"hidden\":\"T\"},{\"name\":\"Entry 16\",\"second_line\":\"Second Line 16\",\"hidden\":\"F\"},{\"name\":\"Entry 17\",\"second_line\":\"Second Line 17\",\"hidden\":\"F\"},{\"name\":\"Entry 18\",\"second_line\":\"Second Line 18\",\"hidden\":\"F\"},{\"name\":\"Entry 19\",\"second_line\":\"Second Line 19\",\"hidden\":\"F\"},{\"name\":\"EntryH 20\",\"second_line\":\"Second Line 20\",\"hidden\":\"T\"},{\"name\":\"Entry 21\",\"second_line\":\"Second Line 21\",\"hidden\":\"F\"},{\"name\":\"Entry 22\",\"second_line\":\"Second Line 22\",\"hidden\":\"F\"},{\"name\":\"Entry 23\",\"second_line\":\"Second Line 23\",\"hidden\":\"F\"}]";
        String[] items = sampleData.split("\\},\\{");
        for(String item: items){
            AListItem.clear();
            AListItem.load(item);
            if(AListItem.getName().contains("Hide")){
                AListItem.setHidden("T");
            }

            mDb.insert(SQLITE_TABLE, null, AListItem.getContentValues());

            ContentValues values = AListItem.getContentValues();
            Log.v(TAG, "values.toString()" + values.toString());
        }
    }

    public boolean deleteAll() {
        int doneDelete = 0;
        doneDelete = mDb.delete(SQLITE_TABLE, null , null);
        Log.w(TAG, Integer.toString(doneDelete));
        return doneDelete > 0;
    }
}

AListItem.java:

public class AListItem {

    static String rawInputString;

    static String name;
    static String second_line;
    static String hidden;

    static AListItem aListItem;

    private AListItem() {
    }

    public static AListItem getInstance(){
        if (aListItem == null) {
            aListItem = new AListItem();
        }
        return aListItem;
    }

    public static void clear() {
        rawInputString = null;
        name = null;
        second_line = null;
        hidden = null;
    }

    public static ContentValues getContentValues() {
        ContentValues values = new ContentValues();
        values.put("NAME", name);
        values.put("SECOND_LINE", second_line);
        values.put("HIDDEN",  hidden);
        return values;
    }


    public static void load(String string) {
        StringBuffer buf = new StringBuffer(string);
        if (buf.substring(0,2).equals("[{")){
            buf.delete(0,2);
        }
        rawInputString = buf.toString();
        parse();
    }

    public static void parse() {

        if (rawInputString == null) {
            System.out.println("nothing to parse");
            return;
        }

        rawInputString = rawInputString.replaceAll("\"\\:null,", "\"\\:\"null\",");
        String[] nvpa = rawInputString.split("\",\"");
        for (String nvpString : nvpa) {
            String[] nvpItem = nvpString.split("\":\"");
            if (nvpItem.length < 2) continue;
            String identifier = nvpItem[0].replaceAll("\"", "");
            String content = nvpItem[1].replaceAll("\"", "");

            switch (identifier) {
                case "name":
                    setName(content);
                    break;
                case "second_line":
                    setSecond_line(content);
                    break;
                case "hidden":
                    setHidden(content);
                    break;
                default:
                    System.out.println("nowhere to put [" + nvpItem[0] + "] " + nvpString + " raw: " + rawInputString);
                    break;
            }
        }
    }

    public static String getName() {
        return name;
    }

    public static void setName(String name) {  AListItem.name = name; }

    public static void setSecond_line(String second_line) {
        AListItem.second_line = second_line;
    }

    public static String getSecond_line() {
        return second_line;
    }

    public static void setHidden(String hidden) {
        AListItem.hidden = hidden;
    }

    public static String getHidden() {
        return hidden;
    }

    public String toString() {
        return getName() + ", " +
                getSecond_line() + ", " +
                getHidden();
    }

}

MySimpleCursorAdapter.java:

public class MySimpleCursorAdapter extends SimpleCursorAdapter {

    Context context;
    Cursor cursor;
    public static final String TAG = "MySimpleCursorAdapter";

    public MySimpleCursorAdapter(Context context, int layout, Cursor cursor, String[] from, int[] to, int flags) {
        super(context, layout, cursor, from, to, flags);
        this.context = context;
        this.cursor = cursor;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        Log.v(TAG,"getView() >>>>>>STARTING");

        ViewHolder viewHolder;
        LayoutInflater inflater = LayoutInflater.from(context);
        if (null == convertView || null == convertView.getTag()) {
            convertView = inflater.inflate(R.layout.b_item, null);
            viewHolder = new ViewHolder(convertView);
        } else {
            viewHolder = (ViewHolder) convertView.getTag();
        }

        for (int i = 0; i < cursor.getColumnCount(); i++) {
            Log.v(TAG, "getView cursor " + i + ": " + cursor.getString(i));
        }

        String hidden = cursor.getString(ViewHolder.HIDDEN);
        if (hidden == null) hidden = "F";
        Log.v(TAG,"Hidden State: " + hidden);
        switch (hidden) {
            case "F":
               viewHolder.showSecondLine(); // DRS 20160827 - Added line suggested by aiwiguna
                break;
            case "T":
                Log.v(TAG,">>>>>Hidden was TRUE<<<<<<<: " + cursor.getString(ViewHolder.NAME));
                viewHolder.hideSecondLine();
                break;
        }

        convertView.setTag(viewHolder);
        View returnView = super.getView(position, convertView, parent);

        Log.v(TAG,"getView() ENDING<<<<<<<<<");

        return returnView;
    }
}

ViewHolder.java:

class ViewHolder {

    public static final String TAG = "ViewHolder";

    public static final int DB_ITEM = 0;
    public static final int NAME = 1;
    public static final int SECOND_LINE = 2;
    public static final int HIDDEN = 3;

    public static final int[] viewsArray = new int[] {
            R.id.bDbItem,
            R.id.bName,
            R.id.bSecondLine,
            R.id.bHidden,
    };

    public static final TextView[] textViewArray = new TextView[viewsArray.length];

    public ViewHolder( final View root ) {
        Log.v(TAG, "ViewHolder constructor");
        for (int i = 0; i < viewsArray.length; i++) {
            textViewArray[i] = (TextView) root.findViewById(viewsArray[i]);
            Log.v(TAG, "             textViewArray[" + i + "]: " + textViewArray[i]);
        }
    }

    public void hideSecondLine() {
        textViewArray[SECOND_LINE].setVisibility(View.INVISIBLE);
    }

    //DRS 20160827 - Addition recommended by aiwiguna
    public void showSecondLine() {
        textViewArray[SECOND_LINE].setVisibility(View.VISIBLE);
    }

}

Solution

  • In order to get this application working,

    1. the ListView must be replaced by a RecyclerView, plus
    2. the SimpleCursorAdapter implementation needs to be replaced by a RecyclerCursorAdapter implementation.

    MyRecyclerCursorAdapter, a new class in the example, extends RecyclerView.Adapter:

    //DRS 20160829 - Added class.  Replaces MySimpleCursorAdapter
    public class MyRecyclerCursorAdapter extends RecyclerView.Adapter{
    
        private Cursor cursor;
        private Context context;
        private static final String TAG = MyRecyclerCursorAdapter.class.getSimpleName();
    
        public MyRecyclerCursorAdapter(Context context, Cursor cursor) {
            this.cursor = cursor;
            this.context = context;
        }
    
        //DRS 20160829 - Critical method within new class
        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            Log.v(TAG, "onCreateViewHolder ");
            Context context = parent.getContext();
            LayoutInflater inflater = LayoutInflater.from(context);
    
            View itemView = inflater.inflate(R.layout.b_item, parent, false);
    
            ViewHolder viewHolder = new ViewHolder(itemView, cursor);
            return viewHolder;
        }
    
        //DRS 20160829 - Critical method within new class
        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            cursor.moveToPosition(position);
            ((ViewHolder)holder).bindFields(cursor);
        }
    
        @Override
        public int getItemCount() {
            return cursor.getCount();
        }
    }
    

    Note that this class carries a Cursor object, which is the link to the SQLite database entries that will be populating the list.

    Also, in order to gain access to the Recycler View, a dependency must be added to build.gradle:

    // DRS 20160829 - Added recyclerview
    dependencies {
        compile fileTree(include: ['*.jar'], dir: 'libs')
        testCompile 'junit:junit:4.12'
        compile 'com.android.support:appcompat-v7:24.2.0'
        compile 'com.android.support:recyclerview-v7:24.2.0'
    }
    

    b_item.xml required no changes.
    activity_lv_ca.xml required a RecyclerView in place of the old ListView:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="fill_parent" android:layout_height="fill_parent"
              android:orientation="vertical">
    
        <!-- DRS 20160829 - Commented ListView, Added RecyclerView
        <ListView android:id="@+id/listView1" android:layout_width="fill_parent"
              android:layout_height="fill_parent" / -->
        <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView1"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="vertical" />
    
    </LinearLayout>
    

    The ViewHolder class now extends Recycler.ViewHolder. Beyond the standard ViewHolder implementation, this customized ViewHolder also has a Cursor which is used to set the text for the TextViews that appear on each row of the list. And this is where the visibility is managed (in a method I called bindFields():

    public class ViewHolder extends RecyclerView.ViewHolder {
        public static final String TAG = "ViewHolder";
        private final Cursor cursor;
    
        public TextView bDbItem;
        public TextView bName;
        public TextView bSecondLine;
        public TextView bHidden;
    
        public static final int DB_ITEM = 0;
        public static final int NAME = 1;
        public static final int SECOND_LINE = 2;
        public static final int HIDDEN = 3;
    
        public ViewHolder(View root, Cursor cursor ) {
            super(root);
            this.cursor = cursor;
            Log.v(TAG, "ViewHolder constructor");
            bDbItem = (TextView) itemView.findViewById(R.id.bDbItem);
            bName = (TextView) itemView.findViewById(R.id.bName);
            bSecondLine = (TextView) itemView.findViewById(R.id.bSecondLine);
            bHidden = (TextView) itemView.findViewById(R.id.bHidden);
        }
    
        public void bindFields(Cursor cursor) {
    
            bDbItem.setText("" + cursor.getInt(DB_ITEM));
            bName.setText(cursor.getString(NAME));
            bSecondLine.setText(cursor.getString(SECOND_LINE));
    
            String hidden = cursor.getString(HIDDEN);
            bHidden.setText(hidden);
            if ("F".equals(hidden)) {
                bSecondLine.setVisibility(View.VISIBLE);
            } else {
                bSecondLine.setVisibility(View.INVISIBLE);
            }
        }
    }
    

    AListItem.java required no changes.
    DBAdapter.java required no changes.

    The working application may be found on github: RecyclerViewSqlite