本文原作者: 小虾米君,原文发布于: TechMerger

https://mp.weixin.qq.com/s/S1Ph5KeA7R509BggVRdx7w

为了能够在低版本的 Android 设备上运行系统新特性,AppCompat 框架自 Support 时代就已推出。但随着 Jetpack 体系的 AndroidX 库的横空出世,AppCompat 相关类则一并迁移到了 AndroidX 库里。

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

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 结构。

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 相关回调。

  2. getDecorView()

    告知 Window 去创建 DecorView,这里要提一下 PhoneWindow 的 generateLayout(),其将依据主题的创建不同的布局结构。

    比如 AppCompatActivity 的话将解析 screen_simple.xml 得到 DecorView 的基本结构,其包括根布局 LinearLayout,用来映射 actionmode 布局的 viewstub 以及承载 App 内容的 ID 为 ContentView。

  3. inflate()

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

  4. removeViewAt() & addView()

    ContentView 的子 View 迁移至 ActionBar 布局下。

    具体方法是将其所有 child 移除并 add 到 ActionBar 布局下 ID 为 action_bar_activity_content 的 ViewGroup 下面,并将原有 ContentView 的 ID 置空,同时将该目标 ViewGroup 的 ID 设置为 Content。

    意味着它将成为 AppCompatActivity 画面承载内容区域的父布局。

公开的 API

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

  • getSupportActionBar() 用以获取 AppCompat 特有的 ActionBar 组件供开发者定制 ActionBar;

  • getDelegate() 获取 AppCompatActivity 内部实现的大管家 AppCompatDelegate 的实例 (实际上将通过静态的 create() 获取实现类 AppCompatDelegateImpl 的实例);

  • getDrawerToggleDelegate() 获取抽屉导航布局 DrawerLayout 的代理类 ActionBarDrawableToggleImpl 的实例,用来和 ActionBar 进行 UI 的交互;

  • onNightModeChanged() 不同于配置了 uiMode 的外部配置变更后才能收到主题变化的通知。

    本 API 可以在暗黑主题的适配模式 (比如跟随系统设置模式和跟随电量设置模式等) 发生变化后得到回调,可利用这个时机做些补充处理。

使用上的注意

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 息息相关,如果没有采用后面的很多处理将失效。

AppCompatDialog

除了使用频率极高的 AppCompatActivity 以外,AppCompatDialog 的曝光率也不低,也是为了在 Dialog 的基础上扩展出新 ToolBar 和暗黑主题的支持。

其实现原理和 AppCompatActivity 几乎一致,都是依赖大管家 AppCompatDelegate 进行实现。

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 采用了该主题就可以自动适配暗黑模式,这是如何做到的?

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() {@Overridepublic void onReceive(Context context, Intent intent) {// 省电模式变化后的回调onChange();}};}mContext.registerReceiver(mReceiver, filter);}...
}private class AutoBatteryNightModeManager extends AutoNightModeManager {...@Overridepublic void onChange() {// 省电模式变化后回调主题切换方法更新主题applyDayNight();}@OverrideIntentFilter 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没有声明要处理暗黑主题变化的话,将重绘Activityif (!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 类的实例。这是怎么做到的,为什么这么做?这就离不开 AppCompatViewInflater 的默默付出。

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 来一探究竟。

AppCompatTextView

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

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

可以看到第二个 TextView 对背景着上了更深的绿色,并对 icon 着上了白色,使得它内部的 icon 和文字相较第一个 TextView 看起来更清楚。

这是通过 AppCompatTextView 提供的 backgroundTint 和 drawableTint 属性实现的,这种给背景和 icon 动态着色的功能就是 Dynamic Tint 特性。

另外可以看到最下面 TextView 的文本内容正好铺满整个屏幕没有在末尾出现省略,而上面那个 TextView 的字体尺寸较大且在尾部用省略号表示。

这种自动适配字体尺寸的效果同样是依赖 AppCompatTextView 提供的相关属性来完成。此为 Auto Size 特性。

Dynamic Tint

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

主要经历这几步:

1. loadFromAttributes() 解析布局里配置的 Tint 属性,核心处理在于能够将设置的 Tint 资源解析成 ColorStateList 实例。

setInternalBackgroundTint() 和 applySupportBackgroundTint() 负责管理和区分 Tint颜色的取自静态配置的属性还是外部动态配置的参数。

// 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);
}

2. setInternalBackgroundTint() 和 applySupportBackgroundTint() 负责管理和区分 Tint 颜色的取自静态配置的属性还是外部动态配置的参数。

3. 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();}...
}

Auto Size

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

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;}...
}

2. 在文本内容初始化或变化的时候计算合适的字体尺寸并反映到 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) {...// 计算最佳sizefinal float optimalTextSize = findLargestTextSizeWhichFits(TEMP_RECTF);// 如果和预设的size不一致的话更新sizeif (optimalTextSize != mTextView.getTextSize()) {setTextSizeInternal(TypedValue.COMPLEX_UNIT_PX, optimalTextSize);}}}...
}

AppCompatImageView

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

AppCompatTextView 不同的是 AppCompatImageView 对 icon 着色采用的属性不是 attr#drawableTintattr#tint

主要由 AppCompatImageHelperImageViewCompat 类实现,原理大同小异,不再赘述。

辅助类

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

  • AppCompatBackgroundHelper

  • AppCompatDrawableManager

  • AppCompatTextHelper

  • AppCompatTextViewAutoSizeHelper

  • AppCompatTextClassifierHelper

  • AppCompatResources

  • AppCompatImageHelper


类图

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

总结

