Android开发之Theme、Style探索及源码浅析

Theme的实质也是Style,所以Theme的定义格式与Style的基本一致,具体格式如下(都定义在res目录下,自己随意起名的xml中):

<?xml version="1.0" encoding="utf-8"?>

可以看见,一个style在定义时至少需要一个name的属性用来被使用者识别,其中的item就是各种不同的属性与指定的属性值,而style除过有name属性以外还可以有parent属性,在这里你先理解为类似java的继承重写关系即可,后面会详细介绍。

不过这里要特别注意,一般在style中使用parent字段的继承适用于继承系统平台现有定义的style,而我们想要继承自己实现的style一般不会通过parent字段来实现,而是通过指定格式的name字段来实现,如下:

看见name字段了吗?证明这个style继承自我们上面自定义的CodeFont style,可见我们自定义的继承是通过“.”来实现的,在使用时只需要@style/CodeFont.Red即可使用该继承重写的style啦,如果你还想继续在这基础上继承,那写法还是一样的,具体如下:

整明白Theme与Style的这个约定了么!就是这么简单而已,不过要注意这两种的严格区别,别乱用,譬如将系统预定义的通过name来使用时错误的。

接着我们来看看style中item属性是怎么搞来的,这玩意如果我们定义控件的style则可以直接在对应控件或者Window的API文档中或者R.attr文档中找到支持哪些属性,依次选择合适的进行使用即可(特别提醒,这个技能很重要,譬如有时候你会说我自定义的Dialog为何背景周边多一个黑框啥玩意的问题,然后上网一顿复制别人的style,也不明白别人为啥这么写,其实一个很重要的技巧就是遇到这种问题自己去API查下相关的属性就搞定了。)。使用系统已存在属性时切记不要忘记<item name="android:inputType">前面的android:前缀,还有就是item中存在的属性不见得对所有View都有效,譬如Theme中需要的以windowXXX开头的属性就不适用于View,但是不会报错,只是View会忽略这些不适合自己的属性,应用适合自己的属性。

2-2 Theme与Style的使用


有了上面的知识我们已经能够定义出Theme与Style了,下来就是怎么将定义的这些样式应用到UI中啦。将style设置到UI主要分两类,如下:

  • 对于单个控件通过style进行引入(注意:ViewGroup的style不会向下传递到子View上,除非用theme方式);

  • 对于Activity、Application等窗口级向下应用的通过theme进行引入;

在Android中有许多预定义的style供我们使用,所以在使用主题时我们可以如下使用:

#b0b0ff

2-3 Theme的兼容性处理


在新版本的Android中添加了很多新的Theme,而老版本又不兼容这些Theme,所以很多时候我们可能需要处理一下这种兼容性问题,譬如我们在res/values/styles.xml文件中定义如下Theme:

当我们想在Android3.0(API 11)以上使用新的Theme则可以res/values-v11目录下定义如下Theme:

这样当我们编译的APK在不同的设备上运行时就能自己切换选择适合自己平台的Theme了。

2-4 Android系统预制的Theme与Style选择


话说Android应用层开发之所以简单的原因就在于系统已经帮我们实现了很多自由选择的功能,关于Style与Theme也不例外(应用层开发难就难在知识面很广),具体使用可以记住如下秘诀:

  • 当我们想要知道Theme具体有哪些属性可以有效使用时,可以查阅API的R.styleable进行选择。

  • 当我们想要知道Style具体有哪些属性可以有效使用时,可以查阅API的R.attr进行选择。

  • 系统为我们提供了很多实用的Theme与Style,我们还可以通过查阅API的R.style进行选择(要注意的是这里的文档查到的不一定全,最好的办法是去查FW下base的res或者appcompat的res),不过要注意,在API中譬如Theme_NoTitleBar主题样式在我们xml引用时要替换为@android:style/Theme.NoTitleBar的格式。

2-5 Android应用资源拓展语法


上面提到的都是Theme与Style相关的东西,其实这两个东西实质都属于res资源的处理,关于Android的res资源使用规则和不同平台软硬件系统匹配的策略不属于本文范围,不过也很简单,感兴趣的同学可以移步到API Guide的App Resources进行研读。这里我们主要简单说下资源的引用语法,因为Theme与Style中也会经常使用到,免得带来不必要的疑惑。

Android中资源在Java文件中引用的语法定义如下:

[<package_name>.]R.<resource_type>.<resource_name>

//注意:当资源在当前APP中则package_name可以省略,当为系统的资源则可换位譬如android.

Android中资源在XML文件中引用的语法定义如下:

@[<package_name>:]<resource_type>/<resource_name>

//注意:package_name的规则同上java中,不过在XML中引入不是本包资源时要注意格式,譬如引用系统的资源格式为android:textColor=“@android:color/secondary_text_dark”

Android系统预制资源在XML文件中引用的特殊语法定义如下:

//可以引用系统所有资源,public & private

@*android:type/name

//只能引用系统public的资源

@android:type/name

//注意:没在frameworks/base/core/res/res/values/public.xml(也就是<sdk_path>\platforms\android-X\data\res\values\public.xml)中申明的资源App时不推荐使用的。

Android在XML文件中引用当前主题属性的语法定义如下:

?[<package_name>:][<resource_type>/]<resource_name>

//资源值允许引用当前主题中的属性的值,这个属性值只能在style资源和XML中使用,随着当前主题的切换该值也在变换,该resource_name不需要自己定义,系统会自己在当前主题下寻找,常见的譬如动画中等。

Android在XML文件中创建或者引用资源语法定义如下:

//在R.java的type内部类中添加一条静态常量id资源标识符,如果标示符(包括系统资源)已经存在则表示引用该标示符。

@+type/name

//在R.java中寻找已经定义的标识符,如果找不到则提示失败错误,一般在xml中定义有先后关系。

@type/name

//所以一般推荐直接使用+号避免不必要的意外。

Android在XML文件中xmlns语法定义如下:

//xmlns(XML Namespaces)是XML的命名空间

//通用XML命名空间格式规则

xmlns:namespace-prefix=“namespaceURI”

在Android的XML中命名空间规则如下:

xmlns:namespace-prefix=http://schemas.android.com/apk/res/应用程序包路径

在使用时规则如下:

namespace-prefix:属性

切记,xmlns的定义必须放在最外层开始的的标记中,譬如我们Activity的xml文件的根布局中的android前缀、tools前缀、自定义View的前缀等。常见的例子如下:

//android即为frameworks/base/core/res/res/values/attrs.xml中的属性

xmlns:android=“http://schemas.android.com/apk/res/android”

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

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中定义如下:

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

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

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

看注释吧,这货有接近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.

    • Below API 11: Gingerbread
    • APIs 11 thru 14: Holo
    • APIs 14 thru XX: Device default dark
    • API XX and above: Device default light with dark action bar
    • @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 resid will be retrieved from this Theme’s

      • resources, its values placed into the Theme object.

      • The semantics of this function depends on the force

      • 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

      •          obtain attribute values.
        
      • @param force If true, values in the style resource will always be

      •          used in the theme; otherwise, they will only be used
        
      •          if not already defined in the theme.
        
    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值