我希望能够在运行时覆盖我的应用资源,例如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时,很容易忘记在特定视图上设置颜色。
包装上下文以返回我们自己的自定义值是更清洁'并且不太容易破损。在探索替代方法之前,我想了解它为什么不起作用。
答案 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
发布答案 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
来加载属性的资源。同样可能对其他视图和属性有效。