在运行时覆盖资源

问题

我希望能够在运行时覆盖我的应用程序资源,例如R.colour.brand_colour或R.drawable.ic_action_start。 我的应用程序连接到将提供品牌颜色和图像的CMS系统。 一旦应用程序已经下载CMS数据,它需要能够重新皮肤本身。

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

除了它是有点。 特别是我从2012年发现了这个学士论文 ,它解释了基本概念 – android中的Activity类扩展了ContextWrapper ,它包含了attachBaseContext方法。 你可以重载attachBaseContext来包装你自己的自定义类的Context,它覆盖getColor和getDrawable等方法。 你自己的getColor的实现可以看起来颜色,但它想要的。 书法库使用类似的方法来注入一个自定义的LayoutInflator,它可以处理加载自定义字体。

代码

我创build了一个简单的活动,它使用这种方法来重载加载的颜色。

 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永远不会加载。 似乎上下文被用来创build窗口和操作栏,而不是活动的内容视图。

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

关于替代方法的一个字

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

 myTextView.setTextColour(getCMSColour("heading_text_colour")) 

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

包装上下文返回我们自己的自定义值是“更清洁”,不太容易破损。 我想了解为什么它不起作用,然后再探索其他方法。

虽然“dynamic覆盖资源”似乎是解决您的问题的直接方法,但我认为更清晰的方法是使用官方的数据绑定实现https://developer.android.com/tools/data-binding/guide.html,因为它并不意味着;黑客机器人的方式。

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

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

经过了相当长的时间,我终于find了一个很好的解决scheme。

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

对于一个样本testing,

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

该解决scheme最初由Roman Zhilich发布

ResourceHackActivity

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

这是如何在TextView (sdk v 23.0.1)的构造函数中作为CharSequence接收文本:

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

其中aTypedArray获得的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布局和创build视图时,方法Resources.getText() (在自定义ContextWrapper返回的资源上)永远不会调用以获取stringtext属性,因为TextView的构造函数直接使用AssetManager为属性加载资源。 其他视图和属性也可能是有效的。