百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

深度解读Jetpack框架的基石-AppCompat

nanshan 2024-11-20 19:25 13 浏览 0 评论


为了能够让低版本的Android系统能够运行新特性,AppCompat框架自Support时代就已推出。但随着AndroidX的一统江湖,AppCompat的相关类则一并迁移到了AndroidX库里。

Android开发者应该都不陌生,在Android Studio上创建的项目默认采用AppCompatActivity作为Activity的基类。可以说,这个类是整个AppCompat框架里最重要的类,也是我们今天研究AppCompat的起点。

1. AppCompatActivity

其间接继承自Activity,之间还继承了其他Activity特色类,可以使得低版本上运行的Activity也能拥有ToolBar和暗黑主题等新功能。

AppCompatActivity extends FragmentActivity extends ComponentActivity extends ComponentActivity extends Activity*

作用

FragmentActivity

采用FragmentController类对AndroidX的Fragment新组件提供支撑,比如提供了咱们常用的getSupportFragmentManager() API。

androidx.activity.ComponentActivity

实现了ViewModel接口,和Lifecycle框架进行配合以支撑ViewModel框架的运行。

androidx.core.app.ComponentActivity

实现了Lifecycle接口并通过ReportFragment支撑Lifecycle框架的运行。

先来感受一下AppCompatActivity和Activity在UI上的表现。

从对比图上看并没有太大区别,但从UI的树形图上看是有些区别的。比如AppCompatActivity的content区域的上方多了一个LinearLayout和ViewStub控件,再比如AppCompatActivity下面的是AppCompatTextView而不是TextView。

那这些差异是如何实现的,有什么用意?

谈到AppCompatActivity实现的话不得不提幕后的大管家AppCompatDelegate类,其承载了AppCompatActivity几乎所有的实现工作。

比如AppCompatActivity复写了setContentView()的逻辑,交由大管家AppCompatDelegate去实现其特有的UI结构。

1.1 AppCompatDelegate

重点介绍下大管家的头号工作:setContentView(),具体分为如下几个小任务。

  • ensureSubDecor()确保ActionBar的特有UI结构创建完毕
  • removeAllViews()确保ContentView的所有Child全部被移除干净
  • inflate()将画面的内容布局解析并添加到ContentView下

第一步ensureSubDecor()的内容比较多,又分为几个子任务,包括调用createSubDecor()创建ActionBar特有布局,setWindowTitle()将Activity标题反映到ToolBar上以及applyFixedSizeWindow()去调整DecorView尺寸。

核心内容在于createSubDecor()这个子任务。它需要确保ActionBar的特有布局创建出来并和Window的DecorView产生联系。

  1. ensureWindow()

获取Activity所属的Window引用并添加window相关回调

  1. getDecorView()

告知Window去创建DecorView,这里要提一下PhoneWindow的generateLayout(),其将依据主题的创建不同的布局结构,比如AppCompatActivity的话将解析screen_simple.xml得到DecorView的基本结构,其包括根布局LinearLayout,用来映射actionmode布局的viewstub以及承载App内容的id为ContentView

  1. inflate()

获取ActionBar的布局,主要是abc_screen_toolbar.xml和abc_screen_content_include.xml两个文件

  1. removeViewAt()和addView()

ContentView的子View迁移至ActionBar布局下。具体方法是将其所有child移除并add到ActionBar布局下id为action_bar_activity_content的ViewGroup下面,并将原有ContentView的id置空,同时将该目标ViewGroup的id设置为Content。意味着它将成为AppCompatActivity画面承载内容区域的父布局

1.2 公开的API

除了setContentView()在打造布局结构上的差异,AppCompatActivity还提供了些Activity所没有的API供开发者使用。

  • getSupportActionBar() 用以获取AppCompat特有的ActionBar组件供开发者定制ActionBar
  • getDelegate() 获取AppCompatActivity内部实现的大管家AppCompatDelegate的实例(实际上将通过静态的create()获取实现类AppCompatDelegateImpl的实例)
  • getDrawerToggleDelegate() 获取抽屉导航布局DrawerLayout的代理类ActionBarDrawableToggleImpl的实例,用来和ActionBar进行UI的交互
  • onNightModeChanged() 不同于配置了uiMode的外部配置变更后才能收到主题变化的通知,本API可以在暗黑主题的适配模式(比如跟随系统设置模式和跟随电量设置模式等)发生变化后得到回调,可利用这个时机做些补充处理

