推荐阅读
(一)RecycleView 初探回收复用,onCreateView和onBindView调用关系
(二)Android RecycleView实现吸附小标题的Demo(附源码)
(三)RecycleView 自定义下拉刷新,上拉加载监听
(四)RecycleView 滑动到置顶、Adapter局部刷新
(五)RecycleView 动态设置改变列表显示的高度
RecycleView 是一个可回收复用的列表控件,也是使用较普遍的。在使用时也会结合业务功能需求做出一些改变。比如两个Recycleview之间有交互,又或者嵌套滑动处理,又或者高度动态设置。本篇正是关于如何动态改变列表的高度。
先看效果图:
RecyclerView.onMeasure() 方法源码,测量顺序如下:
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
// 1、是否进入 自动测量自身尺寸
if (mLayout.mAutoMeasure) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
&& heightMode == MeasureSpec.EXACTLY;
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
if (skipMeasure || mAdapter == null) {
return;
}
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
dispatchLayoutStep2();
// 关键:通过测量孩子view宽高来确定自身尺寸
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
if (mLayout.shouldMeasureTwice()) {
mLayout.setMeasureSpecs(
MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
mState.mIsMeasuring = true;
dispatchLayoutStep2();.
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
}
} else {
//2、如果是固定大小,执行会和上面效果一样
if (mHasFixedSize) {
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
return;
}
// 定制测量
if (mAdapterUpdateDuringMeasure) {
eatRequestLayout();
processAdapterUpdatesAndSetAnimationFlags();
if (mState.mRunPredictiveAnimations) {
mState.mInPreLayout = true;
} else {
// 使用剩余的更新来提供与布局传递一致的状态。
mAdapterHelper.consumeUpdatesInOnePass();
mState.mInPreLayout = false;
}
mAdapterUpdateDuringMeasure = false;
resumeRequestLayout(false);
}
if (mAdapter != null) {
mState.mItemCount = mAdapter.getItemCount();
} else {
mState.mItemCount = 0;
}
eatRequestLayout();
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
resumeRequestLayout(false);
mState.mInPreLayout = false; // 清除
}
}
有两个判断比较显眼:mAutoMeasure、mHasFixedSize。这俩都会让RecycleView自动测量全部孩子的高度,从而能确定自身尺寸MeasuredDimension大小。
通过重写 LayoutManage的onMeasure()方法,获取到RecycleView的一个item的viewholder对象实例,如果这个item实例对象存在,就进行测量item的大小,拿到确切的高度Height值后,就可以动态设置Recycleview显示多少个item的高度了。
需要注意,item的布局最好提前设定固定的高度,否则获取为0。
记得要
设置mAutoMeasure、mHasFixedSize值为false,不设置可能会报错。
LinearLayoutManager mLayoutManager = new LinearLayoutManager(this){
@Override
public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec, int heightSpec) {
View view = recycler.getViewForPosition(0);
if (view != null) {
measureChild(view, widthSpec, heightSpec);
int measuredHeight = view.getMeasuredHeight();
//int measuredWidth = View.MeasureSpec.getSize(widthSpec);
int showHeight = measuredHeight * state.getItemCount();
if(state.getItemCount() >= 5){
showHeight = measuredHeight * 5;
}
setMeasuredDimension(widthSpec, showHeight);
}
}
};
mLayoutManager.setAutoMeasureEnabled(false);
mRecyclerview.setHasFixedSize(false);
mRecyclerview.setLayoutManager(mLayoutManager);
通过adapter传入不同的viewType拿到ViewHolder对象,对这个ViewHolder进行测量,然后得到测量后的高度值。最后,就可以根据item调整设置列表的布局参数的高度。
需要注意,在NestedScrollView嵌套RecycleView时,在RecycleView完全展示时(即按itemCount总数),RecycleView仍然会有上下可滑动的小空间,虽然只是一点点,也是会影响用户体验。因此,需要在完全展开时,将它设置禁止滑动。
boolean isOpen ; //记录展开、收起状态
private boolean setFitHeight(RecyclerView recyclerView){
RecyclerView.Adapter adapter = recyclerView.getAdapter();
int itemCount = adapter.getItemCount();
int measuredHeight = 0;
if (itemCount >0){
RecyclerView.ViewHolder holder = adapter.createViewHolder(recyclerView, adapter
.getItemViewType(0));//通过viewType类型返回ViewHolder
adapter.onBindViewHolder(holder, 0);
holder.itemView.measure(
View.MeasureSpec.makeMeasureSpec(recyclerView.getWidth(), View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
holder.itemView.layout(0, 0, holder.itemView.getMeasuredWidth(), holder.itemView.getMeasuredHeight());
holder.itemView.setDrawingCacheEnabled(true);
holder.itemView.buildDrawingCache();
measuredHeight = holder.itemView.getMeasuredHeight();
}
if (isOpen){
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
measuredHeight * 3);
recyclerView.setLayoutParams(layoutParams);
recyclerView.setNestedScrollingEnabled(true);//允许滑动
return isOpen = false;
}else{
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
measuredHeight * itemCount);
recyclerView.setLayoutParams(layoutParams);
recyclerView.setNestedScrollingEnabled(false);//禁止滑动
return isOpen = true;
}
}
上面两种实现方式,都离开View的测量,因此建议大家多深入学习自定义View流程mesure\layout\draw源码。
第一种方案代码简单,使用方便,但扩展性和灵活性不强。适用于该页面静态显示高度,不动态改变。
第二种方案更值得推荐。因为我们的RecycleView的item会有不同风格大小的时候,它可以通过viewType得到每一种item高度,从而设置固定高度。另外,RecycleView的布局参数LayoutParams的值改变即响应。
点个赞,加关注。