You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义仿RecyclerView的ViewGroup中RelativeLayout内TextView不显示但LinearLayout正常

自定义MyRecyclerView复用RelativeLayout根布局的item时文本不显示问题

我正在实现名为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的测量参数始终是首次加载时的缓存值,复用场景下必然出现适配问题。
修复方案
  1. 给所有调用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)
  1. 完善onMeasure逻辑,不要直接完全调用super,至少要确认自身的测量尺寸,避免wrap_content场景下尺寸异常。
  2. 绑定新数据后如果修改了子View的内容(比如修改了TextView的文本),也需要主动触发测量,避免子View尺寸变化导致的布局异常。

内容的提问来源于stack exchange,提问作者kolibreath

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.04 14:36:03