在运行时覆盖资源

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 布局文件时如何创建视图。我专注于检查为什么分配给布局内TextViews 的文本属性的字符串资源不会被自定义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,但您不能从其他地方注入动态字符串。

以上是关于在运行时覆盖资源的主要内容,如果未能解决你的问题,请参考以下文章

手机和电脑的后台程序是否与前台程序同时运行 只是被前台覆盖了

在运行时覆盖 NativeQuery

在运行时覆盖 angularjs 指令

在单个 AppDomain 上运行多个应用程序实例时如何防止属性覆盖?

运行 jar 时覆盖属性文件

MEF 和 ShadowCopying DLL,以便我可以在运行时覆盖它们