如何在RecyclerView中正确地突出显示所选项目?

182

我试图将 RecyclerView 用作水平的 ListView。 我正在尝试弄清如何突出显示所选项目。 当我单击其中一个项目时,它会被选择并正确地突出显示,但当我单击另一个项目时,第二个项目会与旧的项目一起突出显示。

这是我的 onClick 函数:

@Override
public void onClick(View view) {

    if(selectedListItem!=null){
        Log.d(TAG, "selectedListItem " + getPosition() + " " + item);
        selectedListItem.setBackgroundColor(Color.RED);
    }
    Log.d(TAG, "onClick " + getPosition() + " " + item);
    viewHolderListener.onIndexChanged(getPosition());
    selectedPosition = getPosition();
    view.setBackgroundColor(Color.CYAN); 
    selectedListItem = view;
}

这里是 onBindViewHolder 方法:

@Override
public void onBindViewHolder(ViewHolder viewHolder, int position) {   
    viewHolder.setItem(fruitsData[position]);
    if(selectedPosition == position)
        viewHolder.itemView.setBackgroundColor(Color.CYAN);    
    else
        viewHolder.itemView.setBackgroundColor(Color.RED);

}

使用可聚焦视图来跟踪所选项目并不是一个好主意。请查看我的答案,以获取完整的解决方案。 - Greg Ennis
也许这个可以帮到你:http://amolsawant88.blogspot.in/2015/08/easy-way-to-highlight-selected-rowitem.html - Amol Sawant
RecyclerView项目选择:https://stackoverflow.com/a/38501676/2648035 - Alok Omkar
当你没有任何已经“工作”的东西时,这很难理解,这很糟糕,因为答案会跟着而不太指明应该放在哪里。 - FirstOne
嘿,你把这个OnClick函数放在哪里了?我也在试图找出来。 - Phani Teja
16个回答

208

这是一种更简单的方法。

在RecyclerView Adapter类中有一个private int selectedPos = RecyclerView.NO_POSITION;,在onBindViewHolder方法下尝试:

@Override
public void onBindViewHolder(ViewHolder viewHolder, int position) {   
    viewHolder.itemView.setSelected(selectedPos == position);

}

在您的 OnClick 事件中进行修改:

@Override
public void onClick(View view) {
     notifyItemChanged(selectedPos);
     selectedPos = getLayoutPosition();
     notifyItemChanged(selectedPos); 
}

对于导航抽屉和其他RecyclerView项目适配器非常好用。

注意:一定要在布局中使用像@colabug澄清的选择器来设置背景颜色。

<selector xmlns:android="http://schemas.android.com/apk/res/android">
  <item android:drawable="@color/pressed_color" android:state_pressed="true"/>
  <item android:drawable="@color/selected_color" android:state_selected="true"/>
  <item android:drawable="@color/focused_color" android:state_focused="true"/>
</selector>

否则 setSelected(..) 将不起作用,这种解决方案将变得无效。

@zIronManBox:我试了你的方法。除了在调用onClick方法之前第一个项目已经被高亮显示之外,其他都可以正常工作。有没有什么解决方法? - AJW
@colabug:我还设置了一个背景drawable/selector。我的问题是,在调用onClick方法之前,第一项已经有了背景高亮显示。有什么解决办法吗? - AJW
3
我无法使用getLayoutPosition()。 - ka3ak
3
不要仅使用-1,而是使用RecyclerView.NO_POSITION;(它的值为-1)。 - Martin Marconcini
10
getLayoutPosition是ViewHolder类的一个方法,它的对象作为bind view方法的第一个参数进行传递。因此,可以通过 vieHolder.getLayoutPosition 来访问它。 - Tushar Kathuria
显示剩余5条评论

152

更新[2017年7月26日]:

正如Pawan在评论中提到的那样,IDE警告不要使用固定位置,我已经按照以下方式修改了我的代码。点击监听器移到了ViewHolder,并且我使用getAdapterPosition()方法获取位置。

int selected_position = 0; // You have to set this globally in the Adapter class

@Override
public void onBindViewHolder(ViewHolder holder, int position) {
    Item item = items.get(position);

    // Here I am just highlighting the background
    holder.itemView.setBackgroundColor(selected_position == position ? Color.GREEN : Color.TRANSPARENT);
}

