使用DiffUtil和自定义适配器会导致检测到不一致

时间:2019-06-27 09:13:57

标签: android kotlin android-recyclerview

当前,我尝试使用DiffUtil通知项目中RecyclerView的项目。

同时,我编写了一个自定义适配器,可以添加页眉和页脚。 我使用了此适配器,并添加了更多的页脚,以便在向下滚动时列表可以加载更多内容。这导致了一个问题:我的列表可能会加载更多内容,因此将至少容纳一项(加载更多页脚)。

为避免造成误解,下面的“项目”一词将专门表示这些项目不是页眉或页脚。

然后,当我通过diffUtil将项目从n(n> 0)个项目通知为零时,出现了问题,我的应用程序崩溃了。

这里是例外:java.lang.IndexOutOfBoundsException:检测到不一致。无效的视图持有人适配器positionViewHolder

值得一提的是,如果我使用自定义适配器没有页眉或页脚,一切都会好的。

我一直在寻找解决方案,但是他们的情况对我来说都不一样。

以下是自定义适配器的代码(Java),diffUtil将在其中分发 innerAdapter 的更新:

public class WrapperAdapter extends RecyclerView.Adapter {

    // 0x40000000 为 1位 flag 位,1为 HEADER ,0 为 FOOTER
    private static final int HEADER_FOOTER_TYPE_MASK = 0x40000000;
    // 0x3f0000 为 6位 index 位,对应的 HEADER FOOTER 数组的 index ,也就是说最多保存64个 HEADER 、64个 FOOTER
    private static final int HEADER_FOOTER_INDEX_MASK = 0x3f000000;
    // 后面的 24 位为 view 的 hash code 位,可以保证每个 HEADER、FOOTER 都能有不同的 viewType
    private static final int HEADER_FOOTER_HASH_MASK = 0x00ffffff;

    private RecyclerView.Adapter innerAdapter;
    private RecyclerView.AdapterDataObserver innerObserver = new AdapterDataObserverProxy();
    private List<View> headers = new ArrayList<>();
    private List<View> footers = new ArrayList<>();

    public WrapperAdapter(@NonNull RecyclerView.Adapter adapter) {
        innerAdapter = adapter;
    }

    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        if (viewType < 0) {
            if ((viewType & HEADER_FOOTER_TYPE_MASK) != 0) {
                // HEADER
                int headerIndex = (viewType & HEADER_FOOTER_INDEX_MASK) >> 24;
                return new InnerViewHolder(headers.get(headerIndex));
            } else {
                // FOOTER
                int footerIndex = (viewType & HEADER_FOOTER_INDEX_MASK) >> 24;
                return new InnerViewHolder(footers.get(footerIndex));
            }
        }
        return innerAdapter.onCreateViewHolder(parent, viewType);
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        if (isHeader(position) || isFooter(position)) {
            return;
        }

