Android 开发之 Theme、Style 探索及源码浅析,kotlin 语言实例精解
<!-- The default theme for apps on API level 10 and lower. This is the theme used for
activities that have not explicitly set their own theme.
<p>You can count on this being a dark
background with light text on top, but should try to make no
other assumptions about its appearance. In particular, the text
inside of widgets using this theme may be completely different,
with the widget container being a light color and the text on top
of it a dark color.
<p>If you're developing for API level 11 and higher, you should instead use {@link
#Theme_Holo} or {@link #Theme_DeviceDefault}.</p>
-->
<style name="Theme">
......
......
......
......
......
......
......
<!-- Define these here; ContextThemeWrappers around themes that define them should
always clear these values. -->
......
......
......
<!-- Presentation attributes (introduced after API level 10 so does not
have a special old-style theme. -->
......
......
......
......
......
......
......
......
......
......
......
......
......
......
......
......
......
......
......
......
......
......
</style>
看注释吧,这货有接近 400 多个 item 属性,这也就是我们 Android 关于 Theme 的开山鼻祖了,在我们自定义时其实来这看比去 API 查还方便呢(其实需要两个互相配合,一个查,一个看解释,哈哈),因为它里面定义了关于我们整个应用中文字样式、按钮样式、列表样式、窗体样式、对话框样式等,这些样式都是默认样式,它还有很多我们常用的扩展样式,譬如 Theme.Light、Theme.NoTitleBar、Theme.NoTitleBar.Fullscreen 等等,反正你要有需求来这里搞就行。当我们继承使用时只用在前加上 android:即可,有些属性可能是找不到的。同理,我们所谓的 style、attr 等等也都是这么个框架,大致位置也类似主题 Theme 的,所以这里不再过多说明,自行脑补即可。
3-2 Theme、Style 等 res 资源客户化流程
对于纯 App 开发来说这一个知识点可以忽略,因为本小节需要大致了解 Android 源码的结构和编译框架,对于固件等开发来说这个还是比较重要的,记得以前做 TV 盒子开发时很多系统资源需要替换及添加,也就是说会稍微涉及到修改 System UI 及 FW 的 res,那时候好坑爹,虽然修改的地方不多,只是换几个图标和加几个资源,但是那时候自己还是蒙圈了一段时间才搞明白,所以说有必要啰嗦几句。
首先我们先要明白设备里系统目录下的这些常见 jar 与 apk 的来源,如下:
| 名字 | 解释 |
| --- | --- |
| am.jar | 执行 am 命令所需的 java lib,对应 FW 的 base/cmds/am 目录,具体可以参考下面的 Android.mk 定义。 |
| framework-res.apk | Android 系统资源库集合,对应 FW 的 core/res 目录,具体同理参见 Android.mk 定义。 |
| framework.jar | Android SDK 核心代码,对应 FW 的 base 目录,具体可以参考目录下的 Android.mk 的 MOUDLE 定义。 |
| SystemUI.apk | 从 Android2.2 开始状态栏和下拉通知栏被分割出一个单独的 SystemUI.apk,一般在 system 的 app 或者 priv-app 下(还有很多其他模块呢,譬如 SettingProvider 等,具体可以在设备下看看),对应的源码在 FW 的 packages 下的 SystemUI 中。 |
| Others | 其他的 jar 比较多,不做一一介绍,不同厂商可能还会不同定制,具体可在厂商设备的 system 下看看有哪些包,对应回去通过 Android.mk 文件寻找即可。 |
| android.jar | 切记这个特例,这货是 make sdk 生成的,多方整合,别以为也可以找到对应目录,木有的!还有就是这个 jar 很实用的,很多时候我们想用 AS 直接调运系统的 hide API 等,自己编译一个就能派上用场啦! |
有了上边这几个和我们本文相关的核心常识后我们简单说下怎么修改编译:
修改 FW/base/XXX/下面需要修改的代码;
单独在 XXX 下 mm 编译生成 XXX.jar(apk);
把编译的 jar(apk)包(在 out 目录对应路径下)push 到设备系统 system 的 FW 目录下;
reboot 重启设备验证;
不过这里有些坑大家要明白,我们在 mm 前最好每次都去清除对应 out/obj 目录下的中间文件,特别是资源文件更新时,否则容易被坑。还有就是切记添加系统 API 或者修改 @hide 的 API 或者添加资源(包含添加修改 public.xml 等)后,需要执行 make update-api 命令来同步 base/api 下的 current.txt 的修改,完事再 make 就行啦,这些编译文档都有介绍。
有了上面这些相信大家对于客户化资源也就有了一些认识啦,想想如果我们需要用到 framework.jar 的 hide 资源或者 framework-res.apk 中新加的资源时又不想用反射和源码下编译怎么办?当然是编译一个 no hide 的 jar 引入我们工程即可哇,要注意我们引入以后一定是 Providered 的模式,也就是该 jar 只编译不打包入该 apk,还有就是依赖的先后优先级顺序,否则又用的是 sdk 默认的。还有就是万能的 android.jar 也是一种曲线救国的办法。当然啦,如果是 SDK 开发则完全可以复制一份自己搞,完事编译进系统即可,同时提供给 App 开发。
3-3 Theme、Style 加载时机及加载源码浅析
前面我们介绍了 Android 的 Theme、Style 的定义及使用及 Theme、Style 等 res 的由来,这里我们来看看这些被使用的 Theme 的最终是何时、怎样被加载生效的。我们都知道对于 Theme 有两种方式来使用,具体如下(Style 等 attr 在 View 的使用也比较同类,这里只分析 Theme、其他的请在 View 等地自行分析脑补):
在 AndroidManifest.xml 中
<application>
或者<activity>
节点设置 android:theme 属性;在 Java 代码中调用 setTheme()方法设置 Activity 的 Theme(须在 setContentView()前设置;
可以看见,这两种方式我们都比较常用,甚至有时候还会设置 Window 的一些属性标记,这些标记方法都在 Window 类中。我们平时在设置这些 Theme 时总是有很多疑惑,譬如为毛只能在 setContentView()前设置等等,那么下面我们就来庖丁解牛一把。故事在开始之前可能还需要你自行脑补下《Android应用setContentView与LayoutInflater加载解析机制源码分析》与《Android应用Activity、Dialog、PopWindow、Toast窗口添加机制及源码分析》两篇文章,完事再来继续下面的内容。
关于 Activity 通过 setContentView 方法设置 View 的来源这里就不多说了,参考前面两篇即可,我们直接跳到 PhoneWindow 的 setContentView 方法来看下,如下:
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();//每个 Activity 第一次进来必走
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
......
}
我们接着来看下 installDecor()方法,如下:
private void installDecor() {
if (mDecor == null) {
//仅仅 new DecorView(getContext(), -1)而已,也就是 FrameLayout
mDecor = generateDecor();
......
}
if (mContentParent == null) {
//生成我们布局的父布局
mContentParent = generateLayout(mDecor);
// Set up decor part of UI to ignore fitsSystemWindows if appropriate.
mDecor.makeOptionalFitsSystemWindows();
final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
R.id.decor_content_parent);
......
}
}
接着我们继续看看 generateLayout(mDecor);这个方法,如下:
protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.
//获取当前主题,重点!!!!!!!
TypedArray a = getWindowStyle();
......
//解析一堆主题属性,譬如下面的是否浮动 window(dialog)等
mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
......
// Inflate the window decor.
//依据属性获取不同的布局添加到 Decor
int layoutResource;
int features = getLocalFeatures();
// System.out.println("Features: 0x" + Integer.toHexString(features));
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
layoutResource = R.layout.screen_swipe_dismiss;
}
......
View in = mLayoutInflater.inflate(layoutResource, null);
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
mContentRoot = (ViewGroup) in;
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
......
return contentParent;
}
一样喽,继续先看下 getWindowStyle()方法是神马鬼,这个方法在其基类 Window 中,如下:
/**
Return the {@link android.R.styleable#Window} attributes from this
window's theme.
*/
public final TypedArray getWindowStyle() {
synchronized (this) {
if (mWindowStyle == null) {
mWindowStyle = mContext.obtainStyledAttributes(
com.android.internal.R.styleable.Window);
}
return mWindowStyle;
}
}
哎,没啥好看的,没有逻辑,就是流程,继续跟吧,去 Context 类看看 obtainStyledAttributes(com.android.internal.R.styleable.Window)方法吧,如下:
/**
Return the Theme object associated with this Context.
*/
@ViewDebug.ExportedProperty(deepExport = true)
public abstract Resources.Theme getTheme();
/**
Retrieve styled attribute information in this Context's theme. See
{@link android.content.res.Resources.Theme#obtainStyledAttributes(int[])}
for more information.
@see android.content.res.Resources.Theme#obtainStyledAttributes(int[])
*/
public final TypedArray obtainStyledAttributes(@StyleableRes int[] attrs) {
//获取当前 Theme 对应的 TypedArray 对象
return getTheme().obtainStyledAttributes(attrs);
}
哎呦我去,憋大招呢,急死人了!可以看见 Context 的 getTheme()方法时一个抽象方法,那他的实现在哪呢,看过《Android应用Context详解及源码解析》一文的同学一定知道对于 Activity 来说他的实现类就是 ContextThemeWapprer,那我们赶紧进去看看它到底搞了啥玩意,如下:
@Override
public Resources.Theme getTheme() {
//一旦设置有 Theme 则不再走后面逻辑,直接返回以前设置的 Theme
if (mTheme != null) {
return mTheme;
}
//没有设置 Theme 则获取默认的 selectDefaultTheme
mThemeResource = Resources.selectDefaultTheme(mThemeResource,
getApplicationInfo().targetSdkVersion);
//初始化选择的主题,mTheme 就不为 null 了
initializeTheme();
return mTheme;
}
@Override
public void setTheme(int resid) {
//通过外部设置以后 mTheme 和 mThemeResource 就不为 null 了
if (mThemeResource != resid) {
mThemeResource = resid;
//初始化选择的主题,mTheme 就不为 null 了
initializeTheme();
}
}
我勒个去,憋大招总算憋出来翔了,ContextThemeWapprer 才是重头戏啊,总算看见了光明了。这里的 getTheme 方法有一个判断,没有设置过 Theme(mTheme 为空)则通过 Resources.selectDefaultTheme 获取默认主题,否则用 setTheme 设置的主题。那么我们就来先看下假设没有设置主题,使用默认主题的方法,Resources.selectDefaultTheme 如下:
/**
Returns the most appropriate default theme for the specified target SDK version.
<ul>
<li>Below API 11: Gingerbread
<li>APIs 11 thru 14: Holo
<li>APIs 14 thru XX: Device default dark
<li>API XX and above: Device default light with dark action bar
</ul>
@param curTheme The current theme, or 0 if not specified.
@param targetSdkVersion The target SDK version.
@return A theme resource identifier
@hide
*/
public static int selectDefaultTheme(int curTheme, int targetSdkVersion) {
return selectSystemTheme(curTheme, targetSdkVersion,
com.android.internal.R.style.Theme,
com.android.internal.R.style.Theme_Holo,
com.android.internal.R.style.Theme_DeviceDefault,
com.android.internal.R.style.Theme_DeviceDefault_Light_DarkActionBar);
}
/** @hide */
public static int selectSystemTheme(int curTheme, int targetSdkVersion, int orig, int holo,
int dark, int deviceDefault) {
if (curTheme != 0) {
return curTheme;
}
if (targetSdkVersion < Build.VERSION_CODES.HONEYCOMB) {
return orig;
}
if (targetSdkVersion < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
return holo;
}
if (targetSdkVersion < Build.VERSION_CODES.CUR_DEVELOPMENT) {
return dark;
}
return deviceDefault;
}
哎呀妈呀,这不就解释了我们创建不同版本的 App 时默认主题不一样的原因么,哈哈,原来如果我们没有设置主题 Theme,系统会依据版本给我们选择一个默认的主题,也就是上面这段代码实现了该功能。
我们回过头继续回到 ContextThemeWapprer 的 getTheme 方法,当我们已经设置了 Theme 该方法就直接返回了,恰巧设置 Theme 的方法也在 ContextThemeWapprer 中。那这个方法啥时候被调运的呢?这一小节一开始我们就说了 Activity 的 Theme 设置有两种方法,主动通过 Java 调运 setTheme()和在 AndroidManifest 文件配置,AndroidManifest 文件配置的 Theme 又是啥时候调运的呢?有了前面几篇博客的铺垫,我想你也一定能找到的,就在 ActivityThread 的 performLaunchActivity()方法中,也就是我们通过 startActivity()方法启动 Activity 时就调运了 Activity 的 setTheme 方法,这个就不多说了,感兴趣的自己进去看下就行了,也是流程憋大招,最终调用了 activity.setTheme()完成了 AndroidManifest 文件的 Theme 获取。
我们现在把目光回到 ContextThemeWapprer 的 setTheme 或者 getTheme 中调运的 initializeTheme()方法中来看看,如下:
protected void onApplyThemeResource(Resources.Theme theme, int resid, boolean first) {
theme.applyStyle(resid, true);
}
//大招!!!!!!!
private void initializeTheme() {
//这就解释了为何 setTheme 必须在 setContentView 前调运,不多解释了,很明白了吧!!!!!!!!
final boolean first = mTheme == null;
if (first) {
mTheme = getResources().newTheme();
Resources.Theme theme = getBaseContext().getTheme();
if (theme != null) {
mTheme.setTo(theme);
}
}
onApplyThemeResource(mTheme, mThemeResource, first);
}
这个方法就解释了为何 setTheme 必须在 setContentView 前调运。最终通过 onApplyThemeResource 调运 Resources.Theme 的方法进行了设置,如下:
/**
Place new attribute values into the theme. The style resource
specified by <var>resid</var> will be retrieved from this Theme's
resources, its values placed into the Theme object.
<p>The semantics of this function depends on the <var>force</var>
argument: If false, only values that are not already defined in
the theme will be copied from the system resource; otherwise, if
any of the style's attributes are already defined in the theme, the
current values in the theme will be overwritten.
@param resId The resource ID of a style resource from which to
@param force If true, values in the style resource will always be
*/
public void applyStyle(int resId, boolean force) {
AssetManager.applyThemeStyle(mTheme, resId, force);
mThemeResId = resId;
mKey.append(resId, force);
}
到此注释也说明了一些概念,关于 AssetManager 的应用又是另一个大话题了,这里先不展开讨论,我们只用知道到此一个 Theme 就选择完成了,还有就是一个 Theme 的是怎么被选择出来的,当然对于 Dialog 等 Window 的 Theme 也是一个样子,这里不多说明,感兴趣的自行脑补即可。
到现在为止我们已经找到了 Theme 是怎么来的了,下来我们需要回到我们这一小节开头部分的源码分析,也就是 Resources 的 obtainStyledAttributes()方法,还记得我们最终传递了 com.android.internal.R.styleable.Window 进行获取该 style 么。这货不就是 FW 中 res 的 attr.xml 中自定义的属性么,如下:
<declare-styleable name="Window">
<attr name="windowBackground" />
<attr name="windowBackgroundFallback" />
......
<attr name="windowLightStatusBar" format="boolean" />
</declare-styleable>
可以看见,Style、Theme 其实就是一组自定义的内置在 Android 系统资源中的属性集合,而这里唯一比较特殊的就是这些定义的属性没有声明 format 字段。其实在 Android 中如果某个自定义属性没有声明 format 属性则意味着该属性已经定义过,这里只是别名而已。在哪定义的呢?当然还是 attr 中哇,属性么,自然只能在这了,找找看发下如下:
<declare-styleable name="Theme">
......
<attr name="windowBackground" format="reference" />
<!-- Drawable to draw selectively within the inset areas when the windowBackground
has been set to null. This protects against seeing visual garbage in the
surface when the app has not drawn any content into this area. -->
<attr name="windowBackgroundFallback" format="reference" />
......
</declare-styleable>
原来 Theme 属性集才是这货的正身哇,哈哈。有了这些属性集在 themes.xml 中的 Theme st
yle 就是对上面这些属性的设置值了,如下样例:
<style name="Theme">
<item name="isLightTheme">false</item>
<item name="colorForeground">@color/bright_foreground_dark</item>
<item name="colorForegroundInverse">@color/bright_foreground_dark_inverse</item>
......
</style>
哈哈,有没有觉得和 App 层开发自定义 attr 和 style 一样呢,原来系统也是这么干的,哈哈!
到此我们已经知道 Theme 是怎么来的,其中的 attr 是怎么定义及设置的,下面我们就回到 PhoneWindow 的 generateLayout(DecorView decor) 方法来看下万事俱备以后的应用大杂烩即可,如下:
protected ViewGroup generateLayout(DecorView decor) {
//上面已经分析过了,获取 Theme 及 Theme 中 Style 定义的 attr 的属性,这里获取 Window 的属性
TypedArray a = getWindowStyle();
//获取是否 floating 的窗口,也就是是否 Dialog
mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
......
//各种属性获取及 feature、flag 设置
if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
requestFeature(FEATURE_NO_TITLE);
} else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
// Don't allow an action bar if there is no title.
requestFeature(FEATURE_ACTION_BAR);
}
......
if (a.getBoolean(R.styleable.Window_windowFullscreen, false)) {
setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN & (~getForcedWindowFlags()));
}
// Inflate the window decor.
//依据 Theme 及 feature、flag 获取一个匹配的 layoutResourceId 资源布局
int layoutResource;
int features = getLocalFeatures();
// System.out.println("Features: 0x" + Integer.toHexString(features));
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
layoutResource = R.layout.screen_swipe_dismiss;
评论