深度解读Jetpack框架的基石-AppCompat
nanshan 2024-11-20 19:25 10 浏览 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产生联系。
- ensureWindow()
获取Activity所属的Window引用并添加window相关回调
- getDecorView()
告知Window去创建DecorView,这里要提一下PhoneWindow的generateLayout(),其将依据主题的创建不同的布局结构,比如AppCompatActivity的话将解析screen_simple.xml得到DecorView的基本结构,其包括根布局LinearLayout,用来映射actionmode布局的viewstub以及承载App内容的id为ContentView
- inflate()
获取ActionBar的布局,主要是abc_screen_toolbar.xml和abc_screen_content_include.xml两个文件
- 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 Tint和Auto Size两大特性。
先看下这两特性大体是什么效果。
可以看到第二个TextView对背景着上了更深的绿色,并对icon着上了白色,使得它内部的icon和文字相较第一个TextView看起来更清楚。这是通过AppCompatTextView提供的backgroundTint和drawableTint属性实现的,这种给背景和icon动态着色的功能就是Dynamic Tint特性。
另外可以看到最下面TextView的文本内容正好铺满整个屏幕没有在末尾出现省略,而上面那个TextView的字体尺寸较大且在尾部用省略号表示。这种自动适配字体尺寸的效果同样是依赖AppCompatTextView提供的相关属性来完成。此为Auto Size特性。
6.1 Dynamic Tint
主要依赖AppCompatBackgroundHelper和AppCompatDrawableManager实现,包括反映静态配置和动态修改的Tint属性。
主要经历这几步:
- 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);
}
- setInternalBackgroundTint()和applySupportBackgroundTint() 负责管理和区分Tint颜色的取自静态配置的属性还是外部动态配置的参数
- 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计算自适应的最佳字体尺寸,依赖AppCompatTextHelper和AppCompatTextViewAutoSizeHelper实现。
- 解析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;
}
...
}
- 在文本内容初始化或变化的时候计算合适的字体尺寸并反映到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#drawableTint是attr#tint。由AppCompatImageHelper和ImageViewCompat类实现,原理大同小异,不再赘述。
8. 辅助类
AppCompat框架的开发人员在实现AppCompat扩展控件等特性的时候用到很多辅助类,大家可以自行研究下其细节,学习下一些巧妙的实现思路。
- AppCompatBackgroundHelper
- AppCompatDrawableManager
- AppCompatTextHelper
- AppCompatTextViewAutoSizeHelper
- AppCompatTextClassifierHelper
- AppCompatResources
- AppCompatImageHelper
...
9. 类图
最后上一下AppCompat框架的简易类图,帮助大家有个整体上的认识。
10. 结语
可以看到AppCompat框架整体比较简单,因此也容易被大家忽视。但作为Jetpack系列里的基石,了解一下很有必要。
相关推荐
- Linux 的磁盘系统,和你了解的Windows差别很大
-
我的C盘去哪了?一个系统,如果没有存储,那么也就不能称之为系统。存储性是一个完整系统的重要组成部分。例如AWS最开始的服务就是S3(用来存储数据的云服务),足以见得存储对于一个应用平台是多么的重要。...
- 一文读懂 Linux 硬盘挂载:从问题到解决方案
-
各位互联网大厂的后端开发伙伴们!在咱们日常工作中,操作Linux系统是常有的事儿吧。你们有没有遇到过这样的场景:新添加了一块硬盘,满心欢喜准备用来存储重要数据或者部署新的应用服务,却突然发现不知道...
- 硬盘分区(硬盘分区格式)
-
磁盘(硬盘)分区,可以分C、D、E等分区,大家可能都会用,会根据自已的需要确定所需的空间,但分区是如何工作的呢,内容如下。Windows中有3类:MBR分区:MasterBootRecord,也...
- parted命令工具分区介绍(particle命令)
-
linux系统磁盘分区通常可以使用fdisk和parted命令,当分区大小小于2TB的时候,两种皆可以使用,当分区大于2TB的话,就需要用parted分区。以下介绍parted命令相关使用,以sdb为...
- Linux 服务器上查看磁盘类型的方法
-
方法1:使用lsblk命令lsblk输出说明:TYPE列显示设备类型,如disk(物理磁盘)、part(分区)、rom(只读存储)等。NAME列显示设备名称(如sda、nvme0n1)。TR...
- Linux分区命令fdisk和parted使用介绍
-
摘要:一般情况下,Linux分区都是选择fdisk工具,要求硬盘格式为MBR格式,能支持的最大分区空间为2T。但是目前在实际生产环境中使用的磁盘空间越来越大,呈TB级别增长;而常用的fdisk这个工具...
- linux 分区原理与名词解释(linux操作系统中的分区类型)
-
分区的意义将磁盘分成几份,每份挂在到文件系统的那个目录在linux里的文件系统Ext2:早期的格式,不支持日志功能Ext3:ext2改良版,增加了日志功能,是最基本且最常用的使用格式了Ext4:针对e...
- linux 分区合并(linux合理分区)
-
查看虚拟机当前磁盘挂载情况fdisk-l选择磁盘fdisk/dev/sda查看磁盘分区情况p重新选择分区n选择主分区p保存w创建物理卷pvcreate/dev/sda3查看物理卷信息pvdi...
- 如何在 Linux 系统中永久禁用交换分区 ?
-
Linux操作系统中的交换分区或交换文件充当硬盘上的临时存储区域,当物理内存(RAM)满时,系统使用该存储区域。它用于交换较少使用的内存页,这样系统就不会因为运行应用程序而耗尽物理内存。随着技术的发...
- Linux 如何知道硬盘已用多少空间、未用多少空间
-
刚出社会时,去了一家公司上班,老板为了省钱,买的服务器是低配的,硬盘大小只有40G,有一次网站突然不能访问了,排查半天才知道原来服务器的硬盘空间已用完,已无可用空间。第一步是查看硬盘的使用情况,第二步...
- 用Linux系统管理磁盘空间 就该这么来
-
要想充分有效的管理使用Linux系统中的存储空间,用户必须要做的就是双管齐下,一边扩充空间一边限制空间。不得不说的就是很多时候磁盘空间就像水资源,需节制水流。说到要如何实现限制空间就离不开使用LVM技...
- Windows 11 磁盘怎么分区?(windows11磁盘怎么分区)
-
Windows11磁盘分区技术解析与操作指南:构建高效存储体系一、磁盘分区的技术本质与系统价值磁盘分区作为存储系统的基础架构,通过逻辑划分实现数据隔离与管理优化。Windows11采用NTF...
- linux上创建多个文件分区,格式化为 ext2、ext3、ext4、XFS 文件
-
以下是在Linux系统上创建多个20GB文件分区并格式化为不同文件系统的分步指南:步骤1:创建基础文件(4个20GB文件)bash#创建4个20GB稀疏文件(实际占用空间随写入量增长)ddif=/...
- 救命的U盘低格哪家最强?(低格优盘)
-
周二时有位童鞋留言说U盘之前做过引导盘,现在格式化不了,用各种工具都不行,而且因为U盘厂商的关系,查不到U盘主控,无法量产恢复,特来求助。小编花了点时间特意弄坏一个U盘分区,终于试出方法了,特来分享一...
- Linux 查看硬件磁盘存储大小和磁盘阵列(RAID)的组合方式
-
一、查看硬件磁盘存储大小查看所有磁盘信息:#lsblk该命令会列出所有磁盘(如/dev/sda、/dev/nvme0n1)及其分区和挂载点。查看磁盘总容量:fdisk-l#或parted-...
你 发表评论:
欢迎- 一周热门
-
-
UOS服务器操作系统防火墙设置(uos20关闭防火墙)
-
极空间如何无损移机,新Z4 Pro又有哪些升级?极空间Z4 Pro深度体验
-
手机如何设置与显示准确时间的详细指南
-
如何在安装前及安装后修改黑群晖的Mac地址和Sn系列号
-
NAS:DS video/DS file/DS photo等群晖移动端APP远程访问的教程
-
如何修复用户配置文件服务在 WINDOWS 上登录失败的问题
-
日本海上自卫队的军衔制度(日本海上自卫队的军衔制度是什么)
-
一加手机与电脑互传文件的便捷方法FileDash
-
爱折腾的特斯拉车主必看!手把手教你TESLAMATE的备份和恢复
-
10个免费文件中转服务站,分享文件简单方便,你知道几个?
-
- 最近发表
- 标签列表
-
- linux 查询端口号 (58)
- docker映射容器目录到宿主机 (66)
- 杀端口 (60)
- yum更换阿里源 (62)
- internet explorer 增强的安全配置已启用 (65)
- linux自动挂载 (56)
- 禁用selinux (55)
- sysv-rc-conf (69)
- ubuntu防火墙状态查看 (64)
- windows server 2022激活密钥 (56)
- 无法与服务器建立安全连接是什么意思 (74)
- 443/80端口被占用怎么解决 (56)
- ping无法访问目标主机怎么解决 (58)
- fdatasync (59)
- 405 not allowed (56)
- 免备案虚拟主机zxhost (55)
- linux根据pid查看进程 (60)
- dhcp工具 (62)
- mysql 1045 (57)
- 宝塔远程工具 (56)
- ssh服务器拒绝了密码 请再试一次 (56)
- ubuntu卸载docker (56)
- linux查看nginx状态 (63)
- tomcat 乱码 (76)
- 2008r2激活序列号 (65)