1.3 使用上的注意

AppCompatActivity的注释上有如下说明,推荐采用Theme.AppCompat主题。

You can add an ActionBar to your activity when running on API level 7 or higher by extending this class for your activity and setting the activity theme to Theme.AppCompat or a similar theme.

经过验证如果我们使用了别的主题就会得到如下的crash。

You need to use a Theme.AppCompat theme (or descendant) with this activity.

原理在于上面自己的大管家AppCompatDelegate在创建ActionBar布局的时候有意地确保Activity是否采用了AppCompatTheme主题,尤其是如果没有指定AppCompat定义的windowActionBar的属性的话,将抛出如上的异常。

// AppCompatThemeImpl.java
    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.");
        }
        ...
    }

至于为什么用异常来确保AppCompatTheme的采用,因为后续的处理跟AppCompatTheme息息相关,如果没有采用后面的很多处理将失效。

2. AppCompatDialog

除了使用极高的AppCompatActivity以外,AppCompatDialog的曝光率也不低。其实现原理和AppCompatActivity企划一致,都是依赖大管家AppCompatDelegate进行实现。一样是为了在Dialog的基础上扩展出新ToolBar和暗黑主题的支持。

3. AppCompatTheme

前面提到的AppCompatTheme主要分为两个主题。

  • Theme.AppCompat

继承自Base.V7.Theme.AppCompat主题,指定AppCompatViewInflater为widget等class的解析类,并设置AppCompatTheme所定义的基本属性,其顶级主题仍旧是老牌的主题Theme.Holo

  • Theme.AppCompat.DayNight

能够自动适配暗黑主题。其继承自Base.V7.Theme.AppCompat.Light,与Theme.AppCompat的区别主要在于其默认情况下采用了light系的主题,比如colorPrimary采用primary_material_light,而Theme.AppCompat则采用primary_material_dark颜色

App采用了该主题就可以自动适配暗黑模式,这是如何做到的?

4. Dark Theme 暗黑模式

AppCompatActivity在绑定BaseContext的时候会通过AppCompatDelegate的applyDayNight()去解析App设置的暗黑主题模式并做出一些相应的配置工作。

比如常用的跟随省电模式,其指的是设备的省电模式开启后将自动进入暗黑主题,降低功耗。反之关闭之后返回到白天主题。

具体实现是AppCompatDelegate将注册监听省电模式变化的广播(ACTION_POWER_SAVE_MODE_CHANGED)。当省电模式开启/关闭时,广播接收器将自动回调updateForNightMode()去更新对应的主题。

    private boolean applyDayNight(final boolean allowRecreation) {
        ...
        @NightMode final int nightMode = calculateNightMode();
        @ApplyableNightMode final int modeToApply = mapNightMode(nightMode);
        final boolean applied = updateForNightMode(modeToApply, allowRecreation);
        ...
        if (nightMode == MODE_NIGHT_AUTO_BATTERY) {
            // 注册监听省电模式的广播接收器
            getAutoBatteryNightModeManager().setup();
        }
        ...
    }

    abstract class AutoNightModeManager {
        ...
        void setup() {
            ...
            if (mReceiver == null) {
                mReceiver = new BroadcastReceiver() {
                    @Override
                    public void onReceive(Context context, Intent intent) {
                        // 省电模式变化后的回调
                        onChange();
                    }
                };
            }
            mContext.registerReceiver(mReceiver, filter);
        }
        ...
    }

    private class AutoBatteryNightModeManager extends AutoNightModeManager {
        ...
        @Override
        public void onChange() {
            // 省电模式变化后回调主题切换方法更新主题
            applyDayNight();
        }

        @Override
        IntentFilter createIntentFilterForBroadcastReceiver() {
            if (Build.VERSION.SDK_INT >= 21) {
                IntentFilter filter = new IntentFilter();
                filter.addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED);
                return filter;
            }
            return null;
        }
    }

