android - ¿Mejores prácticas para lidiar con costosos cálculos de altura de vista?
layout custom-view (5)
Sigo encontrando un problema de tamaño y diseño para las vistas personalizadas y me pregunto si alguien puede sugerir un enfoque de "mejores prácticas". El problema es el siguiente. Imagine una vista personalizada donde la altura requerida para el contenido depende del ancho de la vista (similar a un TextView de varias líneas). (Obviamente, esto solo se aplica si la altura no está fijada por los parámetros de diseño). El problema es que para un ancho dado, es bastante costoso calcular la altura del contenido en estas vistas personalizadas. En particular, es demasiado costoso para ser computado en el subproceso de la interfaz de usuario, por lo que en algún momento se debe activar un subproceso de trabajo para calcular el diseño y, cuando finalice, la interfaz de usuario debe actualizarse.
La pregunta es, ¿cómo se debe diseñar esto? He pensado en varias estrategias. Todos asumen que cada vez que se calcula la altura, se registra el ancho correspondiente.
La primera estrategia se muestra en este código:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = measureWidth(widthMeasureSpec);
setMeasuredDimension(width, measureHeight(heightMeasureSpec, width));
}
private int measureWidth(int widthMeasureSpec) {
// irrelevant to this problem
}
private int measureHeight(int heightMeasureSpec, int width) {
int result;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
if (width != mLastWidth) {
interruptAnyExistingLayoutThread();
mLastWidth = width;
mLayoutHeight = DEFAULT_HEIGHT;
startNewLayoutThread();
}
result = mLayoutHeight;
if (specMode == MeasureSpec.AT_MOST && result > specSize) {
result = specSize;
}
}
return result;
}
Cuando finaliza el subproceso de diseño, publica un Runnable en el subproceso de la interfaz de usuario para establecer mLayoutHeight
en la altura calculada y luego llamar a requestLayout()
(e invalidate()
).
Una segunda estrategia es tener onMeasure
siempre use el valor actual de corriente para mLayoutHeight
(sin mLayoutHeight
un hilo de diseño). Las pruebas de cambios de ancho y la activación de un hilo de diseño se realizarían anulando onSizeChanged
.
Una tercera estrategia es ser perezoso y esperar para iniciar el subproceso de diseño (si es necesario) en onDraw
.
Me gustaría minimizar el número de veces que un subproceso de diseño se lanza y / o se elimina, al mismo tiempo que se calcula la altura requerida lo antes posible. Probablemente también sería bueno minimizar el número de llamadas a requestLayout()
.
De los documentos, queda claro que onMeasure
puede llamarse varias veces durante el curso de un solo diseño. Es menos claro (pero parece probable) que también se puede llamar a onSizeChanged
varias veces. Así que estoy pensando que poner la lógica en onDraw
podría ser la mejor estrategia. Pero eso parece contrario al espíritu del tamaño de vista personalizado, por lo que tengo un sesgo ciertamente irracional contra él.
Otras personas deben haber enfrentado este mismo problema. ¿Hay enfoques que he perdido? ¿Hay un mejor enfoque?
Android no sabe el tamaño real al principio, necesita calcularlo. Una vez hecho esto, onSizeChanged()
le notificará con el tamaño real.
onSizeChanged()
se llama una vez que el tamaño se ha calculado. Los eventos no tienen que venir de los usuarios todo el tiempo. Cuando Android cambia el tamaño, se llama a onSizeChanged()
. Y lo mismo con onDraw()
, cuando se debe dibujar la vista, se llama a onDraw()
.
onMeasure()
se llama automáticamente justo después de una llamada a measure()
Algún otro enlace relacionado para ti
Por cierto, gracias por hacer una pregunta tan interesante aquí. Feliz de ayudar.:)
Creo que el sistema de diseño en Android no fue realmente diseñado para resolver un problema como este, lo que probablemente sugeriría cambiar el problema.
Dicho esto, creo que el problema central aquí es que su opinión no es realmente responsable de calcular su propia altura. Siempre es el padre de una vista que calcula las dimensiones de sus hijos. Pueden expresar su "opinión", pero al final, al igual que en la vida real, realmente no tienen nada que decir al respecto.
Eso sugeriría echar un vistazo al padre de la vista, o más bien, al primer padre cuyas dimensiones son independientes de las dimensiones de sus hijos. Ese padre podría rechazar el diseño (y, por lo tanto, dibujar) a sus hijos hasta que todos los niños hayan terminado su fase de medición (lo que ocurre en un hilo separado). Tan pronto como lo han hecho, el padre solicita una nueva fase de diseño y los diseños de sus hijos sin tener que medirlos nuevamente.
Es importante que las mediciones de los niños no afecten la medición de dicho padre, de modo que pueda "absorber" la segunda fase de diseño sin tener que volver a medir a sus hijos, y así resolver el proceso de diseño.
[edit] Ampliando esto un poco, puedo pensar en una solución bastante simple que solo tiene un inconveniente menor. Simplemente puede crear un AsyncView
que extienda ViewGroup
y, de manera similar a ScrollView
, solo contiene un solo elemento secundario que siempre llene todo su espacio. El AsyncView
no considera la medida de su hijo por su propio tamaño, y lo ideal es que solo llene el espacio disponible. Todo lo que AsyncView
hace es envolver la llamada de medición de su hijo en un hilo separado, que vuelve a la vista tan pronto como se realiza la medición.
Dentro de esa vista, puedes poner prácticamente lo que quieras, incluyendo otros diseños. En realidad, no importa la profundidad de la "vista problemática" en la jerarquía. El único inconveniente sería que ninguno de los descendientes se representaría hasta que todos los descendientes hayan sido medidos. Pero probablemente querrá mostrar algún tipo de animación de carga hasta que la vista esté lista de todos modos.
La "visión problemática" no tendría que preocuparse por el multihilo de ninguna manera. Puede medirse a sí mismo como cualquier otra vista, tomando todo el tiempo que necesite.
[edit2] Incluso me molesté en hackear una rápida implementación:
package com.example.asyncview;
import android.content.Context;
import android.os.AsyncTask;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
public class AsyncView extends ViewGroup {
private AsyncTask<Void, Void, Void> mMeasureTask;
private boolean mMeasured = false;
public AsyncView(Context context) {
super(context);
}
public AsyncView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
for(int i=0; i < getChildCount(); i++) {
View child = getChildAt(i);
child.layout(0, 0, child.getMeasuredWidth(), getMeasuredHeight());
}
}
@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if(mMeasured)
return;
if(mMeasureTask == null) {
mMeasureTask = new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... objects) {
for(int i=0; i < getChildCount(); i++) {
measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec);
}
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
mMeasured = true;
mMeasureTask = null;
requestLayout();
}
};
mMeasureTask.execute();
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
if(mMeasureTask != null) {
mMeasureTask.cancel(true);
mMeasureTask = null;
}
mMeasured = false;
super.onSizeChanged(w, h, oldw, oldh);
}
}
Vea https://github.com/wetblanket/AsyncView para un ejemplo de trabajo
Hay widgets proporcionados por el sistema operativo, sistema de devolución de llamadas / eventos proporcionado por el sistema operativo, diseño / gestión de restricciones provisto por el sistema operativo, enlace de datos proporcionado por el sistema operativo a los widgets. En conjunto lo llamo API de interfaz de usuario proporcionada por el sistema operativo. Independientemente de la calidad de la API de UI provista por el SO, su aplicación a veces puede no resolverse de manera efectiva usando sus características. Porque podrían haber sido diseñados con diferentes ideas en mente. Por lo tanto, siempre hay una buena opción para abstraer y crear su propia API de UI orientada a tareas, además de la que OS le proporciona. Su propia API provista le permitirá calcular y almacenar y luego usar diferentes tamaños de widgets de una manera más eficiente. Reducir o eliminar cuellos de botella, inter-threading, callbacks, etc. A veces, crear tal api de capa es cuestión de minutos, o a veces puede ser lo suficientemente avanzado como para ser la parte más grande de todo su proyecto. La depuración y el soporte durante largos períodos de tiempo pueden requerir cierto esfuerzo, esto es la principal desventaja de este enfoque. Pero la ventaja es que tu mentalidad cambia. Empieza a pensar "en caliente para crear" en lugar de "cómo encontrar la forma de evitar las limitaciones". No conozco las "mejores prácticas" pero este es probablemente uno de los enfoques más utilizados. A menudo se puede escuchar "usamos el nuestro". Elegir entre los hombres grandes y los hechos a sí mismos no es fácil. Mantenerlo sano es importante.
También puedes hacer algo como esta estrategia:
Crear vista personalizada para niños:
public class CustomChildView extends View
{
MyOnResizeListener orl = null;
public CustomChildView(Context context)
{
super(context);
}
public CustomChildView(Context context, AttributeSet attrs)
{
super(context, attrs);
}
public CustomChildView(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
}
public void SetOnResizeListener(MyOnResizeListener myOnResizeListener )
{
orl = myOnResizeListener;
}
@Override
protected void onSizeChanged(int xNew, int yNew, int xOld, int yOld)
{
super.onSizeChanged(xNew, yNew, xOld, yOld);
if(orl != null)
{
orl.OnResize(this.getId(), xNew, yNew, xOld, yOld);
}
}
}
Y crea un oyente personalizado como:
public class MyOnResizeListener
{
public MyOnResizeListener(){}
public void OnResize(int id, int xNew, int yNew, int xOld, int yOld){}
}
Se crea una instancia del oyente como:
Class MyActivity extends Activity
{
/***Stuff***/
MyOnResizeListener orlResized = new MyOnResizeListener()
{
@Override
public void OnResize(int id, int xNew, int yNew, int xOld, int yOld)
{
/***Handle resize event and call your measureHeight(int heightMeasureSpec, int width) method here****/
}
};
}
Y no olvide pasar a su oyente a su vista personalizada:
/***Probably in your activity''s onCreate***/
((CustomChildView)findViewById(R.id.customChildView)).SetOnResizeListener(orlResized);
Finalmente, puede agregar su CustomChildView a un diseño XML haciendo algo como:
<com.demo.CustomChildView>
<!-- Attributes -->
<com.demo.CustomChildView/>
Un enfoque general del cálculo costoso es la memoization : almacenar en caché los resultados de un cálculo con la esperanza de que esos resultados puedan ser utilizados nuevamente. No sé qué tan bien se aplica la memoización aquí, porque no sé si los mismos números de entrada pueden ocurrir varias veces.