IM专用 LinearLayoutManager 改造

70 阅读2分钟

锚点

IM的消息列表是种特殊的列表场景,使用时是从后往前滑动,最新消息在底部显示。

​ 常规做法:是从前往后加载出列表后,再滚动到指定位置,但是这样会加载滚动过程中的所有内容。

​ 更优的做法:直接渲染指定位置的内容。

锚点通常指向列表的最后一个元素,但是搜索等场景,也可能指定为列表中的任意位置。

计算锚点

找到设置锚点的位置

  1. LinearLayoutManager 中的 updateAnchorInfoForLayout 方法用来生成AnchorInfo。

  2. AnchorInfo 的 mPosition 表示锚点对应的index位置,mCoordinate 表示 坐标。

  3. 粗略的举个例子:微信进入一个会话时,需要显示最新一条消息。此时锚点应该在屏幕底部,mPosition对应最后一条消息,mCoordinate则应该是列表组建的高度。

    mPosition = list.size()-1
    mCoordinate = mOrientationHelper.getEndAfterPadding(); //LinearlayoutManager中的做法
    

自定义锚点位置

修改updateAnchorInfoForLayout方法,在anchorInfo.assignCoordinateFromPadding之后增加修正逻辑,自己计算锚点配置。

    private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state,
                                           AnchorInfo anchorInfo) {
        //...
        anchorInfo.assignCoordinateFromPadding();
        calculateAnchorOffset(recycler,state,anchorInfo);
        if(isIMMode){
            int maxSafeIndex = Math.max(state.getItemCount()-1,0);
            anchorInfo.mPosition = firstTarget == -1 ? maxSafeIndex : Math.min(firstTarget,maxSafeIndex);
        }else{
            anchorInfo.mPosition = this.mStackFromEnd ? state.getItemCount()-1 : 0;
        }
    }
    
    
    /**
     * 计算锚点位置的偏移量
     * 当设置为IM模式时
     * 如果锚点后面的内容足够一屏幕,则偏移量为0
     * 否则 如果锚点前面的内容足够一屏幕,则偏移量为容器高度
     * 否则 偏移量为前面所有view的总高度
     * @param recycler
     * @param state
     * @param anchorInfo
     */
    private void calculateAnchorOffset(RecyclerView.Recycler recycler, RecyclerView.State state,
                                           AnchorInfo anchorInfo){
        //im模式且有内容时
        if(isIMMode && state.getItemCount()>0){
            int totalSize = mOrientationHelper.getEndAfterPadding();
            int totalCount = state.getItemCount();
            int targetPosition = firstTarget == -1? state.getItemCount()-1:firstTarget;
            int totalConsumed = 0;

            int targetHeight = 0;
            for(int i = targetPosition;i<totalCount;i++){
                View view = recycler.getViewForPosition(i);
                this.measureChildWithMargins(view, 0, 0);
                int consumed = mOrientationHelper.getDecoratedMeasurement(view);
                if(i == targetPosition){
                    targetHeight = consumed;
                }
                totalConsumed += consumed;
                if(totalConsumed >= totalSize){
                    anchorInfo.mCoordinate = this.mStackFromEnd ? targetHeight : 0;
                    return;
                }
            }

            totalConsumed = 0;
            
            for(int i = targetPosition;i>=0;i--){
                View view = recycler.getViewForPosition(i);
                this.measureChildWithMargins(view, 0, 0);
                int consumed = mOrientationHelper.getDecoratedMeasurement(view);
                totalConsumed += consumed;
                if(totalConsumed >= totalSize){
                    anchorInfo.mCoordinate = totalSize;
                    return;
                }
            }
            anchorInfo.mCoordinate = totalConsumed;
        }
    }

移除自带的修正逻辑

按照前面的步骤,理应能够正确展示指定的锚点位置,实际上并不能,因为后续代码会自动修正为 列表内容 贴顶部或者贴底部。

//onLayoutChildren 方法的最后有段逻辑需要调整下,im模式下,内容紧贴顶部。

        if (getChildCount() > 0) {
            if(isIMMode){
                int fixOffset = fixLayoutEndGap(endOffset, recycler, state, false);
                startOffset += fixOffset;
                endOffset += fixOffset;
                fixOffset = fixLayoutStartGap(startOffset, recycler, state, true);
                startOffset += fixOffset;
                endOffset += fixOffset;
            }else{
                // because layout from end may be changed by scroll to position
                // we re-calculate it.
                // find which side we should check for gaps.
                if (mShouldReverseLayout ^ mStackFromEnd) {
                    int fixOffset = fixLayoutEndGap(endOffset, recycler, state, true);
                    startOffset += fixOffset;
                    endOffset += fixOffset;
                    fixOffset = fixLayoutStartGap(startOffset, recycler, state, false);
                    startOffset += fixOffset;
                    endOffset += fixOffset;
                } else {
                    int fixOffset = fixLayoutStartGap(startOffset, recycler, state, true);
                    startOffset += fixOffset;
                    endOffset += fixOffset;
                    fixOffset = fixLayoutEndGap(endOffset, recycler, state, false);
                    startOffset += fixOffset;
                    endOffset += fixOffset;
                }
            }
        }

反向加载

为什么使用mStackFromEnd呢?

为了预加载。