Android UITheme/Style/attrsAndroid知识

Android-Theme和Style的使用以及Theme原理浅

2017-03-15  本文已影响425人  1da4ea6f4995
01.jpg

首先感谢工匠若水大神,与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都建使用这个主题。

Paste_Image.png

在style.xml文件中我们可以找到这个主题的定义:
1、colorPrimary表示actionBar的颜色
2、coorPrimaryDark表示状态栏的颜色
3、colorAccent是定义重要的颜色,如EditText的下划线,和光标

Paste_Image.png

Theme和style的设置遵循就近原则,即使我们整个app设置了一个统一的主题,也可以给每个activity都设置主题,这二者并不冲突。
2、使用style
首相我们在style.xml文件中新建一个style,名字叫TextStyle,属性如下:

Paste_Image.png

然后我们再布局文件中将这个样式给一个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,然后如果我们再点击进去会发现它的上面还有一集父级,这说明父级可以是多级的。

Theme定义父级.png
Style的父级定义就是[ 父级名. 子级名 ],下面我们来写一个Style它的父级为上面我们定义的TextStyle,代码如下: Paste_Image.png

****三、Theme和Style的兼容性****
android版本的更新,对于有一些属性在一些版本上面是不被支持的,因此我们需要做版本的兼容。比如我们定义了一个Theme或Style,有些属性在5.1以上的版本中才支持,我们就可以这样在values文件夹中的style.xml中定义低版本的style或theme

Paste_Image.png

然后我们再建立一个名为valus-v19的文件夹,建立style.xml文件,添加名字相同的theme,然后这里定义sdk为19以上版本的theme

Paste_Image.png

****四、追根溯源Theme****
我们上面几步建立的Theme都有一个父级,那我们我们就一步一步的往上找,找到Theme的根源。
我们翻找AppThem的父级可以发现,我们可以看到父级的里面也是一层一层的套着的

Paste_Image.png

在往上找几层我们可以找到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的启动机制?同时通过对这一次源码的探究,大大减轻了面对一大推源码的手足无措感,对以后的源码阅读提升增加了几分信心!路漫漫其修远兮,继续带刀向前赶!

上一篇下一篇

猜你喜欢

热点阅读