您现在的位置是:首页 >学无止境 >【RecyclerView】同时刷新和滚动导致,滚动位置异常(一)网站首页学无止境
【RecyclerView】同时刷新和滚动导致,滚动位置异常(一)
前置:
被选中item高度与非选中item高度不一致,且硬件有点卡,运行会有一定卡顿。
可视界面的item为三个,总数据为十个。
期望效果:
=》
实际上效果:
代码:
mListAdapter.setSelectedPosition(position);
mListView.post(() -> {
mListView.smoothScrollToPosition(position);
});
public void setSelectedPosition(int position) {
if (mSelectedPosition == position) {
return;
}
mSelectedPosition = position;
mSelectedSubPoiPosition = -1;
if (0 > mSelectedPosition) {
return;
}
if (mSelectedPosition >= getItemCount()) {
return;
}
notifyDataSetChanged();
}
代码很简单,先notify在进行scroll,看代码是不会出现这个问题,后来进行初步调试,推测为view未渲染完毕,进行滚动,导致滚动的位置与实际偏差。(选中和未选中的item高度不一致)
一.解决方案
public void setSelectedPosition(int position) {
if (mSelectedPosition == position) {
return;
}
int mLastSelectPos=mSelectedPosition;
mSelectedPosition = position;
mSelectedSubPoiPosition = -1;
if (0 > mSelectedPosition) {
return;
}
if (mSelectedPosition >= getItemCount()) {
return;
}
//修改为:范围刷新
notifyItemRangeChanged(Math.min(mLastSelectPos, mSelectedPosition), Math.abs(mLastSelectPos - mSelectedPosition) + 1);//受影响的itemd都刷新下
}
mListView.post(() -> {
//TODO 添加代码 把刷新动画end,设置为null也会失败,但是end有用
RecyclerView.ItemAnimator itemAnimator = mListView.getItemAnimator();
if (itemAnimator != null) {
itemAnimator.endAnimations();
}
mListView.smoothScrollToPosition(scrollPosition);
});
二.recyclerView的刷新
我的思路是,首先对控件的加载进行优化,现针对notify刷新,其次为滚动动画。
1. 刷新全部的item,notifyDataSetChanged()
2. 刷新指定的item,notifyItemChanged(int)
3. 从指定的位置开始刷新指定个item,notifyItemRangeChanged(int,int) 这个刷新onBindViewHolder方法,position才能保持一直
4. 局部刷新指定的数据,notifyItemChanged(int, Object)
根据浏览器和源码的结果,主要分为以上四种刷新方式。
1.notifyItemChanged
1.1 notifyItemChanged(position)
想先使用notifyItemChanged,指定刷新本次选中和上次选中的item,精准打击,这样刷新是最节约资源的。
notifyItemChanged(mLastSelectPos);
notifyItemChanged(mSelectedPosition);
onBindViewHolder也只会调用俩次。
但是加上滚动,且滚动的目标位置距离过远,导致中间加载的view数量多,滚动的位置也会出现偏差。并未解决到BUG。
1.2 notifyItemChanged(position,payloads)
payloads我理解为一个标识符,在onBindViewHolder的方法中进行判断,针对item的控件单独刷新,其余并无区别。
@Override
public final void onBindViewHolder(VH holder, int position, List<Object> payloads) {
}
用法举个栗子:
//"a"和"b"都是标识符,可以换成其他的
notifyItemChanged(mLastSelectPos,"a");
notifyItemChanged(mSelectedPosition,"b");
@Override
public final void onBindViewHolder(VH holder, int position, List<Object> payloads) {
if (payloads.get(0).equals("a")){
((Item) holder).getmNameTv().setText("我是A");
}else if (payloads.get(0).equals("b")){
((Item) holder).getmNameTv().setText("我是B");
}
}
1.3 它们终归指向何处?
查看源码都指向了 notifyItemRangeChanged !
2.notifyItemRangeChanged
先看参数源码解释:
positionStart :刷新开始的位置/下标
itemCount:从positionStart开始数itemCount个数需要刷新。
用法:
notifyItemRangeChanged(Math.min(mLastSelectPos, mSelectedPosition), Math.abs(mLastSelectPos - mSelectedPosition) + 1)
举个栗子:
notifyItemRangeChanged(0,3),刷新下标:012
notifyItemRangeChanged(3,5),刷新下标:23456
3.刷新流程=》Observable
我们现在知道是由Observable促成刷新,来看下如何进行注册。
首先进行注册
RecyclerView 会一开始new一个RecyclerViewDataObserver进行注册。
private final RecyclerViewDataObserver mObserver = new RecyclerViewDataObserver();
....
public void setAdapter(@Nullable Adapter adapter) {
// bail out if layout is frozen
setLayoutFrozen(false);
setAdapterInternal(adapter, false, true);
processDataSetCompletelyChanged(false);
requestLayout();
}
....
private void setAdapterInternal(@Nullable Adapter adapter, boolean compatibleWithPrevious,
boolean removeAndRecycleViews) {
if (mAdapter != null) {
mAdapter.unregisterAdapterDataObserver(mObserver);
mAdapter.onDetachedFromRecyclerView(this);
}
if (!compatibleWithPrevious || removeAndRecycleViews) {
removeAndRecycleViews();
}
mAdapterHelper.reset();
final Adapter oldAdapter = mAdapter;
mAdapter = adapter;
if (adapter != null) {
adapter.registerAdapterDataObserver(mObserver);
adapter.onAttachedToRecyclerView(this);
}
if (mLayout != null) {
mLayout.onAdapterChanged(oldAdapter, mAdapter);
}
mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
mState.mStructureChanged = true;
}
...
我们来看下RecyclerViewDataObserver里面的方法对应RecyclerView的哪些调用。
notifyDataSetChanged() =>
RecyclerViewDataObserver.onChanged()
notifyItemChange()/notifyItemRangeChanged() =>
RecyclerViewDataObserver.onItemRangeChanged();
先看下最开始调用的RecyclerViewDataObserver.onChanged()里做了什么事。
private class RecyclerViewDataObserver extends AdapterDataObserver {
@Override
public void onChanged() {
assertNotInLayoutOrScroll(null);
mState.mStructureChanged = true;
// 清空mCachedViews
processDataSetCompletelyChanged(true);
// 刷新UI
if (!mAdapterHelper.hasPendingUpdates()) {
requestLayout();
}
}
}
void processDataSetCompletelyChanged(boolean dispatchItemsChanged) {
mDispatchItemsChangedEvent |= dispatchItemsChanged;
mDataSetHasChangedAfterLayout = true;
//添加标识符
markKnownViewsInvalid();
}
调用markKnownViewsInvalid()使用for循环给mCachedViews
的每个viewHolder添加标识符FLAG_UPDATE或者FLAG_INVALID。
// RecyclerView.java/Recycler
void markKnownViewsInvalid() {
// 遍历缓存的view设置标志
int cachedCount = this.mCachedViews.size();
for(int i = 0; i < cachedCount; ++i) {
RecyclerView.ViewHolder holder = (RecyclerView.ViewHolder)this.mCachedViews.get(i);
if (holder != null) {
holder.addFlags(6);
holder.addChangePayload((Object)null);
}
}
// 决定回收和清除view的关键是 是否设置的stableIds
if (RecyclerView.this.mAdapter == null || !RecyclerView.this.mAdapter.hasStableIds()) {
// 将mCacheViews中缓存的viewHolder全部移入mRecyclerPool中
this.recycleAndClearCachedViews();
}
}
下一步查看了网上的相关博客,我们可以倒回去看requestLayout()刷新布局,会执行onMeasure()
onLayout(),那么势必就涉及到viewHolder的复用问题,而在填充fill之前,会在之前
执行 detachAndScrapAttachedViews 方法进行缓存。