即使RecyclerView.findViewHolderForAdapterPosition()在该位置返回null

时间:2016-06-17 16:01:48

标签: android android-recyclerview recycler-adapter android-viewholder

我有一个包含13个项目的列表(虽然可以添加或删除项目),位置0-12。当首次显示包含RecyclerView的片段时,用户只能看到位置0到7(位置7只有一半可见)。在我的适配器中Log每次视图持有者被绑定/绑定(如果语法适用于此,则为idk)并记录其位置。

适配器

@Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
    Log.d(TAG, "onBindViewHolder() position: " + position);
    ...
}

从我的Log我看到0-7的位置受到约束:

Log from Adapter

我有一个selectAll()方法,可以通过适配器位置获取每个ViewHolder。如果返回的holder不是null,我会使用返回的holder更新视图以显示其已被选中。如果返回的持有者是null,我会调用selectOnBind()一个标记该位置的视图的方法更新,以显示它在绑定时被选中,而不是实时,因为它当前未显示:

public void selectAll() {
    for (int i = 0; i < numberOfItemsInList; i++) {
        MyAdapter.ViewHolder holder = (MyAdapter.ViewHolder)
                mRecyclerView.findViewHolderForAdapterPosition(i);

        Log.d(TAG, "holder at position " + i + " is " + holder);

        if (holder != null) {
            select(holder);
        } else {
            selectOnBind(i);
        }
    }
}

在这种方法中,我Log holder及其位置:

Log from selectAll()

所以到目前为止一切看起来都很正常。我们有0-7的位置显示,并且根据Log这些是位置限制。当我在不更改可见视图(滚动)的情况下点击selectAll()时,我看到定义了0-7位置,8-12位置为null。到目前为止一切都很好。

这里有趣的地方。如果在调用selectAll()之后我向下滚动列表位置8和9,则不会显示它们已被选中。

检查Log时,我发现它是因为它们从未被绑定,即使它们被报告为null

Adapter Log after scroll

更令人困惑的是,每次都不会发生这种情况。如果我首先启动应用程序并测试它可能会有效。但事后似乎没有失败。我猜它与被回收的观点有关,但即便如此,它们也不必被束缚?

编辑(6-29-16)
在AndroidStudio更新后,我似乎无法重现该错误。它按照我的预期工作,绑定空视图。如果这个问题重新出现,我将回到这篇文章。

6 个答案:

答案 0 :(得分:15)

发生这种情况是因为:

  • 视图未添加到recyclerview(getChildAt将无效,并将为该位置返回null)
  • 它们也被缓存(onBind不会被调用)

致电recyclerView.setItemViewCacheSize(0)将解决此“问题”。

由于默认值为2(private static final int DEFAULT_CACHE_SIZE = 2;中的RecyclerView.Recycler),因此您将始终获得2个不会调用onBind但未添加到回收站的视图

答案 1 :(得分:6)

在您的情况下,位置8和9的视图未被回收,它们将从窗口分离并将再次附加。对于这些分离视图onBindViewHolder未调用,仅调用onViewAttachedToWindow。如果您在适配器中覆盖这些功能,您可以看到我在说什么。

@Override
    public void onViewRecycled(ViewHolder vh){
        Log.wtf(TAG,"onViewRecycled "+vh);
    }

    @Override
    public void onViewDetachedFromWindow(ViewHolder viewHolder){
        Log.wtf(TAG,"onViewDetachedFromWindow "+viewHolder);
    }

现在为了解决您的问题,您需要跟踪应该回收但已分离的视图,然后执行您的部分过程

@Override
    public void onViewAttachedToWindow(ViewHolder viewHolder){
        Log.wtf(TAG,"onViewAttachedToWindow "+viewHolder);
    }

答案 2 :(得分:2)

Pedro Oliveira和Zartha的回答非常适合理解这个问题,但我没有看到任何我满意的解决方案。

我相信你有2个不错的选择取决于你正在做什么:

选项1

如果您想要onBindViewHolder()调用屏幕外视图,无论它是否已缓存/分离,您都可以执行以下操作:

RecyclerView.ViewHolder view_holder = recycler_view.findViewHolderForAdapterPosition( some_position );

if ( view_holder != null )
{
    //manipulate the attached view
}
else //view is either non-existant or detached waiting to be reattached
    notifyItemChanged( some_position );

这个想法是,如果视图被缓存/分离,那么notifyItemChanged()将告诉适配器视图无效,这将导致调用onBindViewHolder()

选项2

如果您只想执行部分更改(而不是onBindViewHolder()内的所有内容),那么在onBindViewHolder( ViewHolder view_holder, int position )内,您需要将position存储在view_holder中,并在onViewAttachedToWindow( ViewHolder view_holder )中执行您想要的更改。

我建议使用选项1以简化,除非你的onBindViewHolder()正在做一些密集的事情,比如弄乱Bitmaps。

答案 3 :(得分:0)

我认为在recyclelerview中玩视图并不是一个好主意。我总是使用的方法只是为RecyclerView引入一个标志到模型。假设你的模型就像 -

class MyModel{
    String name;
    int age;
}

如果您正在跟踪视图是否被选中,则向模型引入一个布尔值。现在它看起来像 -

class MyModel{
    String name;
    int age;
    boolean isSelected;
}

现在,您的复选框将根据新标志isSelected(在onBindViewHolder()中)进行选择/取消选中。在视图上的每个选择上都会将相应模型选定值的值更改为true,而在未选中时将其更改为false。在你的情况下,只需运行一个循环将所有模型的isSelected值更改为true,然后调用notifyDataSetChanged()

例如,假设您的列表是

ArrayList<MyModel> recyclerList;
private void selectAll(){
    for(MyModel myModel:recyclerList)
        myModel.isSelected = true;
    notifyDataSetChanged();
}

我的建议,在使用recyclerView或ListView时尽量少尝试使用视图。

所以在你的情况下 -

@Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
   holder.clickableView.setTag(position);
   holder.selectableView.setTag(position);
   holder.checkedView.setChecked(recyclerList.get(position).isSelected);
    Log.d(TAG, "onBindViewHolder() position: " + position);
    ...
}

@Override
public void onClick(View view){
    int position = (int)view.getTag();
    recyclerList.get(position).isSelected = !recyclerList.get(position).isSelected;
}

@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
      int position = (int)buttonView.getTag();
      recyclerList.get(position).isSelected = isChecked;
}

希望它能为您提供帮助,如果您需要进一步解释,请与我们联系。)

答案 4 :(得分:0)

所以我认为你的问题在下面由@Pedro Oliveira回答。 RecycleView的主要意义,即他在任何时候都使用特殊算法来缓存ViewHolder。所以接下来onBindViewHolder(...)可能不起作用,例如。如果视图是静态的,或其他东西。

关于您的问题,您认为将RecycleView用于动态更改的视图。 不要做它!因为RecycleView使视图无效并具有缓存系统,所以你会遇到很多问题。

使用LinkedListView执行此任务!

答案 5 :(得分:0)

当列表中有大量项目时,您已传递给recyclerview adapter,则不会遇到onBindViewHolder()在滚动时不执行的问题。

但是如果列表中的项目较少(我检查过列表大小5),则可能会遇到此问题。

更好的解决方案是检查list size

请在下面找到示例代码。

private void setupAdapter(){
    if (list.size() <= 10){
        recycler.setItemViewCacheSize(0);
     }
     recycler.setAdapter(adapter);
     recycler.setLayoutManager(linearLayoutManager);
}