细说 AppCompat 主题引发的坑:You need to use a Theme.AppCompat theme with this activity!

细说 AppCompat 主题引发的坑:You need to use a Theme.AppCompat theme with this activity!_第1张图片

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

成功启动后的 Activity 画面:

细说 AppCompat 主题引发的坑:You need to use a Theme.AppCompat theme with this activity!_第2张图片

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

细说 AppCompat 主题引发的坑:You need to use a Theme.AppCompat theme with this activity!_第3张图片

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

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

细说 AppCompat 主题引发的坑:You need to use a Theme.AppCompat theme with this activity!_第4张图片

突然想起 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"/>
    
    <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"/>