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.
- El archivo existe y el directorio IS, pero listFiles () devuelve null
- Java - ignorar certificado SSL expirado
- Cómo encriptar activos HTML + JS en Android Phonegap aplicación móvil?
- Cómo reducir la base de datos sqlite?
- Primeros intentos de crear el resultado del proyecto de aplicación en java.lang.NullPointerException error
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.
- Ubicación Actualización en función del estado del dispositivo
- La construcción de dos APK de un Android-Source-Tree
- Aplicación para Android Just After Boot
- ¿Cuál es la diferencia entre Android Private Libraries, Dependencies y la carpeta libs?
- ¿Cuál es la mejor práctica para compartir modelos entre un servidor API WEB y una aplicación cliente Android?
- ¿Cómo puedo reproducir varias alarmas mediante la notificación?
- Swizzling método en Android con java
- No se puede obtener el proveedor com.google.android.gms.measurement.AppMeasurementContentProvider
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.
- IsValidFragment Android API 19
- Problema al ordenar la pila de actividades al iniciar la aplicación desde el instalador de aplicaciones para Android y desde la pantalla de inicio