Android自定义布局:如何读取子视图的自定义布局属性?
关于Android自定义布局属性的两个疑问,给你讲透
嘿,刚好之前做自定义ViewGroup时踩过同样的坑,来给你拆解清楚这两个问题,以及正确的实现逻辑:
一、为啥自定义属性要加app:前缀?
Android的XML布局里,命名空间是用来区分属性来源的:
android:开头的属性是系统内置的(比如android:layout_width),对应系统的命名空间http://schemas.android.com/apk/res/android;- 我们自己定义的属性不属于系统标准,所以得用自定义命名空间。你写的
xmlns:app="http://schemas.android.com/apk/res-auto"是Android官方推荐的便捷写法,res-auto会自动匹配当前应用的包名,不用硬编码冗长的包路径。当然你也可以把前缀改成my:或者其他名字,只是app:是行业惯例,大家看着更顺手。
二、最初的代码为啥读不到属性值?
你直接用child.context.theme.obtainStyledAttributes(R.styleable.MyLayout)去读,是从当前应用的主题里拿MyLayout相关的属性,而不是从子View的布局标签里解析对应的属性。子View的布局属性(包括你自定义的good)是和它的LayoutParams绑定的——ViewGroup会负责为每个子View创建对应的LayoutParams,并在这个过程中解析布局属性。所以直接从主题读,自然拿不到子View上设置的app:good值。
三、正确的实现:自定义LayoutParams
要读取子View上的自定义布局属性,必须通过自定义LayoutParams来完成,你修改后的代码是完全正确的,这里再把关键步骤拆解下,方便理解:
1. 自定义属性定义(你已经写对了)
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="MyLayout"> <attr name="good" format="boolean" /> </declare-styleable> </resources>
2. XML布局中使用自定义属性
记得声明自定义命名空间,然后给子View加上app:good属性就行:
<?xml version="1.0" encoding="utf-8"?> <com.loser.mylayouttest.MyLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" xmlns:app="http://schemas.android.com/apk/res-auto"> <Button app:good="true" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </com.loser.mylayouttest.MyLayout>
3. 自定义ViewGroup和LayoutParams
核心是重写ViewGroup的三个方法,让它使用我们自定义的LayoutParams,同时在LayoutParams的构造方法里解析属性:
class MyLayout: LinearLayout { // 别忘了添加这几个构造方法,适配不同的创建场景 constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { setMeasuredDimension(200, 300); // 这里是示例固定尺寸,你可以按需修改 for(index in 0 until childCount) { val child = getChildAt(index); // 强转为自定义LayoutParams,直接拿解析好的good属性 val p = child.layoutParams as MyLayoutParams; Log.d("so", "value = ${p.good}") } } // 子View没有指定布局属性时,返回默认的自定义LayoutParams override fun generateDefaultLayoutParams(): LayoutParams { return MyLayoutParams(context, null); } // 从XML的AttributeSet中解析属性,创建自定义LayoutParams override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams { return MyLayoutParams(context, attrs); } // 校验LayoutParams类型,确保是我们自定义的,避免类型错误 override fun checkLayoutParams(p: ViewGroup.LayoutParams?): Boolean { return p is MyLayoutParams } // 自定义LayoutParams,专门用来解析子View的good属性 inner class MyLayoutParams: LinearLayout.LayoutParams { var good: Boolean = false; constructor(c: Context?, attrs: AttributeSet?) : super(c, attrs) { c?.let { ctx -> attrs?.let { attrSet -> val a = ctx.obtainStyledAttributes(attrSet, R.styleable.MyLayout); good = a.getBoolean(R.styleable.MyLayout_good, false) a.recycle() // 一定要回收TypedArray,不然会内存泄漏 } } } } }
关键方法小说明:
generateDefaultLayoutParams():处理子View没有在XML中写布局属性的场景,返回我们自定义的默认LayoutParams;generateLayoutParams(attrs: AttributeSet?):这是核心!当ViewGroup从XML加载子View时,会调用这个方法,传入子View的AttributeSet,我们就在这里解析app:good属性;checkLayoutParams():确保ViewGroup处理的LayoutParams是我们自定义的类型,避免后续强转出错;- 自定义
MyLayoutParams的构造方法:把解析到的good属性值存在成员变量里,后续在ViewGroup的onMeasure或者其他方法里就能直接获取了。
这样调整后,就能正确读取每个子View上设置的app:good值啦~
内容的提问来源于stack exchange,提问作者Damn Vegetables
相关产品推荐
相关产品推荐