更新主题的处理则是如下关键代码。

    private boolean updateForNightMode(final int mode, final boolean allowRecreation) {
        ...
        // 如果Activity的BaseContext尚未初始化则直接适配新的主题值
        if ((sAlwaysOverrideConfiguration || newNightMode != applicationNightMode)
                && !mBaseContextAttached
                ...) {
            ...
            try {
                ...
                ((android.view.ContextThemeWrapper) mHost).applyOverrideConfiguration(conf);
                handled = true;
            ...
            }
        }

        final int currentNightMode = mContext.getResources().getConfiguration().uiMode
                & Configuration.UI_MODE_NIGHT_MASK;

        // 如果Activity的BaseContext已经创建,
        // 且App没有声明要处理暗黑主题变化的话,将重绘Activity
        if (!handled
                ...) {
            ActivityCompat.recreate((Activity) mHost);
            handled = true;
        }

        // 假使App声明了处理暗黑主题变化的话,
        // 那么将新的主题值更新到Configuration的uiMode属性
        // 并回调Activity#onConfigurationChanged(),等待App的自行处理
        if (!handled && currentNightMode != newNightMode) {
            ...
            updateResourcesConfigurationForNightMode(newNightMode, activityHandlingUiMode);
            handled = true;
        }

        // 最后检查是否要通知App暗黑主题模式发生变化
        // (注意这里指的是App设置的暗黑主题切换的策略发生变更,
        // 比如由跟随系统设置变更为固定暗黑模式等)
        if (handled && mHost instanceof AppCompatActivity) {
            ((AppCompatActivity) mHost).onNightModeChanged(mode);
        }
        ...
    }

细心的开发者可能会注意到我们平常在AppCompatActivity的布局里使用的控件,最终得到的类名称里会多上AppCompat的前缀。比如声明的是TextView控件最后得到的是AppCompatTextView类的实例。这是怎么做到的,为什么这么做?这就离不开ppCompatViewInflater的默默付出。

5. AppCompatViewInflater

核心功能就是将布局里的控件切换为AppCompat版本。在调用LayoutInflater解析App布局的阶段,大管家AppCompatDelegate将调用AppCompatViewInflater将布局中的控件逐个替换。

    final View createView(View parent, final String name, @NonNull Context context...) {
        ...
        switch (name) {
            case "TextView":
                view = createTextView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "ImageView":
                view = createImageView(context, attrs);
                verifyNotNull(view, name);
                break;
            ...
        }
        ...
        return view;
    }

    protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
        return new AppCompatTextView(context, attrs);
    }

除了上面提到的AppCompatTextView,AppCompat的widget目录下有很多为了兼容新特性扩展的控件。以AppCompatTextView和另一个常用的AppCompatImageView来一探究竟。

6. AppCompatTextView

由代码注释就可以看出来该控件在TextView的基础上增加了Dynamic TintAuto Size两大特性。

先看下这两特性大体是什么效果。

可以看到第二个TextView对背景着上了更深的绿色,并对icon着上了白色,使得它内部的icon和文字相较第一个TextView看起来更清楚。这是通过AppCompatTextView提供的backgroundTint和drawableTint属性实现的,这种给背景和icon动态着色的功能就是Dynamic Tint特性。

另外可以看到最下面TextView的文本内容正好铺满整个屏幕没有在末尾出现省略,而上面那个TextView的字体尺寸较大且在尾部用省略号表示。这种自动适配字体尺寸的效果同样是依赖AppCompatTextView提供的相关属性来完成。此为Auto Size特性。

6.1 Dynamic Tint

主要依赖AppCompatBackgroundHelperAppCompatDrawableManager实现,包括反映静态配置和动态修改的Tint属性。

主要经历这几步:

  1. loadFromAttributes() 解析布局里配置的Tint属性,核心处理在于能够将设置的Tint资源解析成ColorStateList实例。
// ColorStateListInflaterCompat.java
    private static ColorStateList inflate(Resources r, XmlPullParser parser) {
        ...
        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
            ...
            final int color = modulateColorAlpha(baseColor, alphaMod);
            colorList = GrowingArrayUtils.append(colorList, listSize, color);
            stateSpecList = GrowingArrayUtils.append(stateSpecList, listSize, stateSpec);
            listSize++;
        }
        ...
        return new ColorStateList(stateSpecs, colors);
    }

  1. setInternalBackgroundTint()和applySupportBackgroundTint() 负责管理和区分Tint颜色的取自静态配置的属性还是外部动态配置的参数
  2. tintDrawable()负责着色,本质在于调用Drawable#setColorFilter()去刷新颜色的绘制
