美文网首页
RecyclerView添加Header和Footer

RecyclerView添加Header和Footer

作者: freelifes | 来源:发表于2024-05-07 15:02 被阅读0次
    1、出现的3个异常

    1.1、第一个,第二个

    java.lang.IllegalArgumentException: Called attach on a child which is not detached: ViewHolder{3d34d42 position=4 id=-1, oldPos=-1, pLpos:-1} hy.sohu.com.ui_lib.hyrecyclerview.hyrecyclerView.HyRecyclerView{12f2c6 VFED..... ......ID 0,427-720,1430 #7f0903c6 app:id/hyrecyclerview_chat}, adapter:hy.sohu.com.ui_lib.hyrecyclerview.hyrecyclerView.PullToLoadAdapter@58e3153, layout:hy.sohu.com.ui_lib.hyrecyclerview.HyLinearLayoutManager@9c47a90, context:hy.sohu.com.app.chat.view.message.SingleChatMsgActivity@561df6
        at androidx.recyclerview.widget.RecyclerView$5.attachViewToParent(RecyclerView.java:931)
        at androidx.recyclerview.widget.ChildHelper.attachViewToParent(ChildHelper.java:241)
        at androidx.recyclerview.widget.RecyclerView.addAnimatingView(RecyclerView.java:1443)
        at androidx.recyclerview.widget.RecyclerView.animateDisappearance(RecyclerView.java:4371)
        at androidx.recyclerview.widget.RecyclerView$4.processDisappeared(RecyclerView.java:617)
        at androidx.recyclerview.widget.ViewInfoStore.process(ViewInfoStore.java:245)
        at androidx.recyclerview.widget.RecyclerView.dispatchLayoutStep3(RecyclerView.java:4208)
        at androidx.recyclerview.widget.RecyclerView.dispatchLayout(RecyclerView.java:3862)
        at androidx.recyclerview.widget.RecyclerView.onLayout(RecyclerView.java:4404)
        at hy.sohu.com.ui_lib.hyrecyclerview.hyrecyclerView.HyRecyclerView.onLayout(HyRecyclerView.java:1055)
    
    
    java.lang.IllegalArgumentException: Called removeDetachedView with a view which"
                            + " is not flagged as tmp detached." + vh + exceptionLabel()
    
    

    出现位置与原因: ChildHelper
    当向RecyclerView添加一个child时,这个child已经有一个parent。

    // mChildHelper = new ChildHelper
    
     public void attachViewToParent(View child, int index,
                        ViewGroup.LayoutParams layoutParams) {
                    final ViewHolder vh = getChildViewHolderInt(child);
                    if (vh != null) {
                        if (!vh.isTmpDetached() && !vh.shouldIgnore()) {
                            throw new IllegalArgumentException("Called attach on a child which is not"
                                    + " detached: " + vh + exceptionLabel());
                        }
                        if (DEBUG) {
                            Log.d(TAG, "reAttach " + vh);
                        }
                        vh.clearTmpDetachFlag();
                    }
                    RecyclerView.this.attachViewToParent(child, index, layoutParams);
                }
    

    1.2、第三个、Cannot call this method while RecyclerView is computing a layout or scrolling HyRecyclerView。

    java.lang.IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling hy.sohu.com.ui_lib.hyrecyclerview.hyrecyclerView.HyRecyclerView{62a0245 VFED..... ........ 0,131-1080,1996 #7f0903cd app:id/hyrecyclerview_chat}, adapter:hy.sohu.com.ui_lib.hyrecyclerview.hyrecyclerView.PullToLoadAdapter@2f22d9a, layout:hy.sohu.com.ui_lib.hyrecyclerview.HyLinearLayoutManager@a1e86cb, context:hy.sohu.com.app.chat.view.message.SingleChatMsgActivity@8fe39bf
        at androidx.recyclerview.widget.RecyclerView.assertNotInLayoutOrScroll(RecyclerView.java:3062)
        at androidx.recyclerview.widget.RecyclerView$RecyclerViewDataObserver.onItemRangeInserted(RecyclerView.java:5558)
        at androidx.recyclerview.widget.RecyclerView$AdapterDataObservable.notifyItemRangeInserted(RecyclerView.java:12286)
        at androidx.recyclerview.widget.RecyclerView$Adapter.notifyItemRangeInserted(RecyclerView.java:0)
        at hy.sohu.com.ui_lib.hyrecyclerview.hyrecyclerView.HeaderAndFooter.HeaderAndFooterRecyclerView$DataObserver.onItemRangeInserted(HeaderAndFooterRecyclerView.java:348)
        at androidx.recyclerview.widget.RecyclerView$AdapterDataObservable.notifyItemRangeInserted(RecyclerView.java:12286)
        at androidx.recyclerview.widget.RecyclerView$Adapter.notifyItemRangeInserted(RecyclerView.java:0)
        at hy.sohu.com.ui_lib.hyrecyclerview.hyadapter.HyBaseNormalAdapter.addData(HyBaseNormalAdapter.java:163)
        at hy.sohu.com.app.chat.view.message.ChatMsgBaseActivity.onSaveLocalSuccess(ChatMsgBaseActivity.kt:910)
    

    出现位置与原因:
    当调用notifyItemChanged,notifyItemRemove时检测mLayoutOrScrollCounter是否> 0 抛出异常。

     private class RecyclerViewDataObserver extends AdapterDataObserver {
            RecyclerViewDataObserver() {
            }
            
            @Override
            public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
                assertNotInLayoutOrScroll(null);
                if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) {
                    triggerUpdateProcessor();
                }
            }
    
            @Override
            public void onItemRangeInserted(int positionStart, int itemCount) {
                assertNotInLayoutOrScroll(null);
                if (mAdapterHelper.onItemRangeInserted(positionStart, itemCount)) {
                    triggerUpdateProcessor();
                }
            }
    
            @Override
            public void onItemRangeRemoved(int positionStart, int itemCount) {
                assertNotInLayoutOrScroll(null);
                if (mAdapterHelper.onItemRangeRemoved(positionStart, itemCount)) {
                    triggerUpdateProcessor();
                }
            }
     
        }
    
    void assertNotInLayoutOrScroll(String message) {
            if (isComputingLayout()) {
                if (message == null) {
                    throw new IllegalStateException("Cannot call this method while RecyclerView is "
                            + "computing a layout or scrolling" + exceptionLabel());
                }
                throw new IllegalStateException(message);
            }
        }
    
      public boolean isComputingLayout() {
            return mLayoutOrScrollCounter > 0;
      }
    
    
    2、产生原因

    为什么mLayoutOrScrollCounter > 0

    2.1、recyclerView的onLayout

    protected void onLayout(boolean changed, int l, int t, int r, int b) {
            dispatchLayout();
      }
    
    
     void dispatchLayout() {
             mState.mIsMeasuring = false;
            if (mState.mLayoutStep == State.STEP_START) {
                dispatchLayoutStep1();
                mLayout.setExactMeasureSpecsFrom(this);
                dispatchLayoutStep2();
            } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
                    || mLayout.getHeight() != getHeight()) {
                dispatchLayoutStep2();
            } else {
                mLayout.setExactMeasureSpecsFrom(this);
            }
            dispatchLayoutStep3();
     }
    
    

    2.2、看下 dispatchLayoutStep1/2

       private void dispatchLayoutStep 1/2 () {
    
            onEnterLayoutOrScroll();
    
            mLayout.onLayoutChildren(mRecycler, mState);
    
            onExitLayoutOrScroll();
    
        }
    
        void onEnterLayoutOrScroll() {
            mLayoutOrScrollCounter++;
          }
    
        void onExitLayoutOrScroll(boolean enableChangeEvents) {
            mLayoutOrScrollCounter--;
        }
    

      从上面看出每次执行 dispatchLayoutStep1/2开始mLayoutOrScrollCounter++,执行完成mLayoutOrScrollCounter--就等于0,期间执行 mLayout.onLayoutChildren()操作。如果onLayoutChildren出现异常,则mLayoutOrScrollCounter就会大于0。
      而java.lang.IllegalArgumentException就是在mLayout.onLayoutChildren方法执行过程中调用的,所以由于第一个异常未处理,产生了第二个异常,实际不可能同时出现两个crash异常。
       原因 : 自定义的HyLinearLayoutManager被try——catch,导致第一个异常没有导致app闪退,当再次执行onItemRangeRemoved时,产生了第二个异常。所以只需要找到第一个问题的原因则可。

      // HyLinearLayoutManager
        @Override
        public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
            try {
                //try catch一下
                super.onLayoutChildren(recycler, state);
            } catch (Exception e) {
                e.printStackTrace();
            }
    
        }
    
    3、分析mLayout.onLayoutChildren(mRecycler, mState);

    以dispatchLayoutStep2中onLayoutChildren方法为例介绍。


    recyclerview缓存
    3.1、 notifyItemRemove(7)

    正常情况下,是不会出现crash,下面是写demo可以验证。

           btn1.setOnClickListener {
                var stringList = (recyclerView.adapter as ItemAdapter).stringList as LinkedList
                stringList.removeAt(1)
                (recyclerView?.adapter as ItemAdapter).notifyItemRemoved(2)
                Log.d(TAG, "onCreate:--after-- " + recyclerView.isComputingLayout)
            }
            btn2.setOnClickListener {
                adapter.notifyDataSetChanged()
         }
    

    当调用notifyItemRemove(7)时,dispatchLayoutStep2会从mAttachedScrap加载混存的holder。
      3.11、寻找到第0个,加入RecyclerView第0个。
      3.12、寻找到第1个,加入RecyclerView第1个。
      3.13、寻找到第2个,加入RecyclerView第2个。
      3.14、寻找到第3个,此时第三个item的type和mAdapter返回的type不一致。所以找不到,会依据type创建一个holder。

      //getScrapOrHiddenOrCachedHolderForPosition
              for (int i = 0; i < scrapCount; i++) {
                    final ViewHolder holder = mAttachedScrap.get(i);
                    if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
                            && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
                        holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                        return holder;
                    }
                }
    
        //创建
          holder = mAdapter.createViewHolder(RecyclerView.this, type);
    
    

    此时按照type,倒数第二个type为Item_Type_load,会返回一个holder,这个holder的itemview为mLoadView。加入RecyclerView为第三个holder。

        @Override
        public int getItemViewType(int position) {
            if (isLoadPosition(position)) {
                return ITEM_TYPE_LOAD;
            } else if (isBottomPosition(position)) {
                return ITEM_TYPE_BOTTOM;
            }
            return super.getItemViewType(position);
        }
    

    此时mAttachedScrap中有一个持有mLoadView的ViewHolder,RecyclerView中也加入了一个持有LoadView的ViewHolder。
      3.15、寻找第4个,此时找到的事mLoadView的holder,但是这个holder的type和mAdapter返回type不一致,此时需要把holder移除。出现如下crash。因为刚才我们已经加入到recyclerView,移除报错。

       protected void removeDetachedView(View child, boolean animate) {
            ViewHolder vh = getChildViewHolderInt(child);
            if (vh != null) {
                if (vh.isTmpDetached()) {
                    vh.clearTmpDetachFlag();
                } else if (!vh.shouldIgnore()) {
                    throw new IllegalArgumentException("Called removeDetachedView with a view which"
                            + " is not flagged as tmp detached." + vh + exceptionLabel());
                }
            }
        }
    
    3.2、notifyItemRemove(3)

    删除3,但是我们通知3时,当notifyItemRemove(3)。

    缓存

      3.21、寻找到第0个,加入RecyclerView第0个。
      3.22、寻找到第1个,加入RecyclerView第1个。
      3.23、寻找到第2个,加入RecyclerView第2个。
      3.24、寻找到第3个,此时mAttachedScrap中有个2个位置为3个holder,一个是移除holder,一个mLoadView(位置已经做了正确的偏移),找到了holder。
      3.25、寻找到第4个,也找到已经偏移位置的line。加入到RecyclerView中,不会出现任何问题。

    3.3、notifyItemRemove(4)

      3.3.1、寻找到第0个,加入RecyclerView第0个。
      3.3.2、寻找到第1个,加入RecyclerView第1个。
      3.3.3、寻找到第2个,加入RecyclerView第2个。
      3.3.4、寻找到第3个,此时mAttachedScrap中找到的holder的type和mAdapter需要的type不一致,所以会创建一个holder加入进去。我们上面知道加入的事mLoadView。
       此时mAttachedScrap和RecyclerView中各有一个持有同一个mLoadView的ViewHolder。
      3.3.5、寻找到第4个,也找到已经偏移位置的line。加入到RecyclerView中,不会出现任何问题。因为在这个位置找到了holder,不需要移除持有mLoadView的viewHolder,所以不会报第一个中的异常。
      3.3.6、执行动画。


    动画

    对于loadViewHolder执行消失动画。

     void animateDisappearance(@NonNull ViewHolder holder,
                @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) {
            addAnimatingView(holder);
            holder.setIsRecyclable(false);
            if (mItemAnimator.animateDisappearance(holder, preLayoutInfo, postLayoutInfo)) {
                postAnimationRunner();
            }
        }
    
         private void addAnimatingView(ViewHolder viewHolder) {
            final View view = viewHolder.itemView;
            final boolean alreadyParented = view.getParent() == this;
            mRecycler.unscrapView(getChildViewHolder(view));
            if (viewHolder.isTmpDetached()) {
                // re-attach
                mChildHelper.attachViewToParent(view, -1, view.getLayoutParams(), true);
            } else if (!alreadyParented) {
                mChildHelper.addView(view, true);
            } else {
                mChildHelper.hide(view);
            }
        }
    

    mChildHelper.attachViewToParent就抛出了crash。因为loadViewHolder所持有的ItemView已经被加入到了RecyclerView。

    4、解决方案
    私信
       public void removeData(String msgId) {
            int index = -1;
            for (Iterator iterable = getDatas().iterator(); iterable.hasNext(); ) {
                ChatMsgBean chatMsgBaseBean = (ChatMsgBean) iterable.next();
                index++;
                if (!TextUtils.isEmpty(msgId) && msgId.equals(chatMsgBaseBean.msgId)) {
                    try {
                        iterable.remove();
                        notifyItemRemoved(index);
                        if (index > 0) {
                            notifyItemChanged(index - 1);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
    
                }
            }
        }
    

    上述代码执行
      notifyItemRemoved(0) 执行完只剩1条数据。
      notifyItemRemoved(1);
    当执行notifyItemRemoved(1)时,此时list只有一条数据,就会导致上面notifyItemRemoved(4)的场景。
    这里只列举删除,当然增删改查都可能导致这个问题。

     public void removeData(String msgId) {
            int index = -1;
            List<ChatMsgBean> datas = getDatas();
    
            for (int i = 0; i < datas.size(); i++) {
                ChatMsgBean chatMsgBean = datas.get(i);
                if (!TextUtils.isEmpty(msgId) && msgId.equals(chatMsgBean.msgId)) {
                    index = i;
                    break;
                }
            }
    
            if (index != -1) {
                datas.remove(index);
                notifyItemRemoved(index);
            }
        }
    

      4.1、避免每次创建的viewholder和缓存中的ViewHolder持有同一个View.

    
       //PullToLoadAdapter
        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            if (viewType == ITEM_TYPE_LOAD) {
                mRealLoadView = createWrapView(mLoadView);
                return new RecyclerView.ViewHolder(mRealLoadView) {
                };
            } else if (viewType == ITEM_TYPE_BOTTOM) {
                mRealBottomView = createWrapView(mBottomView);
                return new RecyclerView.ViewHolder(mRealBottomView) {
                };
            }
    
            return super.onCreateViewHolder(parent, viewType);
        }
    
     // HeaderAndFooterAdapter
    @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    //        如果是头部
            if (isHeaderType(viewType)) {
                int headerPosition = mHeaderViews.indexOfKey(viewType);
                View headerView = mHeaderViews.valueAt(headerPosition);
    //            if (mOnCreateHeaderViewHolderListener != null && headerView != null
    //                    && headerView.getTag() != null && headerView.getTag() instanceof Integer) {
    //                if ((int) headerView.getTag() > 0) {
    //                    return mOnCreateHeaderViewHolderListener.onCreateHeaderViewHolder(parent, viewType, (int) headerView
    //                    .getTag());
    //                }
    //            }
                return createHeaderAndFooterViewHolder(createWrapView(headerView));
            }
    //        如果是placeHolder
            if (isPlaceHolderType(viewType)) {
                View view = LayoutInflater.from(mContext.getApplicationContext()).inflate(R.layout.placeholer_recyclerview, null,
                        false);
                return new HyPlaceHolderView(view);
            }
    //        如果是尾部
            if (isFooterType(viewType)) {
                int footerPosition = mFooterViews.indexOfKey(viewType);
                View footerView = mFooterViews.valueAt(footerPosition);
                return createHeaderAndFooterViewHolder(createWrapView(footerView));
            }
            return mRealAdapter.onCreateViewHolder(parent, viewType);
        }
    
    
     protected FrameLayout createWrapView(View view) {
            FrameLayout frameLayout = new FrameLayout(mContext);
            ViewParent parent = view.getParent();
            LogUtil.d("HyRecyclerView",
                    "onCreateViewHolder_0: " + parent + "  " + view.getTag() + " params.h =" + view.getLayoutParams() + "  " +
                            "visiable=  " + view.getVisibility());
            if (parent != null && parent instanceof ViewGroup) {
                ViewGroup viewGroup = (ViewGroup) parent;
                viewGroup.removeView(view);
            }
            frameLayout.setVisibility(view.getVisibility());
            if (view.getLayoutParams() != null) {
                ViewGroup.LayoutParams frlayoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                        view.getLayoutParams().height);
                frameLayout.setLayoutParams(frlayoutParams);
            } else {
                ViewGroup.LayoutParams frlayoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                        ViewGroup.LayoutParams.WRAP_CONTENT);
                frameLayout.setLayoutParams(frlayoutParams);
            }
            frameLayout.addView(view);
            return frameLayout;
        }
    

    因为topView和mRefreshView用于刷新,bottomView和loadView用于加载,因此
    添加了一层,在PullToRefreshRecyclerView和HyRecyclerView里面都需要替换控件,并且将bottomView等属性赋值包裹的View。
      4.2、从上面可知,因为在对应position没有获取到holder,所以新创建了一个。想办法从mAttachscrap中获取缓存。

     ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                    boolean dryRun, long deadlineNs) {
                boolean fromScrapOrHiddenOrCache = false;
                ViewHolder holder = null;
                // 0) If there is a changed scrap, try to find from there
                if (mState.isPreLayout()) {
                    holder = getChangedScrapViewForPosition(position);
                    fromScrapOrHiddenOrCache = holder != null;
                }
                // 1) Find by position from scrap/hidden list/cache
                if (holder == null) {
                    holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
                    if (holder != null) {
                        if (!validateViewHolderForOffsetPosition(holder)) {
                            // recycle holder (and unscrap if relevant) since it can't be used
                            if (!dryRun) {
                                // we would like to recycle this but need to make sure it is not used by
                                // animation logic etc.
                                holder.addFlags(ViewHolder.FLAG_INVALID);
                                if (holder.isScrap()) {
                                    removeDetachedView(holder.itemView, false);
                                    holder.unScrap();
                                } else if (holder.wasReturnedFromScrap()) {
                                    holder.clearReturnedFromScrapFlag();
                                }
                                recycleViewHolderInternal(holder);
                            }
                            holder = null;
                        } else {
                            fromScrapOrHiddenOrCache = true;
                        }
                    }
                }
                if (holder == null) {
                    final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                    if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
                        throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
                                + "position " + position + "(offset:" + offsetPosition + ")."
                                + "state:" + mState.getItemCount() + exceptionLabel());
                    }
    
                    final int type = mAdapter.getItemViewType(offsetPosition);
                    // 2) Find from scrap/cache via stable ids, if exists
                    if (mAdapter.hasStableIds()) {
                        holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                                type, dryRun);
                        if (holder != null) {
                            // update position
                            holder.mPosition = offsetPosition;
                            fromScrapOrHiddenOrCache = true;
                        }
                    }
    }
    

    处理给adapter的item设置唯一的ID,重写geiItemID方法。

       mAdapter.setHasStableIds(true);
    
        @Override
        public long getItemId(int position) {
            return super.getItemId(position);
        }
    
    总结

      添加header和footer时,由于缓存复用,避免创建的Viewholder持有同一个View。

    相关文章

      网友评论

          本文标题:RecyclerView添加Header和Footer

          本文链接:https://www.haomeiwen.com/subject/gnmffjtx.html