        innerAdapter.onBindViewHolder(holder, position - headers.size());
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List payloads) {
        if (isHeader(position) || isFooter(position)) {
            return;
        }

        innerAdapter.onBindViewHolder(holder, position - headers.size(), payloads);
    }

    @Override
    public int getItemViewType(int position) {
        if (isHeader(position))  {
            return Integer.MIN_VALUE | HEADER_FOOTER_TYPE_MASK | ((position & (HEADER_FOOTER_INDEX_MASK >> 24)) << 24) | (headers.get(position).hashCode() & HEADER_FOOTER_HASH_MASK);
        }

        if (isFooter(position)) {
            int footerIndex = position - innerAdapter.getItemCount() - headers.size();
            return Integer.MIN_VALUE | ((footerIndex & (HEADER_FOOTER_INDEX_MASK >> 24)) << 24) | (footers.get(footerIndex).hashCode() & HEADER_FOOTER_HASH_MASK);
        }

        int innerViewType = innerAdapter.getItemViewType(position - headers.size());
        if (innerViewType < 0) {
            throw new IllegalArgumentException("View type cannot be negative, which is claimed by HEADER and FOOTER");
        }
        return innerViewType;
    }

    @Override
    public int getItemCount() {
        if (innerAdapter.getItemCount() == 0) {
            return headers.size();
        } else {
            return innerAdapter.getItemCount() + headers.size() + footers.size();
        }
    }

    private boolean isHeader(int position) {
        return position < headers.size();
    }

    private boolean isFooter(int position) {
        return position > getItemCount() - footers.size() - 1;
    }

    private class InnerViewHolder extends RecyclerView.ViewHolder {
        InnerViewHolder(@NonNull View itemView) {
            super(itemView);
        }
    }

    public void addHeader(@NonNull View header) {
        if (!headers.contains(header)) {
            headers.add(header);
            notifyItemInserted(headers.size() - 1);
        }
    }

    public void removeHeader(@NonNull View header) {
        if (headers.contains(header)) {
            int index = headers.indexOf(header);
            headers.remove(index);
            notifyItemRemoved(index);
        }
    }

    public void addFooter(@NonNull View footer) {
        if (!footers.contains(footer)) {
            footers.add(footer);
            notifyItemInserted(getItemCount() - 1);
        }
    }

    public void removeFooter(@NonNull View footer) {
        if (footers.contains(footer)) {
            int index = footers.indexOf(footer);
            footers.remove(index);
            notifyItemRemoved(headers.size() + innerAdapter.getItemCount() + index);
        }
    }


    @Override
    public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) {
        if (holder instanceof InnerViewHolder) {
            super.onViewRecycled(holder);
        } else {
            innerAdapter.onViewRecycled(holder);
        }
    }

    @Override
    public boolean onFailedToRecycleView(@NonNull RecyclerView.ViewHolder holder) {
        if (holder instanceof InnerViewHolder) {
            return super.onFailedToRecycleView(holder);
        } else {
            return innerAdapter.onFailedToRecycleView(holder);
        }

    }

    @Override
    public void onViewAttachedToWindow(@NonNull RecyclerView.ViewHolder holder) {
        if (holder instanceof InnerViewHolder) {
            super.onViewAttachedToWindow(holder);
        } else {
            innerAdapter.onViewAttachedToWindow(holder);
        }
    }

    @Override
    public void onViewDetachedFromWindow(@NonNull RecyclerView.ViewHolder holder) {
        if (holder instanceof InnerViewHolder) {
            super.onViewDetachedFromWindow(holder);
        } else {
            innerAdapter.onViewDetachedFromWindow(holder);
        }
    }

    @Override
    public void registerAdapterDataObserver(@NonNull RecyclerView.AdapterDataObserver observer) {
        super.registerAdapterDataObserver(observer);
        innerAdapter.registerAdapterDataObserver(innerObserver);
    }

    @Override
    public void unregisterAdapterDataObserver(@NonNull RecyclerView.AdapterDataObserver observer) {
        super.unregisterAdapterDataObserver(observer);
        innerAdapter.unregisterAdapterDataObserver(innerObserver);
    }

    @Override
    public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
        innerAdapter.onAttachedToRecyclerView(recyclerView);
    }

    @Override
    public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
        innerAdapter.onDetachedFromRecyclerView(recyclerView);
    }

    @Override
    public void setHasStableIds(boolean hasStableIds) {
        super.setHasStableIds(hasStableIds);
        innerAdapter.setHasStableIds(hasStableIds);
    }

    @Override
    public long getItemId(int position) {
        if (isHeader(position)) return super.getItemId(position);
        if (isFooter(position)) return super.getItemId(position);
        return innerAdapter.getItemId(position);
    }

    private class AdapterDataObserverProxy extends RecyclerView.AdapterDataObserver {
        @Override
        public void onChanged() {
            WrapperAdapter.this.notifyDataSetChanged();
        }

        @Override
        public void onItemRangeChanged(int positionStart, int itemCount) {
            WrapperAdapter.this.notifyItemRangeChanged(positionStart + WrapperAdapter.this.headers.size(), itemCount);
        }

        @Override
        public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) {
            WrapperAdapter.this.notifyItemRangeChanged(positionStart + WrapperAdapter.this.headers.size(), itemCount, payload);
        }

        @Override
        public void onItemRangeInserted(int positionStart, int itemCount) {
            WrapperAdapter.this.notifyItemRangeInserted(positionStart + WrapperAdapter.this.headers.size(), itemCount);
        }

        @Override
        public void onItemRangeRemoved(int positionStart, int itemCount) {
            WrapperAdapter.this.notifyItemRangeRemoved(positionStart + WrapperAdapter.this.headers.size(), itemCount);
        }

        @Override
        public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
            WrapperAdapter.this.notifyItemMoved(fromPosition + WrapperAdapter.this.headers.size(), toPosition + WrapperAdapter.this.headers.size());
        }
    }
}

以下是使用自定义适配器的recyclerView的主要代码(科特琳):

    private var wrapperAdapter: WrapperAdapter? = null
    override fun setAdapter(adapter: Adapter<*>?) {
        wrapperAdapter = if (adapter != null) {
            WrapperAdapter(adapter)
        } else {
            null
        }
        super.setAdapter(wrapperAdapter)
    }

    fun addHeader(header: View) {
        wrapperAdapter?.addHeader(header)
    }

    fun addFooter(footer: View) {
        wrapperAdapter?.addFooter(footer)
    }

    fun removeHeader(header: View) {
        wrapperAdapter?.removeHeader(header)
    }

    fun removeFooter(footer: View) {
        wrapperAdapter?.removeFooter(footer)
    }

this question最有用的答案是为我工作的。 但是我认为这只是避免崩溃,而不是解决它以确保它不再引起异常。所以我认为这不是一个好方法。所以我来寻求帮助。

1 个答案:

答案 0 :(得分:1)

getItemCount()的实现来看,似乎只在有项目时才显示页脚。当最后一个项目被删除时,您必须通知包装适配器该页脚也被删除。

说有2个页眉,1个项目,3个页脚:getItemCount()返回6。如果删除内部项目,则仅从内部适配器传播1个删除内容,但是getItemCount()现在返回2预期的5

我建议修改AdapterDataObserverProxy

@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
    WrapperAdapter.this.notifyItemRangeRemoved(positionStart + WrapperAdapter.this.headers.size(), itemCount);
    if (itemCount > 0 && innerAdapter.getItemCount() == 0 && footers.size() > 0) {
        // no more inner items, notify the removal of the footers
        int firstFooterPosition = headers.size();
        int footerCount = footers.size();
        WrapperAdapter.this.notifyItemRangeRemoved(firstFooterPosition, footerCount);
    }
}