RecyclerView重置自定义View LayoutParams问题:为何手动设置构造函数参数才有效?
你遇到的这个问题,本质是RecyclerView的LayoutManager会自动为itemView生成适配自身的LayoutParams,覆盖掉自定义View初始未明确设置的布局参数。先给你拆解原因,再提供更优雅的解决办法,避免硬编码重复代码。
为什么手动设置LayoutParams能解决问题?
当你直接new OnlineVideoView(context)作为itemView时,这个自定义View初始的LayoutParams是null(既没通过LayoutInflater从XML加载,也没手动设置)。RecyclerView的LinearLayoutManager在处理item时,会默认给它分配LinearLayout.LayoutParams(wrap_content, wrap_content),这就导致你期望的match_parent宽度、margin等布局效果全部失效。
而你在自定义View的init里手动设置layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT)后,RecyclerView会把这个通用LayoutParams转换成自身适配的LinearLayout.LayoutParams,并保留你设置的宽高属性,所以布局就能正常显示了。
另外要注意:你在view_online_video.xml的merge标签里设置的android:layout_margin="24dp"是无效的——merge标签的属性仅作用于内部子View,不会自动应用到父容器(也就是你的OnlineVideoView)上。
更优雅的解决方案(避免重复代码)
方案1:在ViewHolder创建时设置LayoutParams(解耦自定义View)
把LayoutParams的设置逻辑移到ArticleSectionsAdapter的onCreateViewHolder中,让自定义View不用关心自己被放在哪个容器里,复用性更强:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder { val videoView = OnlineVideoView(parent.context) // 直接设置RecyclerView专用的LayoutParams videoView.layoutParams = RecyclerView.LayoutParams( RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT ) // 如果需要margin,也可以在这里设置 (videoView.layoutParams as RecyclerView.LayoutParams).apply { marginStart = 24.dpToPx(parent.context) marginEnd = 24.dpToPx(parent.context) topMargin = 24.dpToPx(parent.context) bottomMargin = 24.dpToPx(parent.context) } return ViewHolder(videoView) } // 拓展函数:dp转px fun Int.dpToPx(context: Context): Int { return (this * context.resources.displayMetrics.density).toInt() }
方案2:用XML定义自定义View的布局参数(最推荐)
创建一个专门的item布局文件item_online_video.xml,在里面明确设置自定义View的布局参数:
<?xml version="1.0" encoding="utf-8"?> <com.example.layouttest.OnlineVideoView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="24dp"/>
然后在Adapter里通过LayoutInflater加载这个布局:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder { val view = LayoutInflater.from(parent.context) .inflate(R.layout.item_online_video, parent, false) as OnlineVideoView return ViewHolder(view) }
这种方式完全不需要在代码里硬编码LayoutParams,LayoutInflater会自动根据XML配置生成正确的参数,RecyclerView也会识别并保留这些设置,同时自定义View的代码可以简化(去掉手动设置LayoutParams的逻辑):
class OnlineVideoView : ConstraintLayout { constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) init { inflate(context, R.layout.view_online_video, this) setPadding(24) // 移除手动设置LayoutParams的代码 } }
方案3:在自定义View中动态适配父容器(耦合性稍高)
如果希望自定义View在被放入RecyclerView时自动适配,可以在init里判断父容器类型,动态设置参数:
init { inflate(context, R.layout.view_online_video, this) setPadding(24) // 仅当父容器是RecyclerView时设置参数 post { if (parent is RecyclerView) { layoutParams = RecyclerView.LayoutParams( RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT ) (layoutParams as RecyclerView.LayoutParams).margin = 24.dpToPx(context) } } }
不过这个方案会让自定义View和RecyclerView耦合,不推荐在需要复用的自定义View中使用。
内容的提问来源于stack exchange,提问作者MikkelT