// ResourceManagerInternal.java
    static void tintDrawable(Drawable drawable, TintInfo tint, int[] state) {
        ...
        if (tint.mHasTintList || tint.mHasTintMode) {
            drawable.setColorFilter(createTintFilter(
                    tint.mHasTintList ? tint.mTintList : null,
                    tint.mHasTintMode ? tint.mTintMode : DEFAULT_MODE,
                    state));
        } else {
            drawable.clearColorFilter();
        }
        ...
    }

6.2 Auto Size

需要解决的问题是对Text内容依据最大宽度和当前size计算自适应的最佳字体尺寸,依赖AppCompatTextHelperAppCompatTextViewAutoSizeHelper实现。

  1. 解析AutoSize相关属性的配置并设定是否需要自动适配字体尺寸的Flag。
// AppCompatTextViewAutoSizeHelper.java
    void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {
        ...
        if (a.hasValue(R.styleable.AppCompatTextView_autoSizeTextType)) {
            mAutoSizeTextType = a.getInt(R.styleable.AppCompatTextView_autoSizeTextType,
                    TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE);
        }
        ...
        if (supportsAutoSizeText()) {
            if (mAutoSizeTextType == TextViewCompat.AUTO_SIZE_TEXT_TYPE_UNIFORM) {
                ...
                setupAutoSizeText();
            }
        ...
        }
    }

    private boolean setupAutoSizeText() {
        if (supportsAutoSizeText()
                && mAutoSizeTextType == TextViewCompat.AUTO_SIZE_TEXT_TYPE_UNIFORM) {
            ...
            if (!mHasPresetAutoSizeValues || mAutoSizeTextSizesInPx.length == 0) {
                ...
                for (int i = 0; i < autoSizeValuesLength; i++) {
                    autoSizeTextSizesInPx[i] = Math.round(
                            mAutoSizeMinTextSizeInPx + (i * mAutoSizeStepGranularityInPx));
                }
                mAutoSizeTextSizesInPx = cleanupAutoSizePresetSizes(autoSizeTextSizesInPx);
            }
            mNeedsAutoSizeText = true;
        }
        ...
    }
  1. 在文本内容初始化或变化的时候计算合适的字体尺寸并反映到UI上。
// AppCompatTextView.java
    protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
        ...
        if (mTextHelper != null && !PLATFORM_SUPPORTS_AUTOSIZE && mTextHelper.isAutoSizeEnabled()) {
            mTextHelper.autoSizeText();
        }
    }

// AppCompatTextHelper.java
    void autoSizeText() {
        mAutoSizeTextHelper.autoSizeText();
    }

// AppCompatTextViewAutoSizeHelper.java
    void autoSizeText() {
        ...
        if (mNeedsAutoSizeText) {
            ...
            synchronized (TEMP_RECTF) {
                ...
                // 计算最佳size
                final float optimalTextSize = findLargestTextSizeWhichFits(TEMP_RECTF);
                // 如果和预设的size不一致的话更新size
                if (optimalTextSize != mTextView.getTextSize()) {
                    setTextSizeInternal(TypedValue.COMPLEX_UNIT_PX, optimalTextSize);
                }
            }
        }
        ...
    }

7. AppCompatImageView

AppCompatTextView一样扩展了针对background和src的Dynamic Tint功能。

AppCompatTextView不同的是AppCompatImageView对icon着色采用的属性不是attr#drawableTintattr#tint。由AppCompatImageHelperImageViewCompat类实现,原理大同小异,不再赘述。

8. 辅助类

AppCompat框架的开发人员在实现AppCompat扩展控件等特性的时候用到很多辅助类,大家可以自行研究下其细节,学习下一些巧妙的实现思路。

  • AppCompatBackgroundHelper
  • AppCompatDrawableManager
  • AppCompatTextHelper
  • AppCompatTextViewAutoSizeHelper
  • AppCompatTextClassifierHelper
  • AppCompatResources
  • AppCompatImageHelper

...

9. 类图

最后上一下AppCompat框架的简易类图,帮助大家有个整体上的认识。

10. 结语

可以看到AppCompat框架整体比较简单,因此也容易被大家忽视。但作为Jetpack系列里的基石,了解一下很有必要。

