在运行时覆盖资源
Posted
技术标签:
【中文标题】在运行时覆盖资源【英文标题】:Overriding resources at runtime 【发布时间】:2015-08-11 21:56:08 【问题描述】:问题
我希望能够在运行时覆盖我的应用资源,例如 R.colour.brand_colour 或 R.drawable.ic_action_start。我的应用程序连接到将提供品牌颜色和图像的 CMS 系统。一旦应用下载了 CMS 数据,它就需要能够重新换肤。
我知道你要说什么 - 是不可能的。
除了它有点像。特别是我从 2012 年发现了这个 Bachelor Thesis,它解释了基本概念 - android 中的 Activity 类扩展了 ContextWrapper
,其中包含 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 从未加载。似乎上下文被用于创建窗口和操作栏,而不是活动的内容视图。
我的问题是 - 布局充气器从哪里加载资源,如果不是活动上下文?我也想知道 - 是否有一种可行的方法来覆盖颜色的加载和运行时的可绘制对象?
关于替代方法的一句话
我知道可以通过其他方式从 CMS 数据中为应用设置主题 - 例如,我们可以创建一个方法 getCMSColour(String key)
,然后在我们的 onCreate()
中,我们有一堆代码如下:
myTextView.setTextColour(getCMSColour("heading_text_colour"))
类似的方法可以用于可绘制对象、字符串等。但这会导致大量样板代码——所有这些都需要维护。在修改 UI 时,很容易忘记在特定视图上设置颜色。
包装 Context 以返回我们自己的自定义值更“干净”且不易损坏。在探索替代方法之前,我想了解它为什么不起作用。
【问题讨论】:
您的解决方案有效:在活动中,如果您调用 getResources().getColor(R.color.theme_colour) 结果是 Color.GREEN 正如预期的那样。充气机似乎使用了另一种方法来检索颜色,我不知道是哪一种。我尝试包装应用程序上下文,但结果相同... 是的,我知道调用 getResource().getColour() 将返回绿色。但是我的问题是,当布局膨胀时,为什么我设置的控件不是 android:colour="@color/theme_colour" green! 不是您问题的答案(实际上,如果可能的话,我会非常感兴趣),但作为另一种替代方法,您可以自己覆盖使用的小部件(TextView、ImageView 等)您自己的“资源提供者”实现(您已在“替代方法”段落中添加并在您的视图中使用它。这样,您可以减少样板代码的数量,并且更容易维护主题。至少,如果所有其他方法都失败了,我个人会采用这种方法,而不是覆盖每个活动/片段中的主题和资源。 @kha 在这里有一个合理的方法。除此之外,没有一种可靠的方法可以用任意数据动态替换从 XML 引用的值。任何涉及反射的方法都会破坏您的应用程序(我们看到很多这种情况是由于 M 中的资源框架更改)。 啊,我明白了。我为我之前的困惑道歉。在一个理想的世界里,你的方法会奏效。在一个理想的世界里,我会有头发。这不是一个理想的世界,更可惜的是。 【参考方案1】:虽然“动态覆盖资源”似乎是解决问题的直接方法,但我认为更简洁的方法是使用官方数据绑定实现 https://developer.android.com/tools/data-binding/guide.html,因为它并不意味着黑客攻击安卓方式。
您可以使用 POJO 传递您的品牌设置。您可以编写 @brandingConfig.buttonColor
并将您的视图与所需的值绑定,而不是使用像 @color/button_color
这样的静态样式。有了适当的活动层次结构,它不应该添加太多的样板。
这还使您能够更改布局上更复杂的元素,即:根据品牌设置在其他布局上包含不同的布局,使您的 UI 高度可配置,无需太多努力。
【讨论】:
我刚刚试用了数据绑定库,它似乎可以很好地满足我们的需要。唯一缺少的就是数据绑定品牌配置的漂亮设计预览——例如,执行android:textColor="@brandingConfig.buttonColor"
之类的操作不会在android studio 中的视觉布局预览中产生任何有趣的结果。这总是可以通过添加 tools:
前缀属性来设置预览的字符串、可绘制对象等内容来解决
经过深思熟虑,我将其标记为答案。您还没有回答“布局充气器从哪里加载资源,如果不是活动上下文?”这个问题。但是,您已经成功回答了“是否有一种可行的方法可以在运行时覆盖颜色和可绘制对象的加载?”这个问题?系统非常接近我想要实现的目标。此外,感谢您意识到问题是数据绑定之一。本质上,我试图做的是用最少的样板从 java 中获取许多值到 xml 布局中。【参考方案2】:
与 Luke Sleeman 基本相同,我查看了 LayoutInflater
在解析 XML 布局文件时如何创建视图。我专注于检查为什么分配给布局内TextView
s 的文本属性的字符串资源不会被自定义ContextWrapper
返回的Resources
对象覆盖。同时,通过TextView.setText()
或TextView.setHint()
以编程方式设置文本或提示时,字符串会按预期覆盖。
这就是在TextView
(sdk v 23.0.1) 的构造函数中以CharSequence
接收文本的方式:
// 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
返回的资源上)永远不会被调用来获取字符串对于文本属性,因为TextView
的构造函数直接使用AssetManager
来加载属性的资源。这可能对其他视图和属性有效。
【讨论】:
啊——谢谢你解决了最后一个难题——布局充气机从哪里加载资源!不幸的是,包装 Context 以实现动态主题的方法永远行不通。 这似乎是原生的 applyStyle 实现:github.com/aosp-mirror/platform_frameworks_base/blob/…【参考方案3】:找了很久,终于找到了一个很好的解决方案。
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);
在 Main 活动中,
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
【讨论】:
确实可以使用反射使 R 对象上的字段指向新资源 - 问题是新资源也需要在 XML 中静态定义!我们对来自 CMS 的动态颜色、字符串、可绘制对象等感兴趣。因此,使用您的解决方案,您可以轻松制作 R.string。初始值 = R.string。 evil_value,但您不能从其他地方注入动态字符串。以上是关于在运行时覆盖资源的主要内容,如果未能解决你的问题,请参考以下文章
手机和电脑的后台程序是否与前台程序同时运行 只是被前台覆盖了