如何强制自定义View始终用指定样式,禁止XML中修改样式?
我有一个自定义的PrimaryButton,基于MaterialButton实现,代码如下:
class PrimaryButton @JvmOverloads constructor( context: Context, attributeSet: AttributeSet? = null, defStyleAttr: Int = R.attr.primary_button_style ) : MaterialButton(context, attributeSet, R.attr.primary_button_style)
现在需要让XML布局中设置的style属性完全失效——比如下面这段XML里的style="@style/Small.Secondary.Button"不能对按钮样式产生任何影响,必须确保这个按钮始终强制使用primary_button_style。
<com.stuff.buttons.PrimaryButton android:id="@+id/first_button" android:layout_width="0dp" android:layout_height="wrap_content" android:text="Primary Button" android:layout_margin="16dp" style="@style/Small.Secondary.Button"
解决方案:忽略XML中的style属性,强制应用固定样式
核心思路是在自定义View的构造过程中,过滤掉XML传入的style属性,或者通过主题包装的方式强制覆盖样式,同时保留其他XML属性的正常生效。
方法一:过滤AttributeSet中的style属性
通过包装原始AttributeSet,移除其中的style属性后再传给父类构造函数,这样XML里设置的style就不会被解析。
修改后的PrimaryButton代码:
class PrimaryButton @JvmOverloads constructor( context: Context, attributeSet: AttributeSet? = null, defStyleAttr: Int = R.attr.primary_button_style ) : MaterialButton( context, attributeSet?.let { cleanStyleAttribute(it) }, R.attr.primary_button_style ) { private fun cleanStyleAttribute(attrs: AttributeSet): AttributeSet { return object : AttributeSet by attrs { override fun getAttributeCount(): Int { var count = attrs.attributeCount // 检查并扣除style属性的计数 for (i in 0 until attrs.attributeCount) { if (attrs.getAttributeName(i) == "style") { count-- break } } return count } override fun getAttributeName(index: Int): String { var adjustedIndex = index // 跳过style属性的索引 for (i in 0 until attrs.attributeCount) { if (attrs.getAttributeName(i) == "style") { adjustedIndex += if (adjustedIndex >= i) 1 else 0 break } } return attrs.getAttributeName(adjustedIndex) } override fun getAttributeValue(index: Int): String? { var adjustedIndex = index for (i in 0 until attrs.attributeCount) { if (attrs.getAttributeName(i) == "style") { adjustedIndex += if (adjustedIndex >= i) 1 else 0 break } } return attrs.getAttributeValue(adjustedIndex) } } } }
方法二:用ContextThemeWrapper强制应用固定样式
这是更简洁的方式:创建一个主题包装器,把我们的primary_button_style强制应用到Context中,再传给父类,这样不管XML里设置什么style,都会被主题中的样式覆盖。
代码示例:
class PrimaryButton @JvmOverloads constructor( context: Context, attributeSet: AttributeSet? = null, defStyleAttr: Int = R.attr.primary_button_style ) : MaterialButton( // 包装Context,强制应用primary_button_style ContextThemeWrapper(context, R.style.AppTheme).apply { theme.applyStyle(R.attr.primary_button_style, true) }, attributeSet, R.attr.primary_button_style )
注意:把
R.style.AppTheme替换成你项目实际使用的主题即可。
方法三:通过XML序列化移除style属性
如果上面的方式不够稳妥,可以把AttributeSet序列化成XML字符串,移除其中的style标签后再重新解析成AttributeSet,确保彻底过滤掉style属性。
代码示例:
class PrimaryButton @JvmOverloads constructor( context: Context, attributeSet: AttributeSet? = null, defStyleAttr: Int = R.attr.primary_button_style ) : MaterialButton( context, attributeSet?.run { // 序列化AttributeSet到XML,移除style属性 val xmlWriter = StringWriter() val xmlSerializer = Xml.newSerializer().apply { setOutput(xmlWriter) startDocument("UTF-8", null) startTag("", "button") for (i in 0 until attributeCount) { if (getAttributeName(i) != "style") { attribute("", getAttributeName(i), getAttributeValue(i)) } } endTag("", "button") endDocument() } // 重新解析成AttributeSet XmlPullParserFactory.newInstance().newPullParser().apply { setInput(StringReader(xmlWriter.toString()), "UTF-8") nextTag() } }, R.attr.primary_button_style )
效果验证
修改完成后,XML中设置的任何style属性都不会影响PrimaryButton的样式,按钮会始终使用primary_button_style定义的样式,同时android:text、android:layout_margin等其他XML属性依然正常生效。
内容的提问来源于stack exchange,提问作者Ricardo Rodrigues

