细说 AppCompat 主题引发的坑:you need to use a AppCompat theme!
一般来说按照文档的建议去做,出现问题的概率很低。但很多人的情况不同,每每会发生意外状况,就比如这次没有使用 AppCompat 主题引发的坑!
AppCompat
框架作为 Jetpack
集合的基石,非常重要。Android Studio 上创建的默认项目都会自动集成 AppCompat 框架,并采用其提供的 AppCompatActivity
作为 Activity Base。
App 侧给 Activity 配置的主题一般扩展自 SDK 提供的系统主题或 AppCompat 提供的主题,前者的话极有可能引发一些 AppCompat 框架的使用异常。
非 AppCompat 主题引发了异常
如果配置的是扩展自 SDK 的主题,Activity 必然无法启动,并发生如下异常:
RuntimeException: Unable to start activity xxx: java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.
原因很简单,AppCompat 框架的诸多后续处理紧密关联该主题配置的属性。因此在加载画面前将严格检查是否采用了 AppCompat 系主题,否则将抛出异常。
class AppCompatDelegateImpl ... {
private ViewGroup createSubDecor() {
TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
if (!a.hasValue(R.styleable.AppCompatTheme_windowActionBar)) {
a.recycle();
throw new IllegalStateException(
"You need to use a Theme.AppCompat theme (or descendant) with this activity.");
}
...
return subDecor;
}
}
如何解决这个问题呢?
主题改为扩展自 AppCompat 系主题。 但如果自己的主题覆写的地方很多,这将耗费很长时间,而且很多自定义的属性可能还会和 AppCompat 主题产生冲突,需要逐个分析、细细调整
Activity 改用 SDK 版本,即
android.app.Activity
。 但随着 Jetpack 框架的日渐成熟和流行,很多重要的框架非常依赖于 AppCompatActivity 的支持,比如Lifecycle
框架、ViewModel
框架、Preference
等。这可能导致其他的框架功能发生问题,也不是很好AppCompat 哪里有兼容性问题解决哪里的回避方案。 比如上面的异常其实就是检查是否配置了 AppCompat 框架提供的
windowActionBar
属性而已,那么我们在自己的主题里加上该属性的引用就可以了。不好的地方就在于,很多不是异常的 UI 展示问题,如果没有发现的话,很容易被忽略。也就是说,这个方案容易改得不全,产生遗漏
前2个方案没啥好说,我们具体来分析下第3个方案具体怎么操作。
如何使用非 AppCompat 主题
在扩展自 SDK 的主题里额外配置下 windowActionBar 属性即可,true 或者 false 依需而定。
<style name="Theme.MaterialExtension" parent="android:Theme.Material.Light">
...
</style>
<style name="Theme.MaterialExtension.Customize">
<item name="windowActionBar">true</item>
</style>
成功启动后的 Activity 画面:
等等,复选框设置条目的 CheckBox
怎么不见了?
查看了 CheckBoxPreference
的源码,没有发现什么特别的处理。
通过 Layout Inspector
看了下布局,发现了一点线索:视图当中,CheckBox 的实例是存在的,只是 Width 变成了0。而且 CheckBox 的实现类名变成了 AppCompatCheckBox
。
突然想起 AppCompat 框架为了让低版本系统能使用上诸如 Auto Size
和 Background Tint
的新功能,会给 SDK 的大部分控件重新扩展一个 AppCompat 前缀的同名控件。所以猜测,AppCompatCheckBox 依赖的兼容性属性,我们的主题里没有配置。
如何兼容 AppCompat 控件
来看下 AppCompatCheckBox 控件的源码,我们发现构造函数里针对复选按钮有特别的实现。
public AppCompatCheckBox( ... ) {
...
mCompoundButtonHelper = new AppCompatCompoundButtonHelper(this);
mCompoundButtonHelper.loadFromAttributes(attrs, defStyleAttr);
}
具体就是通过 AppCompatCompoundButtonHelper 去加载 buttonCompat 属性配置的复选按钮图片。
void loadFromAttributes(@Nullable AttributeSet attrs, int defStyleAttr) {
TintTypedArray a =
TintTypedArray.obtainStyledAttributes(mView.getContext(), attrs,
R.styleable.CompoundButton, defStyleAttr, 0);
ViewCompat.saveAttributeDataForStyleable(mView, mView.getContext(),
R.styleable.CompoundButton, attrs, a.getWrappedTypeArray(), defStyleAttr, 0);
try {
boolean buttonDrawableLoaded = false;
if (a.hasValue(R.styleable.CompoundButton_buttonCompat)) {
final int resourceId = a.getResourceId(R.styleable.CompoundButton_buttonCompat, 0);
if (resourceId != 0) {
try {
mView.setButtonDrawable(
AppCompatResources.getDrawable(mView.getContext(), resourceId));
buttonDrawableLoaded = true;
} ...
}
}
...
} finally {
a.recycle();
}
}
很明显,我们的主题里没有配置这个属性,所以 CheckBox 显示不出来。
当然可以直接在我们的主题里配置这个属性,但如果能和 AppCompat 框架设置一样的,省去了提供复选框资源,岂不更好。
<declare-styleable name="CompoundButton">
<attr name="android:button"/>
<!-- Compat attr to load backported drawable types -->
<attr format="reference" name="buttonCompat"/>
...
</>
通过搜索发现 AppCompat 主题给 CheckBox 控件配置的 Style 里使用了 buttonCompat
的 Attr。
<style name="Base.Widget.AppCompat.CompoundButton.CheckBox" parent="android:Widget.CompoundButton.CheckBox">
<item name="android:button">?android:attr/listChoiceIndicatorMultiple</item>
<item name="buttonCompat">?attr/listChoiceIndicatorMultipleAnimated</item>
<item name="android:background">?attr/controlBackground</item>
</style>
<style name="Widget.AppCompat.CompoundButton.CheckBox" parent="Base.Widget.AppCompat.CompoundButton.CheckBox"/>
<style name="Base.V7.Theme.AppCompat.Light" parent="Platform.AppCompat.Light">
<item name="checkboxStyle">@style/Widget.AppCompat.CompoundButton.CheckBox</item>
</style>
这样的话,在我们的主题里同样应用这个 Style 就行了。
<style name="Theme.MaterialExtension.Customize">
...
<item name="checkboxStyle">@style/Widget.AppCompat.CompoundButton.CheckBox</item>
</style>
更快速的兼容方法
如果发现哪个控件有问题,都像上面的办法一样去查的话着实花时间。其实直接到 AppCompat 主题的实现里搜索控件相关的兼容性 Style,拷贝过来即可。
比如一步步找到 AppCompat 主题的具体实现,在里面搜索得到 CheckBox 关键字的 Style。
<style name="Theme.AppCompat.DayNight.DarkActionBar" parent="Theme.AppCompat.Light.DarkActionBar"/>
<style name="Theme.AppCompat.Light.DarkActionBar" parent="Base.Theme.AppCompat.Light.DarkActionBar"/>
<style name="Base.Theme.AppCompat.Light.DarkActionBar" parent="Base.Theme.AppCompat.Light">
<style name="Base.Theme.AppCompat.Light" parent="Base.V7.Theme.AppCompat.Light">
<style name="Base.V7.Theme.AppCompat.Light" parent="Platform.AppCompat.Light">
...
<item name="editTextStyle">@style/Widget.AppCompat.EditText</item>
<item name="editTextBackground">@drawable/abc_edit_text_material</item>
<item name="editTextColor">?android:attr/textColorPrimary</item>
...
<!-- CheckBox 的兼容性 Style 藏在这里 -->
<item name="checkboxStyle">@style/Widget.AppCompat.CompoundButton.CheckBox</item>
</style>
注意
除了 AppCompatActivity,AppCompat 框架里提供的 AppCompatDialog
同样也有主题的限制,需要留意一下。AppCompatDialog 内视图发生兼容问题的话, 和 Activity 的 AppCompat 控件一样处理。
结语
针对非 AppCompat
主题的使用问题有3种解决方案:
主题改为扩展自 AppCompat 系主题
Activity 改用 SDK 版本的
android.app.Activity
手动解决AppCompat 的兼容性问题
毋庸置疑的是 AppCompat 框架对主题的限制源于后续的 UI 逻辑与其紧密相连,所以最佳解决办法肯定是方案1,即提供 AppCompat 系主题。
如果确实不需要 Jetpack
其他框架的协同(虽然这极不可能),那么可以选择方案2如果现有主题太过庞大,一时半会儿无法切换到 AppCompat 主题,而且影响的画面很少、不复杂。那么可以试试方案3去逐个解决