相关推荐

0722-6.2.0-如何在RedHat7.2使用rpm安装CDH(无CM)

文档编写目的在前面的文档中,介绍了在有CM和无CM两种情况下使用rpm方式安装CDH5.10.0,本文档将介绍如何在无CM的情况下使用rpm方式安装CDH6.2.0,与之前安装C5进行对比。环境介绍:...

ARM64 平台基于 openEuler + iSula 环境部署 Kubernetes

为什么要在arm64平台上部署Kubernetes,而且还是鲲鹏920的架构。说来话长。。。此处省略5000字。介绍下系统信息;o架构:鲲鹏920(Kunpeng920)oOS:ope...

生产环境starrocks 3.1存算一体集群部署

集群规划FE:节点主要负责元数据管理、客户端连接管理、查询计划和查询调度。>3节点。BE:节点负责数据存储和SQL执行。>3节点。CN:无存储功能能的BE。环境准备CPU检查JDK...

在CentOS上添加swap虚拟内存并设置优先级

现如今很多云服务器都会自己配置好虚拟内存,当然也有很多没有配置虚拟内存的,虚拟内存可以让我们的低配服务器使用更多的内存,可以减少很多硬件成本,比如我们运行很多服务的时候,内存常常会满,当配置了虚拟内存...

国产深度(deepin)操作系统优化指南

1.升级内核随着deepin版本的更新,会自动升级系统内核,但是我们依旧可以通过命令行手动升级内核,以获取更好的性能和更多的硬件支持。具体操作:-添加PPAs使用以下命令添加PPAs:```...

postgresql-15.4 多节点主从(读写分离)

1、下载软件[root@TX-CN-PostgreSQL01-252software]#wgethttps://ftp.postgresql.org/pub/source/v15.4/postg...

Docker 容器 Java 服务内存与 GC 优化实施方案

一、设置Docker容器内存限制(生产环境建议)1.查看宿主机可用内存bashfree-h#示例输出(假设宿主机剩余16GB可用内存)#Mem:64G...

虚拟内存设置、解决linux内存不够问题

虚拟内存设置(解决linux内存不够情况)背景介绍  Memory指机器物理内存,读写速度低于CPU一个量级,但是高于磁盘不止一个量级。所以,程序和数据如果在内存的话,会有非常快的读写速度。但是,内存...

Elasticsearch性能调优(5):服务器配置选择

在选择elasticsearch服务器时,要尽可能地选择与当前业务量相匹配的服务器。如果服务器配置太低,则意味着需要更多的节点来满足需求,一个集群的节点太多时会增加集群管理的成本。如果服务器配置太高,...

Es如何落地

一、配置准备节点类型CPU内存硬盘网络机器数操作系统data节点16C64G2000G本地SSD所有es同一可用区3(ecs)Centos7master节点2C8G200G云SSD所有es同一可用区...

针对Linux内存管理知识学习总结

现在的服务器大部分都是运行在Linux上面的,所以,作为一个程序员有必要简单地了解一下系统是如何运行的。对于内存部分需要知道:地址映射内存管理的方式缺页异常先来看一些基本的知识,在进程看来,内存分为内...

MySQL进阶之性能优化

概述MySQL的性能优化,包括了服务器硬件优化、操作系统的优化、MySQL数据库配置优化、数据库表设计的优化、SQL语句优化等5个方面的优化。在进行优化之前,需要先掌握性能分析的思路和方法,找出问题,...

Linux Cgroups(Control Groups)原理

LinuxCgroups(ControlGroups)是内核提供的资源分配、限制和监控机制,通过层级化进程分组实现资源的精细化控制。以下从核心原理、操作示例和版本演进三方面详细分析:一、核心原理与...

linux 常用性能优化参数及理解

1.优化内核相关参数配置文件/etc/sysctl.conf配置方法直接将参数添加进文件每条一行.sysctl-a可以查看默认配置sysctl-p执行并检测是否有错误例如设置错了参数:[roo...

如何在 Linux 中使用 Sysctl 命令?

sysctl是一个用于配置和查询Linux内核参数的命令行工具。它通过与/proc/sys虚拟文件系统交互,允许用户在运行时动态修改内核参数。这些参数控制着系统的各种行为,包括网络设置、文件...

取消回复欢迎 发表评论: