细说 AppCompat 主题引发的坑:use a AppCompat theme with this activity!

2,080 阅读5分钟

一般来说按照文档的建议去做,出现问题的概率很低。但很多人的情况不同,每每会发生意外状况,就比如这次没有使用 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;
    }
}

如何解决这个问题呢?

  1. 主题改为扩展自 AppCompat 系主题。 但如果自己的主题覆写的地方很多,这将耗费很长时间,而且很多自定义的属性可能还会和 AppCompat 主题产生冲突,需要逐个分析、细细调整

  2. Activity 改用 SDK 版本,即 android.app.Activity 但随着 Jetpack 框架的日渐成熟和流行,很多重要的框架非常依赖于 AppCompatActivity 的支持,比如 Lifecycle 框架、ViewModel 框架、Preference 等。这可能导致其他的框架功能发生问题,也不是很好

  3. 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 画面:

12-widget

等等,复选框设置条目的 CheckBox 怎么不见了?

12-widget

查看了 CheckBoxPreference 的源码,没有发现什么特别的处理。

通过 Layout Inspector 看了下布局,发现了一点线索:视图当中,CheckBox 的实例是存在的,只是 Width 变成了0。而且 CheckBox 的实现类名变成了 AppCompatCheckBox

突然想起 AppCompat 框架为了让低版本系统能使用上诸如 Auto SizeBackground 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种解决方案:

  1. 主题改为扩展自 AppCompat 系主题

  2. Activity 改用 SDK 版本的 android.app.Activity

  3. 手动解决AppCompat 的兼容性问题

毋庸置疑的是 AppCompat 框架对主题的限制源于后续的 UI 逻辑与其紧密相连,所以最佳解决办法肯定是方案1,即提供 AppCompat 系主题。

  • 如果确实不需要 Jetpack 其他框架的协同(虽然这极不可能),那么可以选择方案2
  • 如果现有主题太过庞大,一时半会儿无法切换到 AppCompat 主题,而且影响的画面很少、不复杂。那么可以试试方案3去逐个解决

推荐阅读

AppCompat发布两年了,还没了解?

Jetpack新成员SplashScreen:打造全新的App启动画面

一气呵成:用Compose完美复刻Flappy Bird!