Search code examples
androidlistviewactionbarsherlockandroid-actionmode

How to change list item background in ListView?


I want to use contextual menu in ActionBarSherlock which actually doesn't allow to use ListView.CHOICE_MODE_MULTIPLE_MODAL.

I've created my own implementation for multi-selecting items on list, but the problem is (actually that's a great feature in other cases) that android reuses views in ListView. I handle multi-selecting items on list by coloring their background to mark them as selected, moreover I store selection in SparseArray but it doesn't matter now, because it's working perfectly.

As you can guess, background color is replicated every 10 items on my list starting from item which I've selected.

So what should I do to provide different view for each list item? Or maybe there's another solution for that problem?

I use SimpleCursorAdapter with ViewBinder to fill in my list items.


    /***************************************
     ******** ACTION MODES HANDLING ********
     ***************************************/

    @Override
    public boolean onItemLongClick(AdapterView<?> parent, View v, int position, long id) {
        switchActionMode();

        if (isInActionMode) {
            checkItem(position, v);
            startActionMode();
        }
        else {
            uncolorAndClearAllItems();
            actionMode.finish();
        }

        return true;
    }

    private void switchActionMode() {
        isInActionMode ^= true;
    }

    private void checkItem(int position, View v) {
        boolean isChecked = isItemChecked(position);
        setItemChecked(position, v, !isChecked);
        colorItem(v, !isChecked);
    }

    private boolean isItemChecked(int position) {
        return checkedItems.get(position, false);
    }

    private void startActionMode() {
        actionMode = activity.startActionMode(new ActionModeCallback());
    }

    private void setItemChecked(int position, View v, boolean isChecked) {
        if (isChecked) {
            checkedItems.put(position, true);
        }
        else {
            checkedItems.delete(position);
        }
    }

    private void colorItem(View v, boolean isChecked) {
        int color;
        if (isChecked) {
            color = COLOR_BLUE;
        }
        else {
            color = Color.TRANSPARENT;
        }
        v.setBackgroundColor(color);
    }

    private void colorItem(int position, boolean isChecked) {
        View listItemView = listView.getChildAt(position);
        colorItem(listItemView, isChecked);
    }

    private final class ActionModeCallback implements ActionMode.Callback {
        @Override
        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
            activity.getSupportMenuInflater().inflate(R.menu.list_action_menu, menu);
            return true;
        }

        @Override
        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
            isInActionMode = true;
            swipeDismissList.pause();
            return true;
        }

        @Override
        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
            int itemsSize = checkedItems.size();

            switch (item.getItemId()) {
                case R.id.menu_dismiss:
                    dismissFeeds(itemsSize);
                    break;
                case R.id.menu_mark_as_read:
                    markFeedsAsRead(itemsSize);
                    break;
            }

            restartLoaderIfNecessary(itemsSize);
            mode.finish();
            return true;
        }

        private void dismissFeeds(int itemsSize) {
            ArrayList<Feed> feeds = new ArrayList<Feed>();
            for (int i = itemsSize - 1; i >= 0; i--) {
                int position = checkedItems.keyAt(i);
                Feed dismissedFeed = dismissFeedAndReturn(position);
                feeds.add(dismissedFeed);
            }
            createUndoAction(feeds, R.string.undobar_message_deleted_selected, Action.DISMISSED);
        }

        private void markFeedsAsRead(int itemsSize) {
            ArrayList<Long> feedsIDs = new ArrayList<Long>();
            for (int i = itemsSize - 1; i >= 0; i--) {
                int position = checkedItems.keyAt(i);
                Feed readFeed = markFeedAsReadAndReturn(position);
                feedsIDs.add(readFeed.ID());
            }
            createUndoAction(feedsIDs, R.string.undobar_message_read_selected, Action.READ);
        }

        private void createUndoAction(ArrayList<? extends Serializable> feedsIDs, int messageId, Action actionType) {
            UndoableCollection undoableAction = new UndoableCollection(feedsIDs, actionType);
            undoBarController.showUndoBar(undoableAction, messageId);
        }

        private void restartLoaderIfNecessary(int itemsSize) {
            if (itemsWillChange(itemsSize)) {
                restartLoader();
            }
        }

        private boolean itemsWillChange(int itemsSize) {
            return itemsSize != 0;
        }

        @Override
        public void onDestroyActionMode(ActionMode mode) {
            isInActionMode = false;
            swipeDismissList.resume();
            uncolorAndClearAllItems();
        }
    }

    private void uncolorAndClearAllItems() {
        int size = checkedItems.size();
        for (int i = size - 1; i >= 0; i--) {
            int position = checkedItems.keyAt(i);
            colorItem(position, false);
        }
        checkedItems.clear();
    }

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

        if (isInActionMode) {
            checkItem(position, v);
            clearCheckedItemsIfNoSelection();
        }
    }

    private void clearCheckedItemsIfNoSelection() {
        if (noItemSelected()) {
            checkedItems.clear();
            actionMode.finish();
        }
    }

    private boolean noItemSelected() {
        return checkedItems.size() == 0;
    }