可以看到 AppCompat 框架整体比较简单,因此也容易被大家忽略。但作为 Jetpack 系列里的基石,了解并掌握显得尤为必要。


长按右侧二维码

查看更多开发者精彩分享

"开发者说·DTalk" 面向中国开发者们征集 Google 移动应用 (apps & games) 相关的产品/技术内容。欢迎大家前来分享您对移动应用的行业洞察或见解、移动开发过程中的心得或新发现、以及应用出海的实战经验总结和相关产品的使用反馈等。我们由衷地希望可以给这些出众的中国开发者们提供更好展现自己、充分发挥自己特长的平台。我们将通过大家的技术内容着重选出优秀案例进行谷歌开发技术专家 (GDE) 的推荐。

 点击屏末 | 阅读原文 | 即刻报名参与 "开发者说·DTalk"


AppCompat 发布两年了,还没了解? | 开发者说·DTalk相关推荐

  1. OSChina 周四乱弹 ——前两天BUG还没改完啊?老子不改了!

    2019独角兽企业重金招聘Python工程师标准>>> Osc乱弹歌单(2017)请戳(这里) [今日歌曲] @=clouddyy  :#每日乱弹音乐# <Safe and S ...

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

    近日随笔 近期疫情日渐严峻,大家多多保重,出门记得戴口罩.希望河北,黑龙江能尽早控制住好局面迎来拐点,全国人民过个好年. 为了能够让低版本的Android系统能够运行新特性,AppCompat框架自S ...

  3. java eden s0 s1_不是吧!做了两年java还没弄懂JVM堆?进来看看你就明白了

    堆的核心概述 一个JVM实例只存在一个堆内存,堆也是java内存管理的核心区域 Java堆区在jvm启动的时候被创建,其空间大小也就确定了.是jvm管理的最大一块内存空间.(堆内存的大小可以调节) & ...

  4. c语言结构体中的ps,练习结构体的时候出错,(ps有两个函数还没写)

    该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 #include typedef struct { int num; char name[20]; int score[3]; } Student; vo ...

  5. mysql数据库错误1317_请问为什么我的mysql数据库一直连接不上?两天了还没找到错误,请帮我看一下呀急急急...

    我的mysql数据库版本是是5.7.26,但是导入驱动时,找不到5.7.26,导入是5.1.13 最后出错的信息是 org.mybatis.spring.MyBatisSystemException: ...

  6. DALL·E才发布两天就被复现?官方论文还没出,大神们就在自制代码和视频了

    萧箫 发自 凹非寺 量子位 报道 | 公众号 QbitAI 没想到,OpenAI刚公布DALL·E,就已经有人在复现了. 虽然还是个半成品,不过大体框架已经搭建好了,一位第三方作者Philip Wan ...

  7. 官方论文还没出,刷爆AI圈的DALL·E刚发布就被复现?两天800 star!

    点击上方"CVer",选择加"星标"置顶 重磅干货,第一时间送达 萧箫 发自 凹非寺 来源:量子位(QbitAI) 没想到,OpenAI刚公布DALL·E,就已 ...

  8. 进公司两个月了还没上手项目_27个“经验证且易于上手”的赚钱在线业务创意

    进公司两个月了还没上手项目 Are you looking for online business ideas to make extra income on the side? The intern ...

  9. 电脑关机Matlab文件没保存,文件还没保存就关机了?别怕,两招搞定它

    原标题:文件还没保存就关机了?别怕,两招搞定它 大家好!我依然是你们帅气依旧的小琛哥. 大家有没有碰到过这种情况. 老板交给你一项很重要的任务! 虽然晚上十二点了,但是今天晚上必须赶出来! 结果电脑突 ...

最新文章

  1. HTML5----CSS显示半个字符
  2. c++学习笔记之基础---类内声明函数后在类外定义的一种方法
  3. RX学习笔记:正则表达式
  4. pandas 入门(2)
  5. sed教程入门与实例练习(一)
  6. 两台电脑怎么共享_怎么在电脑上创建共享文件(必须是在同一个网段)
  7. HTML5之美(转)
  8. 人脸识别demo使用教程
  9. VSTO 实现word的多级列表功能
  10. 牛客网华为机试题训练汇总(JavaScript)
  11. 最牛比的NBIOT芯片MDM9206
  12. 算法学习:LeetCode-592. 分数加减运算
  13. python画网络图_python3中NetworkX网络图绘制
  14. 笔迹宽度估计的低质量文本图像二值化(Robust Document Image Binarization Technique for Degraded Document Images)
  15. Android开发必会技术!Flutter中网络图片加载和缓存源码分析,完整PDF
  16. Unity 3D中的射线与碰撞检测
  17. 潘多拉开发板——emwin5.44裸机移植记录(ST7789驱动)
  18. 【知识图谱】深入浅出讲解知识图谱(技术、构建、应用)
  19. 微信小程序开发入门教程(十一)
  20. 设计模式 | 备忘录模式及典型应用

热门文章

  1. 【教程概览】整理下我学习过的C++教程
  2. 如何登陆系统服务器,linux系统 怎么登陆服务器
  3. linux下去掉^M字符
  4. 0-300V 50mA 可调电源
  5. makefile 中的.PHONY
  6. webstorm激活方法
  7. clickhouse Mutations删除操作报错及解决方案
  8. 关于Asp.net中DataGrid绑定事件DataGrid1_ItemDataBound的奇怪问题!
  9. NVIDIA查看CPU、内存、GPU、DLA使用情况
  10. 海贼王热血航线服务器维护,航海王热血航线手游2021年3月23日例行停服维护公告_航海王热血航线手游3月23日更新了什么_玩游戏网...