Android-Theme和Style的使用以及Theme原理浅
首先感谢工匠若水大神,与qinjuning大神文章的解惑与引路,有不明白的地方,第二篇文章写得很是清楚,大家可以去看这篇文章。
http://blog.csdn.net/yanbober/article/details/51015630
http://blog.csdn.net/qinjuning/article/details/8829877
总:最近一段时间在写项目练手,被一个Theme(主题)和style弄得晕头转脑,于是决定将theme和style的使用和原理浅浅的探究一下!这样既能加深自己对android的理解,也能明白theme和style的原理,以不至于天天喊着我刚才设置的白色主题怎么显示成黑了!我的style怎么不起作用了!deng deng deng 之类的笑话了!写下这篇文章加深自己的理解。
****一、Theme和Style的使用****
首先我们要明白一件事情,Theme是用来定义activity和dialog的样式,还可以定义内部元素的样式(这样一般使用style)。而Style主要是用来定义内部元素(说白了就是内部控件)的样式。从Theme定义在style.xml中我们就可以看出,Theme的本质其实也是Style。
1、使用Theme
这个很简单再我们新建一个项目是,工具就一边给我们的app添加了一个默认的Theme,在Manifest.xml文件中application节点下android:theme=""属性就是给我们添加了一个默认的主题,表示我们整个app都建使用这个主题。
在style.xml文件中我们可以找到这个主题的定义:
1、colorPrimary表示actionBar的颜色
2、coorPrimaryDark表示状态栏的颜色
3、colorAccent是定义重要的颜色,如EditText的下划线,和光标
Theme和style的设置遵循就近原则,即使我们整个app设置了一个统一的主题,也可以给每个activity都设置主题,这二者并不冲突。
2、使用style
首相我们在style.xml文件中新建一个style,名字叫TextStyle,属性如下:
然后我们再布局文件中将这个样式给一个textview使用,然后我们再运行一下,看一下效果。
Paste_Image.png Paste_Image.png看!效果出来了!和我们所定义的效果一样。我们再回过头来看我们再布局文件中并无给textview添加任何属性,但是我们给textview添加了一个style,然后我们再看看我们所定义的TextStyle样式,item中所定义的属性,是不是就是我们平时在布局文件中给控件添加的属性了?对!style中所能定义的属性,就是控件能支持的属性,至于能定义一些什么属性,可以到api中进行详细的查看,下面来说说style的定义。
****二、Theme和Style的定义****
Style的定义很简单,想要给控件增加那些效果,然后查看对应api是否支持,在将需要定义的属性放到item中进行定义就好。Theme是一种特殊的style,其定义模式和style一样,只是item中所定义的属性不同。
具体的定义可以翻看上不我们所定义的Theme以及Style,这里我们降下Theme和Style都是支持父级的,我们可以理解为java中的继承,子级拥有父级的属性,进行扩展与修改。
Theme的父级定义是使用paret字段,下面这个就是我们自定义的BaseTheme,它的父级为Theme.AppComPat.Light.DarkActionBar,然后如果我们再点击进去会发现它的上面还有一集父级,这说明父级可以是多级的。
Style的父级定义就是[ 父级名. 子级名 ],下面我们来写一个Style它的父级为上面我们定义的TextStyle,代码如下: Paste_Image.png
****三、Theme和Style的兼容性****
android版本的更新,对于有一些属性在一些版本上面是不被支持的,因此我们需要做版本的兼容。比如我们定义了一个Theme或Style,有些属性在5.1以上的版本中才支持,我们就可以这样在values文件夹中的style.xml中定义低版本的style或theme
然后我们再建立一个名为valus-v19的文件夹,建立style.xml文件,添加名字相同的theme,然后这里定义sdk为19以上版本的theme
Paste_Image.png****四、追根溯源Theme****
我们上面几步建立的Theme都有一个父级,那我们我们就一步一步的往上找,找到Theme的根源。
我们翻找AppThem的父级可以发现,我们可以看到父级的里面也是一层一层的套着的
在往上找几层我们可以找到Theme.Light 这个Style,可以发现到这里就没有Theme父了,而是Style地继承,者也很好的说明Theme的本质就是Style,然后我们发现这里面定义了许多的item。
Paste_Image.png然后我们继续往上找一层,发现了Theme的老巢了!Theme!这个里面定义了几百个各种各样的属性,然后我们仔细的看看这些属性,不都是一些控件的属性吗? 对的,Theme里面就是定义了各种控件,窗口(Dialog、Activity)等的样式属性。我们平时用到的Theme.NoActionBar等以及其他一些Theme都是对Theme的扩展、修改。
Paste_Image.png****五、Theme与Style加载原理浅浅析****
之前我们说过我们设置Theme可以在Manifest.xml文件中定义,还能在java代码中设置使用setTheme(这个我没在实际代码中用过,而且必须是在setContentView()方法之前设置),而android初始化界面的入口,是setContentView(),那么我们就从这里开始着手浅析。
现在我们写activity时一般是继承AppComatActivity,这个是用来进行版本适配的,最近继承的还是Activity,所以我们直接来看Activity中的setContentView()方法。
/**
* Set the activity content from a layout resource. The resource will be
* inflated, adding all top-level views to the activity.
*
* @param layoutResID Resource ID to be inflated.
*
* @see #setContentView(android.view.View)
* @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
*/
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
/**
* Set the activity content to an explicit view. This view is placed
* directly into the activity's view hierarchy. It can itself be a complex
* view hierarchy. When calling this method, the layout parameters of the
* specified view are ignored. Both the width and the height of the view are
* set by default to {@link ViewGroup.LayoutParams#MATCH_PARENT}. To use
* your own layout parameters, invoke
* {@link #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)}
* instead.
*
* @param view The desired content to display.
*
* @see #setContentView(int)
* @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
*/
public void setContentView(View view) {
getWindow().setContentView(view);
initWindowDecorActionBar();
}
/**
* Set the activity content to an explicit view. This view is placed
* directly into the activity's view hierarchy. It can itself be a complex
* view hierarchy.
*
* @param view The desired content to display.
* @param params Layout parameters for the view.
*
* @see #setContentView(android.view.View)
* @see #setContentView(int)
*/
public void setContentView(View view, ViewGroup.LayoutParams params) {
getWindow().setContentView(view, params);
initWindowDecorActionBar();
}
这里有三个重载方法,用处都是一样的设置布局,我们看一个就好了,方法里面先调用getWindow(),然后再调用setContentView();
/**
* Retrieve the current {@link android.view.Window} for the activity.
* This can be used to directly access parts of the Window API that
* are not available through Activity/Screen.
*
* @return Window The current window, or null if the activity is not
* visual.
*/
public Window getWindow() {
return mWindow;
}
getWindow(),获取了一个window对象,然后再调用window对象的setContentView()方法,window是一个抽象类,我们直接来看他的实现类PhoneWindow,PhoneWindow实现了Window的全部方法,是window的唯一实现类。
@Override
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
在方法中我们可以看到,首先判断视图为不为空,然后调用installDecor()方法,这里我们可以看到,如果布局不为空的话会移除所有的控件,我们再看installDecor()方法。
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
//!!!这里传-1进去只是获得了一个FrameLayout,即生成一个布局
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
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);
if (decorContentParent != null) {
//后面是一大段代码,前面是
······
······
·······
}
}
我擦嘞!这么长的一个方法,有点蒙圈了,当时要看还是的。首先获取了一个window的一个装饰类mDecor(一个FrameLayout),后面就是对其的一些设置,我们主要看generateLayout(mDecor)方法,这个里面它直接得到了一个内容布局。
protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.
TypedArray a = getWindowStyle();
if (false) {
System.out.println("From style:");
String s = "Attrs:";
for (int i = 0; i < R.styleable.Window.length; i++) {
s = s + " " + Integer.toHexString(R.styleable.Window[i]) + "="
+ a.getString(i);
}
System.out.println(s);
}
···············
···············
}
mDecor.finishChanging();
return contentParent;
}
一看又是一个大方法,但是不要慌!稳住,方法的第一行代码,我们要的东西已经出来了,getWindowStyle(),这里就是重点所在了,而后面的那些代码则是依据getWindowStyle()返回的TypeArry进行设置,我们进入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()方法,我们接着往下面看。
/**
* 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) {
//注意这里!!!getTheme()
return getTheme().obtainStyledAttributes(attrs);
}
还是没有什么特别的,就是获取到主题,然后返回主题的TypeArry对象,好给后续步骤进行主题设置。我们点进getTheme方法中看。
/**
* Return the Theme object associated with this Context.
*/
@ViewDebug.ExportedProperty(deepExport = true)
public abstract Resources.Theme getTheme();
是Context中的一个抽象方法,那么它在Context的那个子类里面实现了了?在ContextThemeWrapper中实现了,那我们进入类中看看getTheme方法做了什么。
@Override
public Resources.Theme getTheme() {
if (mTheme != null) {
return mTheme;
}
// 这里是选择默认的主题资源
mThemeResource = Resources.selectDefaultTheme(mThemeResource,
getApplicationInfo().targetSdkVersion);
initializeTheme();
return mTheme;
}
// 设置Theme,通过调用此方法然后mTheme和mThemeResource就不为null了,然后再初始化题。
// 当我们在Manifest.xml 中配置了them,在应用程序启动时会调用此方法,具体的需要去查找android启动activity的过程。
@Override
public void setTheme(int resid) {
if (mThemeResource != resid) {
mThemeResource = resid;
initializeTheme();
}
}
如主题不为空就直接返回主题,然后先选择一个默认的主题资源。我们也可以进去看看,没有什么特别的,就是对不同的sdk选择对应的主题资源。这里我们主要看initializeTheme()初始化主题。
private void initializeTheme() {
final boolean first = mTheme == null;
if (first) {
mTheme = getResources().newTheme();//调用方法获取主题,就是new了一个Theme。
final Resources.Theme theme = getBaseContext().getTheme();//调用ContextImpl类的getTheme(),获取默认的Theme
if (theme != null) {
mTheme.setTo(theme);//将获取到的默认的主题设置给mTheme
}
}
onApplyThemeResource(mTheme, mThemeResource, first);//应用主题资源
}
当主题为空时,new一个Theme,然后调用ContextImpl类的getTheme(),获取默认的Theme ,然后将得到的Theme设置给mTheme,然后通过onApplyThemeResource(),应用主题资源,最后是AsserManger中调用原生的方法(c 或 c++)去加载资源。
然后我们再回到getWindowStyle()方法中,这里对用了obtainStyledAttributes()方法,并且传进去com.android.internal.R.styleable.Window这是一组自定义属性集合,在android内置的系统资源中,里面定义者window的基本属性属性。而这个方法最终是通过AssetManager获取这一组自定义的属性集合,得到一个TypeArry以供使用,去设置界面窗口的样式。
<declare-styleable name="Window">
<attr name="windowBackground" /> //该界面所对应的背景图片, drawable / color
<attr name="windowFrame" /> //该界面所对应的前景frontground图片, drawable / color
<attr name="windowNoTitle" /> //是否带有title , boolean类型
<attr name="windowFullscreen" /> //是否全屏 , boolean类型
<attr name="windowIsFloating" /> //是否是悬浮窗类型 , boolean类型
<attr name="windowIsTranslucent" /> //是否透明 , boolean类型
<attr name="windowSoftInputMode" /> //设置键盘弹出来的样式 , 例如: adjustsize 等 ,其实也是int类型
<......>
<......>
<declare-styleable />
其中的而在Theme节点中有包含着window的定义,特殊的是如果某个自定义属性如果没有指名 format属性,那么该属性必须在当前已经定义,即该属性只是一个别名。而我们自定义的Theme则必须是要指定一个parent字段,我们所自定义的也只是对系统所定义的做一个修改与扩展。
<declare-styleable name="Theme">
<attr name="windowBackground" format="reference" />
<!-- Drawable to use as a frame around the window. -->
<attr name="windowFrame" format="reference" />
<!-- Flag indicating whether there should be no title on this window. -->
<attr name="windowNoTitle" format="boolean" />
<!-- Flag indicating whether this window should fill the entire screen. -->
<attr name="windowFullscreen" format="boolean" />
<!-- Flag indicating whether this is a floating window. -->
<attr name="windowIsFloating" format="boolean" />
<!-- Flag indicating whether this is a translucent window. -->
<attr name="windowIsTranslucent" format="boolean" />
<!-- Flag indicating that this window's background should be the
user's current wallpaper. -->
<attr name="windowShowWallpaper" format="boolean" />
<!-- This Drawable is overlaid over the foreground of the Window's content area, usually
to place a shadow below the title. -->
<!-- This Drawable is overlaid over the foreground of the Window's content area, usually
to place a shadow below the title. -->
<attr name="windowContentOverlay" format="reference" />
<!--more -->
</declare-styleable>
知道Theme的使用与原理了,那么对于Style则就能够理解了,在控件初始化时获取style属性,回去style定义,获取TypeArry对象给控件进行设置,然后控件绘制是就会依据TypeArry里面获取的属性来绘制控件,从而实现样式效果。
****总结:****对于Theme的原理,上面这些论述还有许多不清楚的地方,我也只是知道一个大概,并不是完全的清楚,所以以后对这一块还是不能说自己已经清楚了,同时又生出了许多的问题比如:Context到底是个什么东西?Activity的启动机制?同时通过对这一次源码的探究,大大减轻了面对一大推源码的手足无措感,对以后的源码阅读提升增加了几分信心!路漫漫其修远兮,继续带刀向前赶!