Pregunta Sobrescribir recursos en tiempo de ejecución


El problema

Me gustaría poder anular mis recursos de aplicaciones como R.colour.brand_colour o R.drawable.ic_action_start en tiempo de ejecución. Mi aplicación se conecta a un sistema CMS que proporcionará colores e imágenes de marca. Una vez que la aplicación ha descargado los datos de CMS, debe poder volver a protegerse.

Sé lo que vas a decir: no es posible anular recursos en tiempo de ejecución.

Excepto que es un poco. En particular, he encontrado esto Tesis de licenciatura de 2012 que explica el concepto básico - La clase de actividad en Android se extiende ContextWrapper, que contiene el método attachBaseContext. Puede reemplazar attachBaseContext para envolver el contexto con su propia clase personalizada que anula métodos como getColor y getDrawable. Su propia implementación de getColor podría ver el color como quisiera. los Biblioteca de caligrafía utiliza un enfoque similar para inyectar un LayoutInflator personalizado que puede ocuparse de cargar fuentes personalizadas.

El código

He creado una actividad simple que utiliza este enfoque para anular la carga de un color.

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

El problema es que no funciona! El registro muestra llamadas para cargar recursos como layout / activity_main y mipmap / ic_launcher, sin embargo color / theme_colour nunca se carga. Parece que el contexto se está utilizando para crear la ventana y la barra de acciones, pero no la vista de contenido de la actividad.

Mi pregunta es - ¿De dónde el inflador de diseño carga recursos, si no el contexto de las actividades?  También me gustaría saber - ¿Hay alguna manera factible de anular la carga de colores y objetos arrastrables en tiempo de ejecución?

Una palabra sobre enfoques alternativos

Sé que es posible tematizar una aplicación de los datos de CMS de otras maneras, por ejemplo, podríamos crear un método getCMSColour(String key) entonces dentro de nuestro onCreate() tenemos un montón de código en la línea de:

myTextView.setTextColour(getCMSColour("heading_text_colour"))

Un enfoque similar podría tomarse para los drawables, strings, etc. Sin embargo, esto daría como resultado una gran cantidad de código repetitivo, todo lo cual necesita mantenimiento. Al modificar la interfaz de usuario, sería fácil olvidarse de establecer el color en una vista particular.

Envolver el contexto para devolver nuestros propios valores personalizados es 'más limpio' y menos propenso a la rotura. Me gustaría entender por qué no funciona, antes de explorar enfoques alternativos.


49
2018-05-29 06:36


origen


Respuestas:


Si bien los "recursos que anulan dinámicamente" pueden parecer la solución directa a su problema, creo que un enfoque más limpio sería utilizar la implementación de enlace de datos oficial. https://developer.android.com/tools/data-binding/guide.html ya que no implica seco la forma de Android.

Puede pasar su configuración de marca usando un POJO. En lugar de usar estilos estáticos como @color/button_color podrías escribir @{brandingConfig.buttonColor} y une tus puntos de vista con los valores deseados. Con una jerarquía de actividad adecuada, no debería agregar demasiada repetición.

Esto también le da la capacidad de cambiar elementos más complejos en su diseño, es decir: incluye diferentes diseños en otro diseño dependiendo de la configuración de marca, haciendo que su UI sea altamente configurable sin demasiado esfuerzo.


8
2018-06-10 06:03



Después de buscar durante bastante tiempo, finalmente encontré una solución excelente.

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

Para una prueba de muestra,

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

Y dentro de la actividad principal,

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

Esta solución fue publicada originalmente por, Roman Zhilich 

ResourceHackActivity


5
2018-06-04 13:02



Teniendo básicamente el mismo problema que Luke Sleeman, eché un vistazo a cómo LayoutInflater está creando las vistas al analizar los archivos de diseño XML. Me centré en verificar por qué los recursos de cadena asignados al atributo de texto de TextViews dentro del diseño no son sobrescritos por mi Resources objeto devuelto por una costumbre ContextWrapper. Al mismo tiempo, las cadenas se sobrescriben como se esperaba al configurar el texto o la sugerencia mediante programación a través de TextView.setText() o TextView.setHint().

Así es como el texto se recibe como un CharSequence dentro del constructor de la TextView (sdk v 23.0.1):

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

dónde a es un TypedArray obtenido antes:

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

los Theme.obtainStyledAttributes() método llama a un método nativo en el 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);

...

Y esta es la declaración de la AssetManager.applyStyle() método:

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


En conclusión, aunque el LayoutInflater está utilizando el contexto extendido correcto, al inflar los diseños XML y crear las vistas, los métodos Resources.getText() (en los recursos devueltos por la costumbre ContextWrapper) nunca son llamados para obtener las cadenas para el atributo de texto, porque el constructor del TextView está usando el AssetManager directamente para cargar los recursos para los atributos. Lo mismo podría ser válido para otras vistas y atributos.


3
2017-11-13 13:11