【问题标题】:Is this custom CursorAdapter for a ListView properly coded for Android?这个 ListView 的自定义 CursorAdapter 是否为 Android 正确编码?
【发布时间】:2012-10-04 17:26:26
【问题描述】:

我对自定义 CursorAdapter 上的代码一直不满意,直到今天我决定审查它并解决一个困扰我很长时间的小问题(有趣的是,我的应用程序的用户都没有报告过这样的问题问题)。

这里是我的问题的一个小描述:

我的自定义 CursorAdapter 覆盖 newView()bindView() 而不是 getView() 作为我看到的大多数示例。我在这两种方法之间使用 ViewHolder 模式。但我的主要问题是我为每个列表项使用的自定义布局,它包含一个ToggleButton

问题是当列表项视图滚动出视图然后滚动回视图时,按钮状态未保持。之所以存在这个问题,是因为cursor 从未意识到在按下ToggleButton 时数据库数据发生了变化,并且它总是提取相同的数据。单击ToggleButton 时我尝试重新查询光标并解决了问题,但速度很慢。

我已经解决了这个问题,我将在此处发布整个课程以供审核。为了更好地解释我的编码决定,我已经对这个特定问题的代码进行了彻底的注释。

您觉得这段代码好看吗?你会以某种方式改进/优化或改变它吗?

P.S:我知道 CursorLoader 是一个明显的改进,但我暂时没有时间处理这么大的代码重写。不过,这是我在路线图中的内容。

代码如下:

public class NotesListAdapter extends CursorAdapter implements OnClickListener {

    private static class ViewHolder {
        ImageView icon;
        TextView title;
        TextView description;
        ToggleButton visibility;
    }

    private static class NoteData {
        long id;
        int iconId;
        String title;
        String description;
        int position;
    }

    private LayoutInflater mInflater;

    private NotificationHelper mNotificationHelper;
    private AgendaNotesAdapter mAgendaAdapter;

    /*
     * This is used to store the state of the toggle buttons for each item in the list
     */
    private List<Boolean> mToggleState;

    private int mColumnRowId;
    private int mColumnTitle;
    private int mColumnDescription;
    private int mColumnIconName;
    private int mColumnVisibility;

    public NotesListAdapter(Context context, Cursor cursor, NotificationHelper helper, AgendaNotesAdapter adapter) {
        super(context, cursor);

        mInflater = LayoutInflater.from(context);

        /*
         * Helper class to post notifications to the status bar and database adapter class to update
         * the database data when the user presses the toggle button in any of items in the list
         */
        mNotificationHelper = helper;
        mAgendaAdapter = adapter;

        /*
         * There's no need to keep getting the column indexes every time in bindView() (as I see in
         * a few examples) so I do it once and save the indexes in instance variables
         */
        findColumnIndexes(cursor);

        /*
         * Populate the toggle button states for each item in the list with the corresponding value
         * from each record in the database, but isn't this a slow operation?
         */
        for(mToggleState = new ArrayList<Boolean>(); !cursor.isAfterLast(); cursor.moveToNext()) {
            mToggleState.add(cursor.getInt(mColumnVisibility) != 0);
        }
    }

    @Override
    public View newView(Context context, Cursor cursor, ViewGroup parent) {
        View view = mInflater.inflate(R.layout.list_item_note, null);

        /*
         * The ViewHolder pattern is here only used to prevent calling findViewById() all the time
         * in bindView(), we only need to find all the views once
         */
        ViewHolder viewHolder = new ViewHolder();

        viewHolder.icon = (ImageView)view.findViewById(R.id.imageview_icon);
        viewHolder.title = (TextView)view.findViewById(R.id.textview_title);
        viewHolder.description = (TextView)view.findViewById(R.id.textview_description);
        viewHolder.visibility = (ToggleButton)view.findViewById(R.id.togglebutton_visibility);

        /*
         * I also use newView() to set the toggle button click listener for each item in the list
         */
        viewHolder.visibility.setOnClickListener(this);

        view.setTag(viewHolder);

        return view;
    }

    @Override
    public void bindView(View view, Context context, Cursor cursor) {
        Resources resources = context.getResources();

        int iconId = resources.getIdentifier(cursor.getString(mColumnIconName),
                "drawable", context.getPackageName());

        String title = cursor.getString(mColumnTitle);
        String description = cursor.getString(mColumnDescription);

        /*
         * This is similar to the ViewHolder pattern and it's need to access the note data when the
         * onClick() method is fired
         */
        NoteData noteData = new NoteData();

        /*
         * This data is needed to post a notification when the onClick() method is fired
         */
        noteData.id = cursor.getLong(mColumnRowId);
        noteData.iconId = iconId;
        noteData.title = title;
        noteData.description = description;

        /*
         * This data is needed to update mToggleState[POS] when the onClick() method is fired
         */
        noteData.position = cursor.getPosition();

        /*
         * Get our ViewHolder with all the view IDs found in newView()
         */
        ViewHolder viewHolder = (ViewHolder)view.getTag();

        /*
         * The Html.fromHtml is needed but the code relevant to that was stripped
         */
        viewHolder.icon.setImageResource(iconId);
        viewHolder.title.setText(Html.fromHtml(title));
        viewHolder.description.setText(Html.fromHtml(description));

        /*
         * Set the toggle button state for this list item from the value in mToggleState[POS]
         * instead of getting it from the database with 'cursor.getInt(mColumnVisibility) != 0'
         * otherwise the state will be incorrect if it was changed between the item view scrolling
         * out of view and scrolling back into view
         */
        viewHolder.visibility.setChecked(mToggleState.get(noteData.position));

        /*
         * Again, save the note data to be accessed when onClick() gets fired
         */
        viewHolder.visibility.setTag(noteData);
    }