public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {

    public ViewHolder(View itemView) {
        super(itemView);
        itemView.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        // Below line is just like a safety check, because sometimes holder could be null,
        // in that case, getAdapterPosition() will return RecyclerView.NO_POSITION
        if (getAdapterPosition() == RecyclerView.NO_POSITION) return;

        // Updating old as well as new positions
        notifyItemChanged(selected_position);
        selected_position = getAdapterPosition();
        notifyItemChanged(selected_position);

        // Do your another stuff for your onClick
    }
}

希望这会有所帮助。


@suku 你在ScrollView中使用RecyclerView吗? - Meet Vora
不,它在线性布局内部。 - suku
@suku,你能在这里发布你的适配器类吗?我认为你的填充布局可能会很复杂。 - Meet Vora
@Spacemonkey 感谢您的反馈。我会检查并稍后告诉您相关情况,因为我现在很忙。 - Meet Vora
1
干得好,Meet。对于这里未选择的代码,它对我有效。int previousselectedPosition = -2; //全局设置。然后在这些行之后添加以下行, if(previousselectedPosition == selected_position) { selected_position = -1; notifyItemChanged(selected_position);
} else { previousselectedPosition = selected_position; } 尝试这个,它会起作用。
- G.N.SRIDHAR
显示剩余20条评论

68

我编写了一个基础适配器类,用于自动处理 RecyclerView 中的项目选择。只需从它派生您的适配器,并使用带有 state_selected 的可绘制状态列表,就像您在列表视图中所做的那样。

这里有一篇博客文章介绍它,但以下是代码:

public abstract class TrackSelectionAdapter<VH extends TrackSelectionAdapter.ViewHolder> extends RecyclerView.Adapter<VH> {
    // Start with first item selected
    private int focusedItem = 0;

    @Override
    public void onAttachedToRecyclerView(final RecyclerView recyclerView) {
        super.onAttachedToRecyclerView(recyclerView);

        // Handle key up and key down and attempt to move selection
        recyclerView.setOnKeyListener(new View.OnKeyListener() {
            @Override
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                RecyclerView.LayoutManager lm = recyclerView.getLayoutManager();

                // Return false if scrolled to the bounds and allow focus to move off the list
                if (event.getAction() == KeyEvent.ACTION_DOWN) {
                    if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
                        return tryMoveSelection(lm, 1);
                    } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
                        return tryMoveSelection(lm, -1);
                    }
                }

                return false;
            }
        });
    }

    private boolean tryMoveSelection(RecyclerView.LayoutManager lm, int direction) {
        int tryFocusItem = focusedItem + direction;

        // If still within valid bounds, move the selection, notify to redraw, and scroll
        if (tryFocusItem >= 0 && tryFocusItem < getItemCount()) {
            notifyItemChanged(focusedItem);
            focusedItem = tryFocusItem;
            notifyItemChanged(focusedItem);
            lm.scrollToPosition(focusedItem);
            return true;
        }

        return false;
    }

    @Override
    public void onBindViewHolder(VH viewHolder, int i) {
        // Set selected state; use a state list drawable to style the view
        viewHolder.itemView.setSelected(focusedItem == i);
    }

    public class ViewHolder extends RecyclerView.ViewHolder {
        public ViewHolder(View itemView) {
            super(itemView);

            // Handle item click and set the selection
            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    // Redraw the old selection and the new
                    notifyItemChanged(focusedItem);
                    focusedItem = getLayoutPosition();
                    notifyItemChanged(focusedItem);
                }
            });
        }
    }
} 

mRecyclerView没有在任何地方声明。我们是否应该将其作为参数传递给构造函数并存储在字段中? - Pedro
1
抱歉,我的适配器是片段的内部类,因此它可以访问到 RecyclerView 字段。否则,您可以将其作为参数传递。或者更好的方法是,在 onRecyclerViewAttached 中处理并将其存储在成员变量中。 - Greg Ennis
1
不错的回答,但为什么要使用getChildPosition()?还有另一种方法https://developer.android.com/reference/android/support/v7/widget/RecyclerView.ViewHolder.html#getAdapterPosition()。 - skyfishjy
2
我尝试过这个,发现连续两次调用NotifyItemChanged()会在较慢的硬件上破坏任何平滑滚动的迹象。在Lollipop更新之前,在Fire TV上表现尤为糟糕。 - Redshirt
显示剩余4条评论