SimpleCursorAdapter

class FeedCursorAdapter(context: Context, cursor: Cursor)
    extends SimpleCursorAdapter(
        context,
        R.layout.list_item,
        cursor,
        Array(C_TITLE, C_SITE, C_ADDED_DATE, C_IMAGE),
        Array(R.id.textViewFeedTitle, R.id.textViewChannelSite, R.id.textViewFeedDate, R.id.imageViewFeedImage),
        0) {

setViewBinder(new FeedCursorViewBinder(context))
}


ViewBinder

class FeedCursorViewBinder(context: Context) extends SimpleCursorAdapter.ViewBinder {
    implicit def int2bool(int: Int) = if (int == 1) true else false
    implicit def longDate2String(longDate: Long) = new Date(longDate).toLocaleString

    private lazy val bitmapUtils = new BitmapUtils(context)

    override def setViewValue(view: View, cursor: Cursor, columnIndex: Int) = {
            /**
              * Get columns indexes from cursor
              */
            def getColumnIndex(columnName: String) = cursor.getColumnIndex(columnName)
        val indexTitle = getColumnIndex(C_TITLE)
        val indexRead = getColumnIndex(C_READ)
        val indexDate = getColumnIndex(C_ADDED_DATE)
        val indexImage = getColumnIndex(C_IMAGE)
        val indexChannel = getColumnIndex(C_CHANNEL)
        val indexSite = getColumnIndex(C_SITE)

            def setTextAndColor(index: Int) = {
                val text = cursor.getString(index)
                val textView = view.asInstanceOf[TextView]
                textView.setTextColor(getTextColor)
                textView.setText(text)
                true
            }

            def getTextColor = {
                val isRead: Boolean = cursor.getInt(indexRead)
                if (isRead) Color.GRAY
                else Color.BLACK
            }

        columnIndex match {
            case `indexTitle` | `indexSite` => setTextAndColor(columnIndex)

            case `indexImage` => {
                val imageId: Int = cursor.getInt(indexImage)
                val imageFromFile = bitmapUtils.readBitmapForFeed(imageId)
                val feedImage =
                    if (imageFromFile != null) imageFromFile
                    else BitmapFactory.decodeResource(context.getResources, imageId)
                val imageViewFeedImage = view.asInstanceOf[ImageView]
                imageViewFeedImage.setImageBitmap(feedImage)
                true
            }

            case `indexDate` => {
                val dateLong = cursor.getLong(indexDate)
                val textViewFeedDate = view.asInstanceOf[TextView]
                textViewFeedDate.setText(dateLong)
                true
            }

            case _ => false
        }
    }
}

Maybe I'll precise my question:

How to set list item background (with cursor adapter) at runtime and not duplicate it, when list item view is reused?


Solution

  • The answer is so simle that I'm ashamed that I didn't find it earlier.
    It's just enough to override the following methods:

    override def getViewTypeCount = if (getCount < 1) LIST_COUNT else getCount
    // which returns how many different views will be in list view
    
    override def getItemViewType(position: Int) = position
    // which defines the type of current list item (when some of list items returns the same type, this view will be reused!)
    

    where LIST_COUNT is big enough to be sure that no more items will be displayed in a ListView.

    Only two more restriction:

    • This IS NOT EFFICIENT at all, because it enforces to create new list items for each row without reusing the old ones.
    • In this technique there's no need to create ViewHolder pattern, because list items won't be used again