Sustitución de 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 necesita ser capaz de re-skin mismo.

Sé lo que está a punto de decir: no es posible sobrecargar recursos en tiempo de ejecución.

Excepto que es un poco. En particular he encontrado esta tesis de licenciatura a partir de 2012 que explica el concepto básico – la clase de actividad en android extends ContextWrapper , que contiene el método attachBaseContext. Puede reemplazar attachBaseContext para incluir el Context con su propia clase personalizada que reemplaza métodos como getColor y getDrawable. Su propia implementación de getColor podría ver el color como quiera. La biblioteca de Caligrafía utiliza un enfoque similar para inyectar un LayoutInflator personalizado que puede hacer frente a la carga de fuentes personalizadas.

El código

He creado una actividad simple que utiliza este acercamiento 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 acción, pero no la vista de contenido de la actividad.

Mis preguntas es – ¿Dónde el inflador de diseño carga los recursos, si no el contexto de las actividades? También me gustaría saber – ¿Existe una manera viable de anular la carga de colores y drawables en tiempo de ejecución?

Una palabra sobre enfoques alternativos

Sé que su posible tema de una aplicación de CMS datos de otras formas – por ejemplo, podríamos crear un método getCMSColour(String key) entonces dentro de nuestro onCreate() tenemos un montón de código a lo largo de las líneas de:

 myTextView.setTextColour(getCMSColour("heading_text_colour")) 

Un acercamiento similar podría ser tomado para los estirables, las secuencias, el etc. Sin embargo esto daría lugar a una gran cantidad de código de boilerplate – todo de que necesita el mantener. Al modificar la interfaz de usuario sería fácil olvidarse de establecer el color en una vista en 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.

Mientras que "los recursos sobrecargando dinámicamente" puede parecer la solución directa a su problema, creo que un enfoque más limpio sería usar la aplicación oficial de vinculación de datos https://developer.android.com/tools/data-binding/guide.html ya que No implica hackear la forma androide.

Puede pasar su configuración de marca con un POJO. En lugar de usar estilos estáticos como @color/button_color puedes escribir @{brandingConfig.buttonColor} y vincular tus vistas con los valores deseados. Con una jerarquía de actividad adecuada, no debe agregar demasiado cliché.

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

Después de buscar un tiempo bastante largo 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

Teniendo básicamente el mismo tema que Luke Sleeman, he LayoutInflater un vistazo a cómo el LayoutInflater está creando las vistas al analizar los archivos de diseño XML. Me concentré en comprobar por qué los recursos de cadena asignados al atributo de texto de TextView s dentro del diseño no se sobrescriben por mi objeto Resources devuelto por un ContextWrapper personalizado. Al mismo tiempo, las cadenas se sobrescriben como se espera cuando se establece el texto o sugerencia mediante programación TextView.setText() o TextView.setHint() .

Éste es cómo el texto se recibe como CharSequence dentro del constructor del TextView (sdk v 23.0.1):

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

Donde a es un TypedArray obtenido anteriormente:

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

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

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 el ContextWrapper personalizado) nunca se llaman para obtener las cadenas para el , Porque el constructor de TextView está utilizando AssetManager directamente para cargar los recursos para los atributos. Lo mismo podría ser válido para otras vistas y atributos.

  • Cambiar android: Theme.Dialog para encender el cuadro de diálogo de AppCompat
  • ¿Cómo leer un archivo de texto del directorio "assets" como una cadena?
  • Utilizar la biblioteca de Zxing en PreviewFrame para aumentar la realidad
  • Problema de compilación en Android Studio con Google Play Services
  • No se puede reproducir un archivo .wav invertido con MediaPlayer
  • Android analiza la serie Json de cadenas
  • ¿Cómo mejorar el tiempo de inicio de una aplicación para Android?
  • Envío de mayúsculas a un TextEdit durante pruebas instrumentadas
  • El inicio de sesión de Twitter de Android no funciona con Fabric SDK - La devolución de llamada no debe ser nula
  • Env-> FindClass función devuelve null
  • ¿Cómo funcionan los anuncios en los teléfonos móviles?
  • FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.