在运行时覆盖资源

Luk*_*man 53 java android android-activity

问题

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

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

除了它有点.特别是我从2012年开始发现了这个学士论文,它解释了基本概念 - android扩展中的Activity类ContextWrapper,它包含了attachBaseContext方法.您可以覆盖attachBaseContext以使用您自己的自定义类包装Context,该自定义类将覆盖getColor和getDrawable等方法.你自己的getColor实现可以看起来像它想要的颜色.该书法库使用类似的方法注入定制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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

关于替代方法的一个词

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

myTextView.setTextColour(getCMSColour("heading_text_colour"))
Run Code Online (Sandbox Code Playgroud)

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

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

Log*_*ain 9

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

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

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


Vic*_*r B 8

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

这就是CharSequenceTextView(sdk v 23.0.1)的构造函数中接收文本的方式:

// android.widget.TextView.java, line 973
text = a.getText(attr);
Run Code Online (Sandbox Code Playgroud)

哪里aTypedArray更早获得的:

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

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);

...
Run Code Online (Sandbox Code Playgroud)

这是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);
Run Code Online (Sandbox Code Playgroud)


总之,即使LayoutInflater使用了正确的扩展上下文,在扩展 XML 布局和创建视图时,也永远不会调用方法Resources.getText()(在 custom 返回的资源上ContextWrapper)来获取 text 属性的字符串,因为的TextView是使用AssetManager直接加载资源的属性。这对于其他视图和属性可能同样有效。


Pro*_*kar 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);
        }
    }
Run Code Online (Sandbox Code Playgroud)

对于样本测试,

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

在主要活动中,

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()));
Run Code Online (Sandbox Code Playgroud)

该解决方案最初由Roman Zhilich发布

ResourceHackActivity