美文网首页Android UITheme/Style/attrsAndroid知识
Android-Theme和Style的使用以及Theme原理浅

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

作者: 1da4ea6f4995 | 来源:发表于2017-03-15 15:14 被阅读425次
    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的启动机制?同时通过对这一次源码的探究,大大减轻了面对一大推源码的手足无措感,对以后的源码阅读提升增加了几分信心!路漫漫其修远兮,继续带刀向前赶!

    相关文章

      网友评论

        本文标题:Android-Theme和Style的使用以及Theme原理浅

        本文链接:https://www.haomeiwen.com/subject/ozcogttx.html