一般来说按照文档的建议去做,出现问题的概率很低。但很多人的情况不同,每每会发生意外状况,就比如这次没有使用 AppCompat 主题引发的坑!
AppCompat
框架作为 Jetpack
集合的基石,非常重要。Android Studio 上创建的默认项目都会自动集成 AppCompat 框架,并采用其提供的 AppCompatActivity
作为 Activity Base。
App 侧给 Activity 配置的主题一般扩展自 SDK 提供的系统主题或 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个方案具体怎么操作。
在扩展自 SDK 的主题里额外配置下 windowActionBar 属性即可,true 或者 false 依需而定。
<style name="Theme.MaterialExtension" parent="android:Theme.Material.Light">
...
style>
<style name="Theme.MaterialExtension.Customize">
- "windowActionBar"
>true
style>
成功启动后的 Activity 画面:
等等,复选框设置条目的 CheckBox
怎么不见了?
查看了 CheckBoxPreference
的源码,没有发现什么特别的处理。
通过 Layout Inspector
看了下布局,发现了一点线索:视图当中,CheckBox 的实例是存在的,只是 Width 变成了0。而且 CheckBox 的实现类名变成了 AppCompatCheckBox
。
突然想起 AppCompat 框架为了让低版本系统能使用上诸如 Auto Size
和 Background Tint
的新功能,会给 SDK 的大部分控件重新扩展一个 AppCompat 前缀的同名控件。所以猜测,AppCompatCheckBox 依赖的兼容性属性,我们的主题里没有配置。
来看下 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"/>
<attr format="reference" name="buttonCompat"/>
...
>
通过搜索发现 AppCompat 主题给 CheckBox 控件配置的 Style 里使用了 buttonCompat
的 Attr。
<style name="Base.Widget.AppCompat.CompoundButton.CheckBox" parent="android:Widget.CompoundButton.CheckBox">
- "android:button"
>?android:attr/listChoiceIndicatorMultiple
- "buttonCompat"
>?attr/listChoiceIndicatorMultipleAnimated
- "android:background">?attr/controlBackground
style>
<style name="Widget.AppCompat.CompoundButton.CheckBox" parent="Base.Widget.AppCompat.CompoundButton.CheckBox"/>