自定义仿RecyclerView的ViewGroup中RelativeLayout内TextView不显示但LinearLayout正常
我正在实现名为MyRecyclerView的自定义ViewGroup,功能模拟原生RecyclerView,暂未实现LayoutManager与ViewHolder逻辑,全部布局流程由MyRecyclerView独立完成。
MyAdapter是为MyRecyclerView提供数据的虚拟接口,代码如下:
val myAdapter = object: MyRecyclerAdapter{ override fun onCreateViewHolder(row: Int, convertView: View?, parent: ViewGroup): View { val id = when(getItemViewType(row)) { 0 -> R.layout.item_custom_view0 1 -> R.layout.item_custom_view1 else -> -1 } val resultView = convertView ?: layoutInflater.inflate(id, parent, false) if(getItemViewType(row) == 1) resultView.findViewById<TextView>(R.id.tv_item1).text = testList[row] return resultView } override fun onBindViewHolder(row: Int, convertView: View?, parent: ViewGroup):View { val id = when(getItemViewType(row)) { 0 -> R.layout.item_custom_view0 1 -> R.layout.item_custom_view1 else -> -1 } val resultView = convertView ?: layoutInflater.inflate(id, parent, false) if(getItemViewType(row) == 1) resultView.findViewById<TextView>(R.id.tv_item1).text = testList[row] return resultView } override fun getItemViewType(row: Int): Int = row%2 override fun getItemViewTypeCount(): Int { return 2 } override fun getItemCount(): Int = itemCount override fun getItemHeight(row: Int): Int = itemHeight // fixed height }
我预期的MyRecyclerView运行逻辑如下图所示:
我没有重写MyRecyclerView的onMeasure方法,因为数据首次加载时无法确定屏幕可容纳的item数量,这部分逻辑我放在onLayout中处理。所有item均设置了固定高度,当item总高度超过MyRecyclerView高度时,不再排布更多item。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) } override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { if(mNeedRelayout || changed) { mNeedRelayout = false mItemViewList.clear() // mItemViewList cached all views on Screen removeAllViews() if (mAdapter != null) { // heights of each item is fixed (as 80dp) and accessed by MyAdapter for(i in 0 until mItemCount) mHeights[i] += mAdapter!!.getItemHeight(i) // since onMeasure is not implemented // mWidth: the width of MyRecyclerView // mHeight: the height of MyRecyclerView mWidth = r - l mHeight = b - t var itemViewTop = 0 var itemViewBottom = 0 // foreach whole data list, and determine how many items should be // put on screen when data is first loaded for(i in 0 until mItemCount){ itemViewBottom = itemViewTop + mHeights[i] // itemView is layout in makeAndSetUpView val itemView = makeAndSetupView(i, l,itemViewTop,r,itemViewBottom) // there is no gap between item views itemViewTop = itemViewBottom mItemViewList.add(itemView) addView(itemView, 0) // if top of current item view is below screen, it should not be // displayed on screen if(itemViewTop > mHeight) break } } } }
我认为makeAndSetUpView返回的itemView是ViewGroup,调用其layout方法后,所有子View都会完成测量和布局流程。
private fun makeAndSetupView( index: Int, left: Int, top: Int, right: Int, bottom: Int ): View{ // a simple reuse scheme val itemView = obtain(index, right-left, bottom-top) // layout children itemView.layout(left, top, right, bottom) return itemView }
当上下滚动MyRecyclerView时,移出屏幕的View会被复用,新的数据文本会设置到对应TextView中填充空白区域,该逻辑通过relayoutViews方法实现:
private fun relayoutViews(){ val left = 0 var top = -mScrollY val right = mWidth var bottom = 0 mItemViewList.forEachIndexed { index, view -> bottom = top + mHeights[index] view.layout(left, top, right, bottom) top = bottom } // Relayout is finished mItemViewList.forEachIndexed { index, view -> val lastView = (view as ViewGroup).getChildAt(0) Log.d("TEST", " i: $index top: ${lastView.top} bottom: ${lastView.bottom} left: ${lastView.left} right: ${lastView.right}") Log.d("TEST", " i: $index width: ${lastView.width} height: ${lastView.height}") } }
此处的调试输出正常,缓存的mItemViewList中的View所有属性都符合预期,TextView也已经被正确设置了文本内容
但滚动复用View时页面上完全看不到TextView的内容
出问题的item布局根容器为RelativeLayout,代码如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="@dimen/item_height"> <TextView android:text=" i am a fixed Text" android:id="@+id/tv_item0" android:layout_centerInParent="true" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </RelativeLayout>
但我将根布局改为LinearLayout后,所有功能都正常,View可以正常复用,文本也能正确显示:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="@dimen/item_height" android:orientation="vertical" android:gravity="center" android:layout_marginTop="2dp"> <TextView android:text="i am a fixed text" android:id="@+id/tv_item0" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </LinearLayout>

核心问题是你对Android View绘制流程的认知有误:调用layout()方法不会自动触发子View的测量流程,只有measure()方法才会触发测量,layout()仅基于已有的测量结果摆放View位置。
首次加载正常是因为inflate View后第一次添加到父容器时,父容器的默认测量流程会触发子View测量,所以RelativeLayout的子View能拿到正确的测量参数正常显示。
复用View时你没有重新触发itemView的测量,直接调用了layout()方法:
- LinearLayout的布局逻辑简单,仅依赖自身的gravity和子View的测量缓存值,刚好可以正常摆放子View,所以看起来正常。
- RelativeLayout的子View布局依赖父容器的测量上下文,没有重新测量的情况下,子View的布局参数计算异常,就会出现不显示的问题。
另外你完全没有重写onMeasure逻辑,也没有主动测量子View,导致子View的测量参数始终是首次加载时的缓存值,复用场景下必然出现适配问题。
- 给所有调用
layout()的地方前置调用measure(),传入正确的测量规格:
在makeAndSetupView和relayoutViews调用view.layout()之前,添加测量逻辑:
// 宽度是MyRecyclerView的宽度,精确模式 val widthSpec = MeasureSpec.makeMeasureSpec(mWidth, MeasureSpec.EXACTLY) // 高度是当前item的固定高度,精确模式 val heightSpec = MeasureSpec.makeMeasureSpec(mHeights[index], MeasureSpec.EXACTLY) itemView.measure(widthSpec, heightSpec) // 再调用layout itemView.layout(left, top, right, bottom)
- 完善
onMeasure逻辑,不要直接完全调用super,至少要确认自身的测量尺寸,避免wrap_content场景下尺寸异常。 - 绑定新数据后如果修改了子View的内容(比如修改了TextView的文本),也需要主动触发测量,避免子View尺寸变化导致的布局异常。
内容的提问来源于stack exchange,提问作者kolibreath

