写点什么

Android 开发之 Theme、Style 探索及源码浅析,音视频小程序开发

用户头像
Android架构
关注
发布于: 8 小时前

//开发调试利器,不再过多说明


xmlns:tools="http://schemas.android.com/tools"


//Email App 中 res/values/attrs.xml 等自定义属性


xmlns:settings="http://schemas.android.com/apk/res/com.android.email"


2-6 Android 应用 Theme、Style 使用小结




到此关于 Android 应用中如何定义 Theme、Style 及使用和继承重写相信大家已经明白了,再出现诡异的现象就可以通过查询相关 API 及 google 结合就能完全理会其中的原因了,而不是停留在能搜到复制;下面一节我们将针对上面的这些使用进行粗略的源码分析说明。


**【工匠若水 [http://blog.csdn.net/yanbober](


) 未经允许严禁转载,请尊重作者劳动成果。[私信联系我](


)】**


3 源码结构浅析


============


有了上面的应用使用基础,下面的源码简单浅析可能存在跳跃性和经验性,不会像之前博客那样系统性的从头到尾进行分析,而是分点点到为止,感兴趣的同学可以自行深入研读。


3-1 追根溯源 Theme、Style 等根源




在我们 App 开发中通常我们会在新建工程后的 AndroidManifest.xml 文件中看见工程默认引用了应用包下自定义的主题 @style/AppTheme(用法完全符合上一大节的规则)。该主题在当前应用包的 style.xml 中定义如下:


<resources>


<item name="colorPrimary">@color/colorPrimary</item>


<item name="colorPrimaryDark">@color/colorPrimaryDark</item>


<item name="colorAccent">@color/colorAccent</item> </style>


</resources>


看着木有,它活生生的继承了 Theme.AppCompat.Light.DarkActionBar 这个 style,这玩意又在 framework 的 support v7 包下 res 的 themes.xml 文件中,具体如下:


<style name="Theme.AppCompat.Light.DarkActionBar" parent="Base.Theme.AppCompat.Light.DarkActionBar" />


这个继承关系一直追踪下去到了该包下的 themes_base.xml 中的如下代码:


<style name="Platform.AppCompat.Light" parent="android:Theme.Light">


<item name="android:windowNoTitle">true</item>


......


</style>


哈哈,原来如此,这里的 Theme.Light 你应该十分熟悉了吧(这就是以前我们 App 用的不是 Support 包,而是默认的时候,theme 默认就是这玩意哈),这玩意就在 framework 的 base 下的 themes.xml 中定义着呢(所以通过了 android:进行引用,留意细节吧),具体如下:


<!-- Theme for a light background with dark text on top. Set your activity


to this theme if you would like such an appearance. As with the


default theme, you should try to assume little more than that the


background will be a light color.


<p>This is designed for API level 10 and lower.</p>-->


<style name="Theme.Light">


<item name="isLightTheme">true</item>


<item name="windowBackground">@drawable/screen_background_selector_light</item>


......


</style>


到这里我们就很容易明白啦,Theme.Light 的父类原来是 Theme 哇,也在这个文件中,如下:


<!-- 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 等,自己编译一个就能派上用场啦! |


有了上边这几个和我们本文相关的核心常识后我们简单说下怎么修改编译:


  1. 修改 FW/base/XXX/下面需要修改的代码;

  2. 单独在 XXX 下 mm 编译生成 XXX.jar(apk);

  3. 把编译的 jar(apk)包(在 out 目录对应路径下)push 到设备系统 system 的 FW 目录下;

  4. 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 设


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


置有两种方法,主动通过 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 中自定义的属性么,如下:

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Android开发之Theme、Style探索及源码浅析,音视频小程序开发