Android ContextThemeWrapper应用

前言

Android obtainStyledAttributes获取属性值(注:在后文中称此文为前文)文中最后的例子处,提出了一个问题——如何对单个元素设置Theme而不影响到整个Activity。

该问题的实际意义是对于有些属性我们只能从Theme中获取而不能通过内联的方式嵌入在单个元素标签内(具体分析可参见前文),因此要想修改这个属性值,只能在Activity使用的Theme中指定。于是,该修改的作用域就会是整个Activity——这不是我们希望的!

在前文中,我提出了使用ContextThemeWrapper来解决该问题。那接下来就来看看应该怎么做把。

ContextThemeWrapper是什么

还是从前文可以得知,View在提取属性时除了从内联的属性集获取值,还从与Context相关联的Theme(通过Context#getTheme获取)中提取属性值。一般来说Activity的getTheme返回的对象就是在注册Activity时指定的Theme了(当然也有可能在运行时通过Context#setTheme改变)。

由于在inflate布局文件时,默认View会从父亲节点继承Context,于是整个布局文件中的所有元素都是使用和根元素一样的Context。自然我们在Theme中定义的某些属性会影响到所有元素。

ContextThemeWrapper是一个类,在Android源码中有对其简短的说明,说这是一个允许你更改或者替换Theme的Context包装。

A context wrapper that allows you to modify or replace the theme of the wrapped context.

其有一个构造方法

    /**
     * Creates a new context wrapper with the specified theme.
     * <p>
     * The specified theme will be applied on top of the base context's theme.
     * Any attributes not explicitly defined in the theme identified by
     * <var>themeResId</var> will retain their original values.
     *
     * @param base the base context
     * @param themeResId the resource ID of the theme to be applied on top of
     *                   the base context's theme
     */
    public ContextThemeWrapper(Context base, @StyleRes int themeResId) {
        super(base);
        mThemeResource = themeResId;
    }

这个构造方法有比较清晰的文档注释,说的是新指定的Theme会被至于原始Theme的顶上(Top),在新的Theme中未指定的值会使用base中的值。意思是这个类除了应用新的Theme外,还保留了Base Theme中的值,只不过会优先使用新的Theme中的值。

看到这个地方其实应该大概了解ContextThemeWrapper是什么了。确实,正如其名,它就是一个可以指定Theme的Context包装。用于在View构造时为其提供Theme属性集。

如何使用ContextThemeWrapper

了解了如上知识,那具体又如何使用呢。我们虽然知道ContextThemeWrapper可以在基础Theme的前提下包装另一个Theme,但要如何把它用起来呢?这儿有两种方法。

代码中使用

这种方式适合手动添加View组件的情况,通过上面提供的ContextThemeWrapper构造方法生成ContextThemeWrapper,然后作为Context提供给View即可。

//手动创建ContextThemeWrapper
Context context = new ContextThemeWrapper(base, resId);
//将ContextThemeWrapper作为context提供给TextView
TextView tv = new TextView(context);

布局中使用

相比起需要在代码中手动构造ContextThemeWrapper,在布局中填充的View也是有办法的。且更方便!在上文中说道默认情况下,View在被inflate时会使用与父节点一样的Context,既然是默认情况,那也有特殊的情况,就是对其指定android:theme注意!一定是带“android”命名空间的属性,示例如下:

    <!-- “FrameStyle”包裹在Activity的Theme之上,且对FrameLayout本身及所有子元素起作用 -->
    <FrameLayout
        android:theme="@style/FrameStyle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp">

        <!-- 此处指定的“RedTextStyle” 包裹在“FrameStyle”之上 -->
        <TextView
            android:theme="@style/RedTextStyle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="red text"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="hello"/>
    </FrameLayout>

这其实是Framework在解析标签时为我们创建了ContextThemeWrapper对象,具体是在LayoutInflater类中实现的,如下:

SDK/sources/android-25/android/view/LayoutInflater.java

    View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
        ...
        // Apply a theme wrapper, if allowed and one is specified.
        if (!ignoreThemeAttr) { //此处一般情况ignoreThemeAttr值为false
            //ATTRS_THEME声明为    private static final int[] ATTRS_THEME = new int[] { com.android.internal.R.attr.theme };
            //即为“android:theme”指定的内容
            final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
            final int themeResId = ta.getResourceId(0, 0);
            if (themeResId != 0) {
                //后续会将context传递给工厂方法用于构造View实例。
                context = new ContextThemeWrapper(context, themeResId);
            }
            ta.recycle();
        }
        ...
    }

如此,我们可以任意控制Theme的作用域了!
配合前文属性的提取流程的讲解,我们可以通过修改属性简单的实现很多效果了。

阅读更多
个人分类: 技术杂记
上一篇Android 属性(attr)引用
下一篇Python BeautifulSoup4 select方法执行css选择器
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