14

正如在这个链接的问题中提到的,为viewHolders设置侦听器应该在onCreateViewHolder中完成。 也就是说,下面的实现最初是针对多重选择的,但我在片段中加入了一个hack以强制进行单选。(*1)

// an array of selected items (Integer indices) 
private final ArrayList<Integer> selected = new ArrayList<>();

// items coming into view
@Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
    // each time an item comes into view, its position is checked
    // against "selected" indices
    if (!selected.contains(position)){
        // view not selected
        holder.parent.setBackgroundColor(Color.LTGRAY);
    }
    else
        // view is selected
        holder.parent.setBackgroundColor(Color.CYAN);
}

// selecting items
@Override
public boolean onLongClick(View v) {
        
        // select (set color) immediately.
        v.setBackgroundColor(Color.CYAN);

        // (*1)
        // forcing single selection here...
        if (selected.isEmpty()){
            selected.add(position); // (done - see note)
        }else {
            int oldSelected = selected.get(0);
            selected.clear(); // (*1)... and here.
            selected.add(position);
            // note: We do not notify that an item has been selected
            // because that work is done here.  We instead send
            // notifications for items which have been deselected.
            notifyItemChanged(oldSelected);
        }
        return false;
}

holder 中没有 parent 字段。 - FractalBob
@FractalBob,显然我的实现中有这个问题。不管怎样,这些代码行不是答案逻辑的一部分。那里是您可以放置自己的逻辑以在选择/取消选择时发生所需操作的地方。 - entitycs

8
我认为我已经找到了如何使用 RecyclerView 的最佳教程,其中包含我们需要的所有基本功能(单选和多选、高亮、涟漪、单击和在多选中删除等)。

这里是它--> http://enoent.fr/blog/2015/01/18/recyclerview-basics/

基于此,我能够创建一个库“FlexibleAdapter”,它扩展了SelectableAdapter。 我认为这应该是适配器的责任,实际上你不需要每次都重写适配器的基本功能,让一个库来完成,这样你就可以重用相同的实现。

这个适配器非常快速,开箱即用(你不需要扩展它);你可以为你需要的每种视图类型自定义项目;ViewHolder 是预定义的:常见事件已实现:单击和长按;它保持旋转后的状态,以及更多更多

请看一看,并随意在你的项目中实施它。

https://github.com/davideas/FlexibleAdapter

维基百科也可用。


写那个适配器的工作做得很好,它看起来非常有用。唯一的问题是它真的需要一些基本示例和文档,我发现即使是让它运行起来也有点困惑。但潜力还是很大的! - Voy
是的,我在那里没有找到足够的信息来开始。即使代码似乎是针对API参考进行注释的,但我仍然找不到API参考。示例应用程序虽然广泛且信息丰富,但如果没有该库的先前知识,很难理解。所有用例都绑定在一起,很少有指示说明哪个演示了什么,类在不同场景中被重复使用,这导致它们被信息超载。我在此处创建了一个新问题,并提出了这些建议:https://github.com/davideas/FlexibleAdapter/issues/120 - Voy
Wiki已经完全重写,正在完成中。 - Davideas

6

请看我的解决方案。我建议您在holder中设置所选位置并将其作为View的标签传递。View应该在onCreateViewHolder(...)方法中设置。这也是设置视图监听器(例如OnClickListener或LongClickListener)的正确位置。

请看下面的示例并阅读代码注释。

public class MyListAdapter extends RecyclerView.Adapter<MyListAdapter.ViewHolder> {
    //Here is current selection position
    private int mSelectedPosition = 0;
    private OnMyListItemClick mOnMainMenuClickListener = OnMyListItemClick.NULL;

    ...