    @Override
    public void onClick(View view) {
        /*
         * Get the new state directly from the toggle button state 
         */
        boolean visibility = ((ToggleButton)view).isChecked();

        /*
         * Get all our note data needed to post (or remove) a notification 
         */
        NoteData noteData = (NoteData)view.getTag();

        /*
         * The toggle button state changed, update mToggleState[POS] to reflect that new change
         */
        mToggleState.set(noteData.position, visibility);

        /*
         * Post the notification or remove it from the status bar depending on toggle button state
         */
        if(visibility) {
            mNotificationHelper.postNotification(
                    noteData.id, noteData.iconId, noteData.title, noteData.description);
        } else {
            mNotificationHelper.cancelNotification(noteData.id);
        }

        /*
         * Update the database note item with the new toggle button state, without the need to
         * requery the cursor (which is slow, I've tested it) to reflect the new toggle button state
         * in the list because the value was saved in mToggleState[POS] a few lines above
         */
        mAgendaAdapter.updateNote(noteData.id, null, null, null, null, visibility);
    }

    private void findColumnIndexes(Cursor cursor) {
        mColumnRowId = cursor.getColumnIndex(AgendaNotesAdapter.KEY_ROW_ID);
        mColumnTitle = cursor.getColumnIndex(AgendaNotesAdapter.KEY_TITLE);
        mColumnDescription = cursor.getColumnIndex(AgendaNotesAdapter.KEY_DESCRIPTION);
        mColumnIconName = cursor.getColumnIndex(AgendaNotesAdapter.KEY_ICON_NAME);
        mColumnVisibility = cursor.getColumnIndex(AgendaNotesAdapter.KEY_VISIBILITY);
    }

}

【问题讨论】:

    标签: android database optimization android-cursoradapter


    【解决方案1】:

    您的解决方案是最佳的,我会将其添加到我的武器中:) 也许,我会尝试对数据库调用进行一些优化。

    确实,由于任务的条件,只有三种解决方案:

    1. 仅更新一行,重新查询游标并重绘所有项目。 (直截了当,蛮力)。
    2. 更新行、缓存结果并将缓存用于绘图项。
    3. 缓存结果,为绘图项使用缓存。当您离开此活动/片段时,然后将结果提交到数据库。

    对于第三种解决方案,您可以使用 SparseArray 来查找更改。

    private SparseArray<NoteData> mArrayViewHolders;
    
    public void onClick(View view) {
         //here your logic with NoteData. 
         //start of my improve
         if (mArrayViewHolders.get(selectedPosition) == null) {
            // put the change into array
            mArrayViewHolders.put(selectedPosition, noteData);
         } else {
            // rollback the change
            mArrayViewHolders.delete(selectedPosition);
         }
         //end of my improve
         //we don't commit the changes to database.
    }
    

    再一次:从一开始这个数组就是空的。当您第一次切换按钮时(有变化),您将 NoteData 添加到数组中。当您第二次切换按钮(有回滚)时,您会从数组中删除 NoteData。等等。

    完成后,只需请求数组并将更改推送到数据库中。

    【讨论】:

    • 我喜欢SparseArray 的想法,但不知道那个课程。它看起来像是一种更有效的缓存按钮状态的方法,而不是将它们全部保存在List 中。但我不喜欢仅在用户离开活动时将结果提交到数据库的想法。那将需要额外的代码来处理这种情况。所以,最后,我想我选择了你列举的第二种解决方案。这基本上就是我最初所做的。我仍然喜欢你的回答,但我可能会再去 1 或 2 天 :)
    【解决方案2】:

    你看到的是Android的View重用。我不认为你通过再次查询游标做错了什么。只是不要使用 cursor.requery() 函数。

    相反,首先总是将切换设置为 false,然后询问光标并在必要时将其打开。

    也许您正在这样做并且我误解了某些内容,但是我认为您不应该这样做会产生缓慢的结果。

    伪代码:

    getView(){
    setToggleFalse();
    boolean value = Boolean.valueOf(cursor.getString('my column'));
    if (value){
       setToggleTrue();
    }
    }
    

    【讨论】:

      【解决方案3】:

      我会在去 CursorLoader 之前等待。似乎 CursorLoader 派生类不适用于 CursorLoader。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多