为了可以让低版本的Android系统可以运行新特性,AppCompat框架自Support时代就已推出。但随着AndroidX的一统江湖,AppCompat的相关类则一并迁移到了AndroidX库里。java
Android开发者应该都不陌生,在Android Studio上建立的项目默认采用AppCompatActivity
做为Activity的基类。能够说,这个类是整个AppCompat
框架里最重要的类,也是咱们今天研究AppCompat
的起点。android
其间接继承自Activity,之间还继承了其余Activity特点类,可使得低版本上运行的Activity也能拥有ToolBar和暗黑主题等新功能。markdown
AppCompatActivity extends FragmentActivity extends ComponentActivity extends ComponentActivity extends Activity*app
类 | 做用 |
---|---|
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。ide
那这些差别是如何实现的,有什么用意?布局
谈到AppCompatActivity
实现的话不得不提幕后的大管家AppCompatDelegate
类,其承载了AppCompatActivity
几乎全部的实现工做。学习
好比AppCompatActivity
复写了setContentView()的逻辑,交由大管家AppCompatDelegate
去实现其特有的UI结构。字体
重点介绍下大管家的头号工做:setContentView(),具体分为以下几个小任务。ui
ensureSubDecor()
确保ActionBar的特有UI结构建立完毕
removeAllViews()
确保ContentView的全部Child所有被移除干净
inflate()
将画面的内容布局解析并添加到ContentView下
第一步ensureSubDecor()的内容比较多,又分为几个子任务,包括调用createSubDecor()建立ActionBar特有布局,setWindowTitle()将Activity标题反映到ToolBar上以及applyFixedSizeWindow()去调整DecorView尺寸。
核心内容在于createSubDecor()这个子任务。它须要确保ActionBar的特有布局建立出来并和Window的DecorView产生联系。
获取Activity所属的Window引用并添加window相关回调
告知Window去建立DecorView,这里要提一下PhoneWindow的generateLayout(),其将依据主题的建立不一样的布局结构,好比AppCompatActivity
的话将解析screen_simple.xml获得DecorView的基本结构,其包括根布局LinearLayout,用来映射actionmode布局的viewstub以及承载App内容的id为ContentView
获取ActionBar的布局,主要是abc_screen_toolbar.xml和abc_screen_content_include.xml两个文件
将ContentView
的子View迁移至ActionBar布局下。具体方法是将其全部child移除并add到ActionBar布局下id为action_bar_activity_content的ViewGroup下面,并将原有ContentView
的id置空,同时将该目标ViewGroup的id设置为Content。意味着它将成为AppCompatActivity
画面承载内容区域的父布局
除了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
息息相关,若是没有采用后面的不少处理将失效。
除了使用极高的AppCompatActivity
之外,AppCompatDialog
的曝光率也不低。其实现原理和AppCompatActivity
企划一致,都是依赖大管家AppCompatDelegate
进行实现。同样是为了在Dialog的基础上扩展出新ToolBar和暗黑主题的支持。
前面提到的AppCompatTheme
主要分为两个主题。
继承自Base.V7.Theme.AppCompat主题,指定AppCompatViewInflater
为widget等class的解析类,并设置AppCompatTheme
所定义的基本属性,其顶级主题仍旧是老牌的主题Theme.Holo
可以自动适配暗黑主题。其继承自Base.V7.Theme.AppCompat.Light,与Theme.AppCompat
的区别主要在于其默认状况下采用了light系的主题,好比colorPrimary采用primary_material_light
,而Theme.AppCompat
则采用primary_material_dark颜色
App采用了该主题就能够自动适配暗黑模式,这是如何作到的?
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
的默默付出。
核心功能就是将布局里的控件切换为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
来一探究竟。
由代码注释就能够看出来该控件在TextView的基础上增长了Dynamic Tint和Auto Size两大特性。
先看下这两特性大致是什么效果。
能够看到第二个TextView对背景着上了更深的绿色,并对icon着上了白色,使得它内部的icon和文字相较第一个TextView看起来更清楚。这是经过AppCompatTextView
提供的backgroundTint和drawableTint属性实现的,这种给背景和icon动态着色的功能就是Dynamic Tint特性。
另外能够看到最下面TextView的文本内容正好铺满整个屏幕没有在末尾出现省略,而上面那个TextView的字体尺寸较大且在尾部用省略号表示。这种自动适配字体尺寸的效果一样是依赖AppCompatTextView
提供的相关属性来完成。此为Auto Size特性。
主要依赖AppCompatBackgroundHelper
和AppCompatDrawableManager
实现,包括反映静态配置和动态修改的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);
}
复制代码
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();
}
...
}
复制代码
须要解决的问题是对Text内容依据最大宽度和当前size计算自适应的最佳字体尺寸,依赖AppCompatTextHelper
和AppCompatTextViewAutoSizeHelper
实现。
// 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;
}
...
}
复制代码
// 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);
}
}
}
...
}
复制代码
和AppCompatTextView
同样扩展了针对background和src的Dynamic Tint功能。
与AppCompatTextView
不一样的是AppCompatImageView
对icon着色采用的属性不是attr#drawableTint是attr#tint。由AppCompatImageHelper
和ImageViewCompat
类实现,原理大同小异,再也不赘述。
AppCompat
框架的开发人员在实现AppCompat
扩展控件等特性的时候用到不少辅助类,你们能够自行研究下其细节,学习下一些巧妙的实现思路。
AppCompatBackgroundHelper
AppCompatDrawableManager
AppCompatTextHelper
AppCompatTextViewAutoSizeHelper
AppCompatTextClassifierHelper
AppCompatResources
AppCompatImageHelper
...
最后上一下AppCompat
框架的简易类图,帮助你们有个总体上的认识。
能够看到AppCompat
框架总体比较简单,所以也容易被你们忽视。但做为Jetpack系列里的基石,了解一下颇有必要。