锚点
IM的消息列表是种特殊的列表场景,使用时是从后往前滑动,最新消息在底部显示。
常规做法:是从前往后加载出列表后,再滚动到指定位置,但是这样会加载滚动过程中的所有内容。
更优的做法:直接渲染指定位置的内容。
锚点通常指向列表的最后一个元素,但是搜索等场景,也可能指定为列表中的任意位置。
计算锚点
找到设置锚点的位置
-
LinearLayoutManager 中的 updateAnchorInfoForLayout 方法用来生成AnchorInfo。
-
AnchorInfo 的 mPosition 表示锚点对应的index位置,mCoordinate 表示 坐标。
-
粗略的举个例子:微信进入一个会话时,需要显示最新一条消息。此时锚点应该在屏幕底部,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呢?
为了预加载。