onMeasure
recyclerView建议使用自动测量机制。也就是mAutoMeasure = true, 本文就按照自动测量模式研究测量,布局过程。
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout.isAutoMeasureEnabled()) {
final int widthMode = View.MeasureSpec.getMode(widthSpec);
final int heightMode = View.MeasureSpec.getMode(heightSpec);
//1 : 第一次测量 , 测量大小wrap-> min(size,parentSize) match -> parentsize
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
//2 :match_parent后续就不执行了。
final boolean measureSpecModeIsExactly =
widthMode == View.MeasureSpec.EXACTLY && heightMode == View.MeasureSpec.EXACTLY;
if (measureSpecModeIsExactly || mAdapter == null) {
return;
}
if (mState.mLayoutStep == RecyclerView.State.STEP_START) {
dispatchLayoutStep1();
}
// set dimensions in 2nd step. Pre-layout should happen with old dimensions for
// consistency
//3: 记录old dimension给 pre Layout
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
dispatchLayoutStep2();
//4.布局完成,依据child大小,再次计算recycler的大小。最大为parentSize, 小于parentSize则为childSize;
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
// if RecyclerView has non-exact width and height and if there is at least one child
// which also has non-exact width & height, we have to re-measure.
if (mLayout.shouldMeasureTwice()) {
mLayout.setMeasureSpecs(
View.MeasureSpec.makeMeasureSpec(getMeasuredWidth(), View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(getMeasuredHeight(), View.MeasureSpec.EXACTLY));
mState.mIsMeasuring = true;
dispatchLayoutStep2();
// now we can get the width and height from the children.
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
}
}
}
onMeasure过程
如果widthSpec,heightSpec都为match_parent时候, dispatchLayoutStep1/2都不会执行了,就会再onLayout中执行,此时recyclerView大小为parentSize 。 如果宽高有一个wrap_content,则会执行上述,然后通过child的所需大小,设置recyclerView的大小。
recycler大小 = Math.min(childNeedSize,parentSize)
dispatchLayoutStep1做了什么了?
private void dispatchLayoutStep1() {
processAdapterUpdatesAndSetAnimationFlags();
mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged;
mState.mInPreLayout = mState.mRunPredictiveAnimations;
if (mState.mRunSimpleAnimations) {
// Step 0: Find out where all non-removed items are, pre-layout
int count = mChildHelper.getChildCount();
for (int i = 0; i < count; ++i) {
final RecyclerView.ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
if (holder.shouldIgnore() || (holder.isInvalid() && !mAdapter.hasStableIds())) {
continue;
}
final RecyclerView.ItemAnimator.ItemHolderInfo animationInfo = mItemAnimator
.recordPreLayoutInformation(mState, holder,
RecyclerView.ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
holder.getUnmodifiedPayloads());
mViewInfoStore.addToPreLayout(holder, animationInfo);
if (mState.mTrackOldChangeHolders && holder.isUpdated() && !holder.isRemoved()
&& !holder.shouldIgnore() && !holder.isInvalid()) {
long key = getChangedHolderKey(holder);
mViewInfoStore.addToOldChangeHolders(key, holder);
}
}
}
if (mState.mRunPredictiveAnimations) {
// Step 1: run prelayout: This will use the old positions of items. The layout manager
// is expected to layout everything, even removed items (though not to add removed
// items back to the container). This gives the pre-layout position of APPEARING views
// which come into existence as part of the real layout.
// Save old positions so that LayoutManager can run its mapping logic.
saveOldPositions();
final boolean didStructureChange = mState.mStructureChanged;
mState.mStructureChanged = false;
// temporarily disable flag because we are asking for previous layout
mLayout.onLayoutChildren(mRecycler, mState);
mState.mStructureChanged = didStructureChange;
for (int i = 0; i < mChildHelper.getChildCount(); ++i) {
final View child = mChildHelper.getChildAt(i);
final RecyclerView.ViewHolder viewHolder = getChildViewHolderInt(child);
if (viewHolder.shouldIgnore()) {
continue;
}
if (!mViewInfoStore.isInPreLayout(viewHolder)) {
int flags = RecyclerView.ItemAnimator.buildAdapterChangeFlagsForAnimations(viewHolder);
boolean wasHidden = viewHolder
.hasAnyOfTheFlags(RecyclerView.ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
if (!wasHidden) {
flags |= RecyclerView.ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
}
final RecyclerView.ItemAnimator.ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(
mState, viewHolder, flags, viewHolder.getUnmodifiedPayloads());
if (wasHidden) {
recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo);
} else {
mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
}
}
}
clearOldPositions();
} else {
clearOldPositions();
}
mState.mLayoutStep = RecyclerView.State.STEP_LAYOUT;
}
1、processAdapterUpdatesAndSetAnimationFlags()
一、执行Adapter的Add,Remove,Move,Update操作,偏移holder的position.和 设置recycler缓存的holder的position。
如果支持PredictiveAnimations : 会将update和remove这些操作推迟到第二次layout阶段执行。比如remove操作,还没有layout完成,没有展示出来,holder已经被remove了,视觉体验不好。
mState.mRunSimpleAnimations = mFirstLayoutComplete
&& mItemAnimator != null
&& (mDataSetHasChangedAfterLayout
|| animationTypeSupported
|| mLayout.mRequestedSimpleAnimations)
&& (!mDataSetHasChangedAfterLayout
|| mAdapter.hasStableIds());
mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations
&& animationTypeSupported
&& !mDataSetHasChangedAfterLayout
&& predictiveItemAnimationsEnabled();
二、设置动画的Flag
SimpleAnimations : item改变,faded in/out淡入淡出。
PredictiveItemAnimations : item的离开和出现,比如add,removed, moved动画
三、可以看到mRunSimpleAnimations 判断是mFirstLayoutComplete为true,意味着第一次layout已经完成。onLayout调用完成。
四 、PredictiveAnimations依赖于simpleAnimation的返回值,因此也是OnLayout完成后的动画。mDataSetHasChangedAfterLayout 只有在调用notifyDatasetChanged时候赋值为true.
2、记录动画信息
mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged;
mState.mInPreLayout = mState.mRunPredictiveAnimations;
mViewInfoStore.addToPreLayout(holder, animationInfo);
mViewInfoStore.addToOldChangeHolders(key, holder);
mLayout.onLayoutChildren(mRecycler, mState);
mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
一、如果itemchange并且simpleAnimale为true,则要记录old viewHolder。
二、simpleAnimation = true,此时的viewholder的position都是之前数据和old position。记录holder的animationInfo,对象保存着left,top,right,bottom。flag为FLAG_PRE
三、predictiveAnimatio = true,onLayoutChildren(),如果有新的holder, 记录holder信息,添加flag为 FLAG_APPEAR标识。
dispatchLayoutStep2做了什么了?
private void dispatchLayoutStep2() {
mAdapterHelper.consumeUpdatesInOnePass();
mState.mInPreLayout = false;
mLayout.onLayoutChildren(mRecycler, mState);
mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
}
一、执行adapter的add,remove,update,move操作,计算真正的holder的position位置和缓存position. 通知layoutmanager有add,remove 等操作。
二、设置prelayout为false,第二次,也是确定最终位置,layoutchildren(),这个方法后续会再详细介绍。主要就是寻找holder的itemview,然后Measure和layout。
onLayout
void dispatchLayout() {
mState.mIsMeasuring = false;
if (mState.mLayoutStep == RecyclerView.State.STEP_START) {
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
|| mLayout.getHeight() != getHeight()) {
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else {
mLayout.setExactMeasureSpecsFrom(this);
}
dispatchLayoutStep3();
}
在onLayout阶段,如果之前宽高全是match_parent则dispatchLayoutStep1/2在此阶段执行,并且设置recyclerview的宽高为match_parent。此阶段isMeasureing为false,因为recycler的大小已经知道了。
dispatchLayoutStep3做了什么?
执行动画,在dispatchLayoutStep1,布局之前(preLayout)阶段记录holder的ItemInfo。dispatchLayoutStep2真正布局完成。dispatchLayoutStep3 再次记录布局完成holder的itemInfo,执行动画。
private void dispatchLayoutStep3() {
mState.mLayoutStep = RecyclerView.State.STEP_START;
if (mState.mRunSimpleAnimations) {
// Step 3: Find out where things are now, and process change animations.
// traverse list in reverse because we may call animateChange in the loop which may
// remove the target view holder.
for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
RecyclerView.ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
if (holder.shouldIgnore()) {
continue;
}
long key = getChangedHolderKey(holder);
final RecyclerView.ItemAnimator.ItemHolderInfo animationInfo = mItemAnimator
.recordPostLayoutInformation(mState, holder);
RecyclerView.ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);
if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) {
// run a change animation
// If an Item is CHANGED but the updated version is disappearing, it creates
// a conflicting case.
// Since a view that is marked as disappearing is likely to be going out of
// bounds, we run a change animation. Both views will be cleaned automatically
// once their animations finish.
// On the other hand, if it is the same view holder instance, we run a
// disappearing animation instead because we are not going to rebind the updated
// VH unless it is enforced by the layout manager.
final boolean oldDisappearing = mViewInfoStore.isDisappearing(
oldChangeViewHolder);
final boolean newDisappearing = mViewInfoStore.isDisappearing(holder);
if (oldDisappearing && oldChangeViewHolder == holder) {
// run disappear animation instead of change
mViewInfoStore.addToPostLayout(holder, animationInfo);
} else {
final RecyclerView.ItemAnimator.ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout(
oldChangeViewHolder);
// we add and remove so that any post info is merged.
mViewInfoStore.addToPostLayout(holder, animationInfo);
RecyclerView.ItemAnimator.ItemHolderInfo postInfo = mViewInfoStore.popFromPostLayout(holder);
if (preInfo == null) {
handleMissingPreInfoForChangeError(key, holder, oldChangeViewHolder);
} else {
animateChange(oldChangeViewHolder, holder, preInfo, postInfo,
oldDisappearing, newDisappearing);
}
}
} else {
mViewInfoStore.addToPostLayout(holder, animationInfo);
}
}
mViewInfoStore.process(mViewInfoProcessCallback);
}
mLayout.removeAndRecycleScrapInt(mRecycler);
mState.mPreviousLayoutItemCount = mState.mItemCount;
mDataSetHasChangedAfterLayout = false;
mDispatchItemsChangedEvent = false;
mState.mRunSimpleAnimations = false;
mState.mRunPredictiveAnimations = false;
mLayout.mRequestedSimpleAnimations = false;
if (mRecycler.mChangedScrap != null) {
mRecycler.mChangedScrap.clear();
}
}
一、如果支持simpleAnimations 则遍历所有holder, 如果这个holder在dispatchLayoutStep1中保存过(必须是flag是update,notremove的),则执行 animateChange动画。否则直接addToPostLayout,记录在在Map<holder,itemInfo>中。
二、执行动画。依据record.flags
1、如果holder 在布局之前存在,布局之后也存在,则
Flag 为 FLAG_PRE_AND_POST 动画为
callback.processPersistent(viewHolder, record.preInfo, record.postInfo);
2、如果holder在布局之前不存在,布局之后存在或者可见。简单理解新加的holder。 但是之前不存在holder不代表为null。官方解释说,已经被layoutManger加进去了,只是不是recycler的child。
callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
3、如果holder布局之前存在,布局之后不存在。动画为
callback.processDisappeared(viewHolder, record.preInfo, null);
4、如果holder布局前后都不存在,但是有出现和消失场景,此时回调如下,没有对应的动画,remove了child,回收child.
callback.unused(viewHolder);
5、FLAG_APPEAR_PRE_AND_POST holder在布局之前存在,布局之后存在,且有出现动画。这种情况可以理解为holder被加入了recyclerview但是不可见,布局之后可见了,且有出现动画。调用与 2 相同。recyclerview目前没有逻辑可以走到这个case. 因为FLAG_APPEAR加入时,必须不在preLayout中。
if(!mViewInfoStore.isInPreLayout(viewHolder)){
if (wasHidden) {
recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo);
} else { // 添加FLAG_APPEAR
mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
}
}
如果要造这种场景,可以考虑自定义缓存的时候,添加这种Flag。
notifydata和动画关系
1、notifyitemChange(index)。
index 动画---- animateChange() oldviewholder离开动画,添加viewholder动画。本来应该是dispappear + appear + pre + post 动画,但是recycler直接animateChange()动画,没有使用Flag标识。
非index动画 --- processPersistent().
2、notifyDataSetChanged() ,view从pool中取,如果数据的viewtype未改变,布局前holder存在,布局后holder也存在,因此调用 processPersistent()。viewtype改变了,则创建新的view, 就执行processAppeared()。
3、notifyItemRemoved(index)
index 动画 :processDisappeared()
新增item:processAppeared()
其他动画:processPersistent()
4、notifyItemRangeInserted(index)
index 动画: processAppeared()
非index动画:processPersistent ()
离开屏幕的item: processDisappeared()
关于notifyItemRemove的IndexOutOfBounds问题
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());
}
}
这里是缓存策略里面寻找viewholder, 如果能找到viewholder,则不会执行到这里。
第一种解决办法:从changeScrap里面获取。
adapter.mStringList.removeAt(position)
adapter.notifyItemRemoved(position)
adapter.notifyItemRangeChanged(position,itemcount);
itemcount的值可以是findlastVisiablecount - position。 也可以mAdapter.getItemcount, rv.getChildCount,这个数字大于findlastVisiablecount - position就可以了,更新只会更新已创建的item。notifyItemRangeChanged不能替换成notifyItemChanged(index), 虽然index位置绕过了crash , 但是其他child不能够匹配,会重新绑定holder,体验不好。还有notifydatasetChange都是性能不好。
第二种解决办法:从AttachScrap中获取。
boolean validateViewHolderForOffsetPosition(RecyclerView.ViewHolder holder) {
if (mAdapter.hasStableIds()) {
return holder.getItemId() == mAdapter.getItemId(holder.mPosition);
}
return true;
}
从AttachScrap获取的holder, 导致旧的holder的itemid 和 remove之后position对应的itemId不一致。因此 adapter中去除getItemId(int position)方法
setHasStableIds(false);
第三种解决办法:去除动画。
adapter.mStringList.removeAt(findFirstVisibleItemPosition)
recyclerView.itemAnimator = null
adapter.notifyItemRemoved(findFirstVisibleItemPosition)
是因为动画导致了第一次的onlayoutchildren,布局前后的remove导致position不一致。去除动画避免第一次layout。
总结
recyclerview 的measure和layout围绕着add,remove,move,update操作,如果存在预定义的动画,onLayoutchildren会执行2次,因此measure和layout分别也会执行2次,第一次预布局,第二次是真正的布局。每次先去除屏幕上的view,再通过缓存策略寻找加入view,然后 measure和layout。
dispatchLayoutStep1: 布局之前记录动画信息,预layout。
dispatchLayoutStep2: 真正layout,缓存add和remove。
dispatchLayoutStep3: 布局之后记录动画信息,执行动画。缓存清理。
网友评论