¿Buenas prácticas para hacer frente al costoso cálculo de la altura de la vista?

Sigo corriendo en 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 sólo se aplica si la altura no está fijada por los parámetros de diseño). La captura es que para un ancho dado, es bastante costoso calcular la altura del contenido en estas vistas personalizadas. En particular, es demasiado caro para calcularse en el hilo de interfaz de usuario, por lo que en algún momento un hilo de trabajo debe ser encendido para calcular el diseño y cuando se termina, la interfaz de usuario debe actualizarse.

La pregunta es, ¿cómo se debe diseñar esto? He pensado en varias estrategias. Todos suponen que siempre 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 hilo de diseño, publica un Runnable en el subproceso de interfaz de usuario para establecer mLayoutHeight a la altura calculada y luego llamar a requestLayout() (e invalidate() ).

Una segunda estrategia es que onMeasure use siempre el valor actual para mLayoutHeight (sin disparar un hilo de diseño). Prueba de cambios en el ancho y encender un hilo de diseño se haría sobreescribiendo onSizeChanged .

Una tercera estrategia es ser perezoso y esperar a encender el hilo de diseño (si es necesario) en onDraw .

Me gustaría minimizar el número de veces que un subproceso de diseño se inicia y / o se mata, al tiempo que calcula la altura requerida lo antes posible. Probablemente sería bueno para minimizar el número de llamadas a requestLayout() también.

De los documentos, está claro que onMeasure puede ser llamado varias veces durante el curso de un solo diseño. Es menos claro (pero parece probable) que onSizeChanged también podría ser llamado 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 la vista personalizada, por lo que tengo un sesgo irracional en contra.

Otras personas deben haber enfrentado el mismo problema. ¿Hay acercamientos que he perdido? ¿Hay un mejor enfoque?

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 la cuestión central aquí es que su opinión no es realmente responsable de calcular su propia altura. Siempre es el padre de una visión que calcula las dimensiones de sus hijos. Ellos pueden expresar su "opinión", pero al final, al igual que en la vida real, realmente no tienen ninguna verdadera opinión en el asunto.

Eso sugeriría que echar un vistazo al padre de la opinión, o mejor dicho, al primer padre cuyas dimensiones son independientes de las dimensiones de sus hijos. Ese padre podría rechazar el diseño (y así dibujar) a sus hijos hasta que todos los niños hayan terminado su fase de medición (lo cual ocurre en un hilo separado). Tan pronto como lo tienen, el padre solicita una nueva fase de diseño y los diseños de sus hijos sin tener que medir de nuevo.

Es importante que las medidas de los niños no afecten la medición de dicho padre, de modo que pueda "absorber" la segunda fase de disposición sin tener que volver a medir a sus hijos, y así resolver el proceso de disposición.

Ampliando en esto un poco, puedo pensar en una solución bastante simple que sólo realmente tiene un inconveniente menor. Usted podría simplemente crear un AsyncView que extienda ViewGroup y, similar a ScrollView , sólo contiene siempre un solo niño que siempre llena todo su espacio. El AsyncView no considera la medida de su hijo para su propio tamaño, e idealmente sólo llena el espacio disponible. Todo lo que hace AsyncView envuelve la llamada de medida de su hijo en un hilo separado, que llama de vuelta a la vista tan pronto como se realiza la medición.

Dentro de esa vista, usted puede casi poner lo que quieras, incluyendo otros diseños. Realmente no importa cuán profunda sea la "visión problemática" en la jerarquía. El único inconveniente sería que ninguno de los descendientes sería traducido hasta que todos los descendientes hayan sido medidos. Pero probablemente querrías 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 de multithreading de ninguna manera. Se puede medir como cualquier otra vista, tomando tanto tiempo como sea necesario.

[Edit2] Incluso me molesté en hackear juntos una implementación rápida:

 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); } } 

Consulte https://github.com/wetblanket/AsyncView para obtener un ejemplo de trabajo

Un enfoque general para el cálculo caro es memoization – el almacenamiento en caché de los resultados de un cálculo con la esperanza de que esos resultados se pueden utilizar de nuevo. No sé cómo se aplica la memoización aquí, porque no sé si los mismos números de entrada pueden ocurrir varias veces.

También puedes hacer algo como esta estrategia:

Crear vista de niño personalizado:

 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 crear algún oyente personalizado como:

 public class MyOnResizeListener { public MyOnResizeListener(){} public void OnResize(int id, int xNew, int yNew, int xOld, int yOld){} } 

Usted instancia el 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 su oyente a su vista personalizada:

  /***Probably in your activity's onCreate***/ ((CustomChildView)findViewById(R.id.customChildView)).SetOnResizeListener(orlResized); 

Por último, puede agregar su CustomChildView a un diseño XML haciendo algo como:

  <com.demo.CustomChildView> <!-- Attributes --> <com.demo.CustomChildView/> 

Android no sabe el tamaño real al inicio, tiene que calcularlo. Una vez hecho esto, onSizeChanged() le notificará con el tamaño real.

onSizeChanged() se llama una vez que se ha calculado el tamaño. Los eventos no tienen que venir de los usuarios todo el tiempo. Cuando android cambie el tamaño, se llama onSizeChanged() . Y lo mismo con onDraw() , cuando la vista debe ser dibujada onDraw() se llama.

onMeasure() se llama automáticamente después de una llamada a la measure()

Otro enlace relacionado para ti

Por cierto, gracias por hacer una pregunta tan interesante aquí. Feliz de ayudar.:)

Existen widgets proporcionados por el sistema operativo, sistema de devolución de llamada / sistema de devolución de llamada proporcionado por el sistema operativo, gestión del diseño / restricción proporcionada por el sistema operativo, vinculación de datos proporcionada por el sistema operativo a los widgets. Alltogether lo llamo API de interfaz de usuario proporcionada por el sistema operativo. Independientemente de la calidad de la API de interfaz de usuario proporcionada por el SO, es posible que su aplicación a veces no se resuelva eficazmente utilizando sus características. Debido a que podría haber sido diseñado con diferentes ideas en mente. Así que siempre hay una buena opción para abstraer, y crear su propio – su API de interfaz de usuario orientada a tareas en la parte superior de la que el sistema operativo le proporciona. Su propio api proporcionado por usted mismo le permitiría calcular y almacenar y luego usar diferentes tamaños de widget de manera más eficiente. Reducir o eliminar los cuellos de botella, entrelazado, devoluciones de llamada, 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 el proyecto. La depuración y el soporte a lo largo de largos períodos de tiempo puede llevar algún esfuerzo, esta es la principal desventaja de este enfoque. Pero la ventaja es que tu mente establece cambios. Usted comienza a pensar "caliente para crear" en lugar de "cómo encontrar el camino alrededor de las limitaciones". No sé acerca de "las mejores prácticas", pero este es probablemente uno de los enfoques más utilizados. A menudo se puede escuchar "usamos los nuestros". No es fácil elegir entre los hechos por uno mismo y los grandes. Mantenerlo cuerdo es importante.

FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.