    // constructor, method which allow to set list yourObjectList

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        //here you prepare your view 
        // inflate it
        // set listener for it
        final ViewHolder result = new ViewHolder(view);
        final View view =  LayoutInflater.from(parent.getContext()).inflate(R.layout.your_view_layout, parent, false);
        view.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //here you set your current position from holder of clicked view
                mSelectedPosition = result.getAdapterPosition();

                //here you pass object from your list - item value which you clicked
                mOnMainMenuClickListener.onMyListItemClick(yourObjectList.get(mSelectedPosition));

                //here you inform view that something was change - view will be invalidated
                notifyDataSetChanged();
            }
        });
        return result;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        final YourObject yourObject = yourObjectList.get(position);

        holder.bind(yourObject);
        if(mSelectedPosition == position)
            holder.itemView.setBackgroundColor(Color.CYAN);
        else
            holder.itemView.setBackgroundColor(Color.RED);
    }

    // you can create your own listener which you set for adapter
    public void setOnMainMenuClickListener(OnMyListItemClick onMyListItemClick) {
        mOnMainMenuClickListener = onMyListItemClick == null ? OnMyListItemClick.NULL : onMyListItemClick;
    }

    static class ViewHolder extends RecyclerView.ViewHolder {


        ViewHolder(View view) {
            super(view);
        }

        private void bind(YourObject object){
            //bind view with yourObject
        }
    }

    public interface OnMyListItemClick {
        OnMyListItemClick NULL = new OnMyListItemClick() {
            @Override
            public void onMyListItemClick(YourObject item) {

            }
        };

        void onMyListItemClick(YourObject item);
    }
}

你认为mSelectedPosition可以声明为静态变量以保持配置更改吗? - Sathesh
不!将其设为静态是错误的,非常错误。 - Konrad Krakowiak
是的,这很危险。但是为了保持配置更改(特别是屏幕旋转),我们可以在活动中声明此变量并从那里获取它,对吧? - Sathesh
有许多解决方案...您可以使用getter和setter,您可以为适配器创建自己的saveInstanceState方法,并在Activity/Fragment的saveInstanceState中调用它。 - Konrad Krakowiak

4

RecyclerView中没有像ListView和GridView一样的选择器,但是您可以尝试下面的方法,这对我有效:

创建一个如下所示的选择器drawable:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"> 
<item android:state_pressed="true">
   <shape>
         <solid android:color="@color/blue" />
   </shape>
</item>

<item android:state_pressed="false">
    <shape>
       <solid android:color="@android:color/transparent" />
    </shape>
</item>
</selector>

然后将这个可绘制对象设置为您的RecyclerView行布局的背景,如下所示:
android:background="@drawable/selector"

2
这并没有什么有用的作用。它只会在按下时间时改变颜色。 - Leo DroidCoder

4

使用接口和回调来做出决策。 创建带有选择和取消选择状态的接口:

public interface ItemTouchHelperViewHolder {
    /**
     * Called when the {@link ItemTouchHelper} first registers an item as being moved or swiped.
     * Implementations should update the item view to indicate it's active state.
     */
    void onItemSelected();


    /**
     * Called when the {@link ItemTouchHelper} has completed the move or swipe, and the active item
     * state should be cleared.
     */
    void onItemClear();
}

在ViewHolder中实现接口:

   public static class ItemViewHolder extends RecyclerView.ViewHolder implements
            ItemTouchHelperViewHolder {

        public LinearLayout container;
        public PositionCardView content;

        public ItemViewHolder(View itemView) {
            super(itemView);
            container = (LinearLayout) itemView;
            content = (PositionCardView) itemView.findViewById(R.id.content);

        }

               @Override
    public void onItemSelected() {
        /**
         * Here change of item
         */
        container.setBackgroundColor(Color.LTGRAY);
    }

    @Override
    public void onItemClear() {
        /**
         * Here change of item
         */
        container.setBackgroundColor(Color.WHITE);
    }
}

回调函数中的运行状态改变:

public class ItemTouchHelperCallback extends ItemTouchHelper.Callback {

    private final ItemTouchHelperAdapter mAdapter;

    public ItemTouchHelperCallback(ItemTouchHelperAdapter adapter) {
        this.mAdapter = adapter;
    }

    @Override
    public boolean isLongPressDragEnabled() {
        return true;
    }

