在运行时覆盖资源

时间:2015-05-29 06:36:16

标签: java android android-activity

问题

我希望能够在运行时覆盖我的应用资源,例如R.colour.brand_colour或R.drawable.ic_action_start。我的应用程序连接到CMS系统,该系统将提供品牌颜色和图像。一旦应用程序下载了CMS数据,它就需要能够自我修复。

我知道你要说什么 - 在运行时覆盖资源是不可能的。

除了它有点。特别是我从2012年发现了这个Bachelor Thesis,它解释了基本概念 - android扩展ContextWrapper中的Activity类,其中包含attachBaseContext方法。您可以覆盖attachBaseContext以使用您自己的自定义类包装Context,该自定义类将覆盖getColor和getDrawable等方法。你自己的getColor实现可以看起来像它想要的颜色。 Calligraphy library使用类似的方法注入自定义LayoutInflator,可以处理加载自定义字体。

代码

我创建了一个简单的Activity,它使用这种方法来覆盖颜色的加载。

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(new CmsThemeContextWrapper(newBase));
    }

    private class CmsThemeContextWrapper extends ContextWrapper{

        private Resources resources;

        public CmsThemeContextWrapper(Context base) {
            super(base);
            resources = new Resources(base.getAssets(), base.getResources().getDisplayMetrics(), base.getResources().getConfiguration()){
                @Override
                public void getValue(int id, TypedValue outValue, boolean resolveRefs) throws NotFoundException {
                    Log.i("ThemeTest", "Getting value for resource " + getResourceName(id));
                    super.getValue(id, outValue, resolveRefs);
                    if(id == R.color.theme_colour){
                        outValue.data = Color.GREEN;
                    }
                }

                @Override
                public int getColor(int id) throws NotFoundException {
                    Log.i("ThemeTest", "Getting colour for resource " + getResourceName(id));
                    if(id == R.color.theme_colour){
                        return Color.GREEN;
                    }
                    else{
                        return super.getColor(id);
                    }
                }
            };
        }

        @Override
        public Resources getResources() {
            return resources;
        }
    }
}

问题是,它不起作用!日志记录显示调用加载资源,例如layout / activity_main和mipmap / ic_launcher,但是从不加载color / theme_colour。似乎上下文用于创建窗口和操作栏,而不是活动的内容视图。

我的问题是 - 布局充气器从哪里加载资源,如果不是活动上下文?我也想知道 - 是否有一种可行的方法来覆盖颜色的加载和运行时的drawable?

关于替代方法的一个词

我知道可以通过CMS数据以其他方式主题应用 - 例如我们可以创建一个方法getCMSColour(String key)然后在我们的onCreate()内我们有一堆代码:

myTextView.setTextColour(getCMSColour("heading_text_colour"))

可以采用类似的方法来绘制drawable,字符串等。但是这会导致大量的样板代码 - 所有这些都需要维护。修改UI时,很容易忘记在特定视图上设置颜色。

包装上下文以返回我们自己的自定义值是更清洁'并且不太容易破损。在探索替代方法之前,我想了解它为什么不起作用。

3 个答案:

答案 0 :(得分:9)

虽然“动态覆盖资源”似乎是您问题的直接解决方案,但我认为更简洁的方法是使用官方数据绑定实现https://developer.android.com/tools/data-binding/guide.html,因为它并不意味着黑客攻击安卓方式。

您可以使用POJO传递品牌设置。您可以编写@color/button_color并使用所需的值绑定视图,而不是使用@{brandingConfig.buttonColor}之类的静态样式。使用适当的活动层次结构,不应添加太多样板。

这也使您能够更改布局中更复杂的元素,即:根据品牌设置在其他布局上包含不同的布局,使您的UI可以高度配置,而不需要太多精力。

答案 1 :(得分:5)

经过相当长的搜索,我终于找到了一个很好的解决方案。

protected void redefineStringResourceId(final String resourceName, final int newId) {
        try {
            final Field field = R.string.class.getDeclaredField(resourceName);
            field.setAccessible(true);
            field.set(null, newId);
        } catch (Exception e) {
            Log.e(getClass().getName(), "Couldn't redefine resource id", e);
        }
    }

对于样本测试,

private Object initialStringValue() {
                // TODO Auto-generated method stub
                 return getString(R.string.initial_value);
            }

在主要活动中,

before.setText(getString(R.string.before, initialStringValue()));

            final String resourceName = getResources().getResourceEntryName(R.string.initial_value);
            redefineStringResourceId(resourceName, R.string.evil_value);

            after.setText(getString(R.string.after, initialStringValue()));

此解决方案最初由 Roman Zhilich

发布

ResourceHackActivity

答案 2 :(得分:4)

与Luke Sleeman基本上有相同的问题,我看一下LayoutInflater在解析XML布局文件时如何创建视图。我专注于检查为什么分配给布局中TextView的text属性的字符串资源不会被自定义Resources返回的ContextWrapper对象覆盖。同时,通过TextView.setText()TextView.setHint()以编程方式设置文本或提示时,将按预期覆盖字符串。

这就是在CharSequence(sdk v 23.0.1)的构造函数中以TextView形式接收文本的方式:

// android.widget.TextView.java, line 973
text = a.getText(attr);

其中a是之前获得的TypedArray

 // android.widget.TextView.java, line 721
 a = theme.obtainStyledAttributes(attrs, com.android.internal.R.styleable.TextView, defStyleAttr, defStyleRes);

Theme.obtainStyledAttributes()方法在AssetManager上调用本机方法:

// android.content.res.Resources.java line 1593
public TypedArray obtainStyledAttributes(AttributeSet set,
            @StyleableRes int[] attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
...
        AssetManager.applyStyle(mTheme, defStyleAttr, defStyleRes,
                parser != null ? parser.mParseState : 0, attrs, array.mData, array.mIndices);

...

这是AssetManager.applyStyle()方法的声明:

// android.content.res.AssetManager.java, line 746
/*package*/ native static final boolean applyStyle(long theme,
        int defStyleAttr, int defStyleRes, long xmlParser,
        int[] inAttrs, int[] outValues, int[] outIndices);


总之,即使LayoutInflater使用正确的扩展上下文,在扩展XML布局和创建视图时,方法Resources.getText()(关于自定义ContextWrapper返回的资源)是从未调用过来获取text属性的字符串,因为TextView的构造函数直接使用AssetManager来加载属性的资源。同样可能对其他视图和属性有效。