studio programacion para móviles libro edición desarrollo curso con aplicaciones java android android-activity

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

ResourceHackActivity


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.