    @Override
    public boolean isItemViewSwipeEnabled() {
        return true;
    }

    @Override
    public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
        int swipeFlags = ItemTouchHelper.END;
        return makeMovementFlags(dragFlags, swipeFlags);
    }

    @Override
    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
        ...
    }

    @Override
    public void onSwiped(final RecyclerView.ViewHolder viewHolder, int direction) {
        ...
    }

    @Override
    public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
        if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
            if (viewHolder instanceof ItemTouchHelperViewHolder) {
                ItemTouchHelperViewHolder itemViewHolder =
                        (ItemTouchHelperViewHolder) viewHolder;
                itemViewHolder.onItemSelected();
            }
        }
        super.onSelectedChanged(viewHolder, actionState);
    }

    @Override
    public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        super.clearView(recyclerView, viewHolder);
        if (viewHolder instanceof ItemTouchHelperViewHolder) {
            ItemTouchHelperViewHolder itemViewHolder =
                    (ItemTouchHelperViewHolder) viewHolder;
            itemViewHolder.onItemClear();
        }
    }   
}

使用回调创建RecyclerView(示例):
mAdapter = new BuyItemsRecyclerListAdapter(MainActivity.this, positionsList, new ArrayList<BuyItem>());
positionsList.setAdapter(mAdapter);
positionsList.setLayoutManager(new LinearLayoutManager(this));
ItemTouchHelper.Callback callback = new ItemTouchHelperCallback(mAdapter);
mItemTouchHelper = new ItemTouchHelper(callback);
mItemTouchHelper.attachToRecyclerView(positionsList);

请查看 iPaulPro 文章了解更多:https://medium.com/@ipaulpro/drag-and-swipe-with-recyclerview-6a6f0c422efd


3

这是我的解决方案,您可以在一个项目(或一组项目)上进行设置,并使用另一次点击取消选择:

 private final ArrayList<Integer> seleccionados = new ArrayList<>();
@Override
    public void onBindViewHolder(final ViewHolder viewHolder, final int i) {
        viewHolder.san.setText(android_versions.get(i).getAndroid_version_name());
        if (!seleccionados.contains(i)){ 
            viewHolder.inside.setCardBackgroundColor(Color.LTGRAY);
        }
        else {
            viewHolder.inside.setCardBackgroundColor(Color.BLUE);
        }
        viewHolder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (seleccionados.contains(i)){
                    seleccionados.remove(seleccionados.indexOf(i)); 
                    viewHolder.inside.setCardBackgroundColor(Color.LTGRAY);
                } else { 
                    seleccionados.add(i);
                    viewHolder.inside.setCardBackgroundColor(Color.BLUE);
                }
            }
        });
    }

2

编程方式

NB:不使用任何文件选择器。

步骤:

  1. 创建一个全局位置变量,即int selectedPosition = -1;

整数:

-1表示默认未选择

0表示默认选择第一项

  1. Check if the variable equals to the current position position

    public void onBindViewHolder(@NonNull ViewHolder holder, int position)
    {
        if(selectedPosition == position)
            holder.itemView.setBackgroundColor(Color.GREEN); //selected
        else
            holder.itemView.setBackgroundColor(Color.BLACK); //not selected
        ....
    }
    
  2. Using setOnClickListener method to navigate between items

    holder.itemView.setOnClickListener(v ->
    {
        if(selectedPosition == position)
        {
            selectedPosition = -1;
            notifyDataSetChanged();
            return;
        }
    
        selectedPosition = position;
        notifyDataSetChanged();
    
        // TODO: Put your itemview clicked functionality below
    });
    

注意

  • Using this code creates a toggle effect on items:

          if(selectedPosition == position)
          {
              selectedPosition = -1;
              notifyDataSetChanged();
              return;
          }
    
          selectedPosition = position;
          notifyDataSetChanged();
    
  • Using only this code creates a classical selection way of a list (One item at a time)

          selectedPosition = position;
          notifyDataSetChanged();
    

1
我认为这不是一个好主意,因为当您取消选择一个项目时,onClick监听器将不会被调用,这意味着您的片段将不知道RecyclerView中发生了什么。 - Calvin Ku

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接