Android style定义中,我们可以给一个新的style样式指定一个父style。众所周知,可以通过两种方式实现:
1,新的style name中通过.将父style name与新的style name连接,如常见的:
<style name="Theme.AppCompat.Light.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="Theme.AppCompat.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
2,新的style name中通过parent指定特定的父style name,如Android源码中的:
<style name="aerr_list_item" parent="Widget.Material.Light.Button.Borderless">
<item name="minHeight">?attr/listPreferredItemHeightSmall</item>
<item name="textAppearance">?attr/textAppearanceListItemSmall</item>
<item name="textColor">?attr/textColorAlertDialogListItem</item>
<item name="gravity">center_vertical</item>
<item name="paddingStart">?attr/dialogPreferredPadding</item>
<item name="paddingEnd">?attr/dialogPreferredPadding</item>
<item name="background">?attr/selectableItemBackground</item>
<item name="drawablePadding">32dp</item>
<item name="drawableTint">?android:attr/colorAccent</item>
<item name="drawableTintMode">src_atop</item>
</style>
以上两种情况,子style最终都会从父styel中继承属性,同时,如果子style中存在同名属性,则覆盖之。
但是,现实中我们往往看到另外一种写法,咋一看上去是杂糅了上述两种方式,如appcompat中常见的:
<style name="RtlOverlay.Widget.AppCompat.Search.DropDown" parent="android:Widget">
<item name="android:paddingLeft">@dimen/abc_dropdownitem_text_padding_left</item>
<item name="android:paddingRight">4dp</item>
</style>
<style name="RtlOverlay.Widget.AppCompat.Search.DropDown.Icon1" parent="android:Widget">
<item name="android:layout_alignParentLeft">true</item>
</style>
<style name="RtlOverlay.Widget.AppCompat.Search.DropDown.Icon2" parent="android:Widget">
<item name="android:layout_toLeftOf">@id/edit_query</item>
</style>
<style name="RtlOverlay.Widget.AppCompat.Search.DropDown.Query" parent="android:Widget">
<item name="android:layout_alignParentRight">true</item>
</style>
我们发现,这种写法还是很普遍的,包括我们项目自己在定义style时,也有不少是此类写法。
那么,问题来了,这种写法下,style最终是怎么样的一个规则呢?
以.写法作为父style,还是以parent作为父style,抑或两者都作为父style,然后按照其他规则处理属性优先级?
最近正好遇到一个相关的Bug。主工程接上UI组件库后,测试效果,写了个简单的TextView,其中textColor使用?attr/brand_color_primary形式指向自定义属性。自然的,需要将当前项目Activity theme(style名是以.连接),指定UI组件库中的一个基础style作为parent,具体通过parent指定,为描述方便,简单将其简化为:
<style name="A.B.C" parent="D">
....
</style>
但运行时出现闪退。错误信息如下:
Process: com.corn.debug, PID: 16205
android.view.InflateException: Binary XML file line #144 in com.corn.debug:layout/fragment_home_me: Failed to resolve attribute at index 5: TypedValue{t=0x2/d=0x7f040271 a=-1}
Caused by: java.lang.UnsupportedOperationException: Failed to resolve attribute at index 5: TypedValue{t=0x2/d=0x7f040271 a=-1}
at android.content.res.TypedArray.getDimensionPixelSize(TypedArray.java:783)
at android.view.ViewGroup$MarginLayoutParams.<init>(ViewGroup.java:8226)
at androidx.constraintlayout.widget.ConstraintLayout$LayoutParams.<init>(ConstraintLayout.java:2691)
at androidx.constraintlayout.widget.ConstraintLayout.generateLayoutParams(ConstraintLayout.java:1916)
at androidx.constraintlayout.widget.ConstraintLayout.generateLayoutParams(ConstraintLayout.java:482)
at android.view.LayoutInflater.rInflate(LayoutInflater.java:1129)
at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:1088)
at android.view.LayoutInflater.rInflate(LayoutInflater.java:1130)
at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:1088)
at android.view.LayoutInflater.rInflate(LayoutInflater.java:1130)
at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:1088)
at android.view.LayoutInflater.rInflate(LayoutInflater.java:1130)
at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:1088)
at android.view.LayoutInflater.inflate(LayoutInflater.java:686)
at android.view.LayoutInflater.inflate(LayoutInflater.java:538)
at com.corn.base.BaseFragment.onCreateView(BaseFragment.java:29)
at androidx.fragment.app.Fragment.performCreateView(Fragment.java:2698)
at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:310)
at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1185)
at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1354)
at androidx.fragment.app.FragmentManager.moveFragmentToExpectedState(FragmentManager.java:1432)
at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1495)
at androidx.fragment.app.BackStackRecord.executeOps(BackStackRecord.java:447)
at androidx.fragment.app.FragmentManager.executeOps(FragmentManager.java:2167)
at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:1990)
at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:1945)
at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1847)
at androidx.fragment.app.FragmentManager$4.run(FragmentManager.java:413)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:224)
at android.app.ActivityThread.main(ActivityThread.java:7520)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:539)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:950)
一开始一直以为是自定义属性写法有问题,最终运行时没有找到其对应的属性值。但最终发现,错误信息指示位置尚未来到新增的测试布局位置,在之前的其他页面就已经发生了闪退。
从报错信息上看,应该是自定义属性没有取到值,通过查看闪退布局,发现有如下写法:
<ImageView
android:id="@id/iv_arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="?attr/insetRight"
android:layout_marginRight="?attr/insetRight"
android:src="@drawable/icon_arrow_right"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
很显然,应该是?attr/insetRight处出了问题。
Activity的主题是在Manifest中进行的配置,追查发现,insetRight是一个自定义属性。
<attr name="insetRight" format="dimension" />
其赋值具体是在<style name="A.B.C" parent="D">....</style>中的B中。也就是说,新指定parent="D"后,最终style没有继承到来自B的特有属性。
重新为insetLeft赋值后,一切运行正常。
<style name="A.B.C" parent="D">
<item name="insetRight">14dp</item>
....
</style>
从结果上看,我们得出的结论如下:
style为单继承,parent方式优先级高于.继承,同时存在时,前缀的作用只作为名字。
也可以自己写自定义几个style再次测试下:
<style name="MyStyle1">
<item name="android:textColor">#00f</item>
</style>
<style name="MyStyle2">
<item name="android:textSize">18sp</item>
</style>
<style name="MyStyle2.SubStyle" parent="MyStyle1">
<item name="android:textStyle">bold</item>
</style>
从提示上看,
MyStyle2是没有生效的。同时,运行后,也确实只有SubStyle与MyStyle1的效果。此时,我们直接将MyStyle2.SubStyle理解成新的style name即可。
Google官方文档上,其实也有类似描述,平时没太注意,本以为只是同时存在.命名与parent时,是两者属性的都会继承,然后再按照parent的为准,最后才是以自己的属性为最终标准的。现在看确实理解错了。
附上官网文档。
Styles and Themes
其中关键的描述为:
踩坑留念。
end~