java - para - manual de programacion android pdf
Anulación de recursos en tiempo de ejecución (3)
El problema
Me gustaría poder anular los recursos de mis 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 del CMS, debe poder volver a pelarse.
Sé lo que está a punto de decir: anular recursos en tiempo de ejecución no es posible.
Excepto que es un poco.
En particular, he encontrado esta
tesis de licenciatura
de 2012 que explica el concepto básico: la clase Activity en Android extiende
ContextWrapper
, que contiene el método attachBaseContext.
Puede anular attachBaseContext para ajustar el contexto con su propia clase personalizada que anula métodos como getColor y getDrawable.
Su propia implementación de getColor podría buscar el color como quisiera.
La
biblioteca de caligrafía
utiliza un enfoque similar para inyectar un LayoutInflator personalizado que puede manejar la carga de 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 acción, pero no la vista de contenido de la actividad.
Mi pregunta es: ¿ De dónde carga el inflador de diseño los recursos, si no el contexto de las actividades? También me gustaría saber: ¿hay alguna forma viable de anular la carga de colores y dibujos en tiempo de ejecución?
Una palabra sobre enfoques alternativos
Sé que es posible crear un tema para una aplicación a partir de datos de CMS de otras maneras; por ejemplo, podríamos crear un método
getCMSColour(String key)
luego dentro de nuestro
onCreate()
tenemos un montón de código en la línea de:
myTextView.setTextColour(getCMSColour("heading_text_colour"))
Se podría adoptar un enfoque similar para los elementos dibujables, las cadenas, 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 en particular.
Ajustar 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.
Al tener 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 concentré en verificar por qué los recursos de cadena asignados al atributo de texto de
TextView
s dentro del diseño no son sobrescritos por mi objeto de
Resources
devuelto por un
ContextWrapper
personalizado.
Al mismo tiempo, las cadenas se sobrescriben como se esperaba al configurar el texto o la sugerencia mediante programación mediante
TextView.setText()
o
TextView.setHint()
.
Así es como se recibe el texto como
CharSequence
dentro del constructor de
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 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 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
LayoutInflater
utiliza 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 atributo de texto, porque el constructor de
TextView
está utilizando el
AssetManager
directamente para cargar los recursos para los atributos.
Lo mismo podría ser válido para otras vistas y atributos.
Después de buscar durante mucho tiempo, finalmente encontré una excelente solución.
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
Si bien "anular recursos dinámicamente" puede parecer la solución directa a su problema, creo que un enfoque más limpio sería utilizar la implementación oficial de enlace de datos https://developer.android.com/tools/data-binding/guide.html ya que no implica hackear la forma de Android.
Puede pasar su configuración de marca utilizando un POJO.
En lugar de usar estilos estáticos como
@color/button_color
, podría escribir
@{brandingConfig.buttonColor}
y vincular sus vistas con los valores deseados.
Con una jerarquía de actividad adecuada, no debería agregar demasiado repetitivo.
Esto también le brinda la capacidad de cambiar elementos más complejos en su diseño, es decir: incluir diferentes diseños en otro diseño dependiendo de la configuración de la marca, haciendo que su IU sea altamente configurable sin demasiado esfuerzo.