Explicación de la vista personalizada de onMeasure

316

Traté de hacer un componente personalizado. Extendí la Viewclase y hago algunos dibujos en un onDrawmétodo anulado. ¿Por qué necesito anular onMeasure? Si no lo hiciera, todo se vería como correcto. ¿Alguien puede explicarlo? ¿Cómo debo escribir mi onMeasuremétodo? He visto un par de tutoriales, pero cada uno es un poco diferente al otro. A veces llaman super.onMeasureal final, a veces usan setMeasuredDimensiony no lo llaman. ¿Dónde está la diferencia?

Después de todo, quiero usar varios componentes exactamente iguales. Agregué esos componentes a mi XMLarchivo, pero no sé qué tan grandes deberían ser. Quiero establecer su posición y tamaño más tarde (por qué necesito establecer el tamaño en onMeasureif onDrawcuando lo dibujo, también funciona) en la clase de componente personalizado. ¿Cuándo exactamente necesito hacer eso?

sennin
fuente

Respuestas:

735

onMeasure()es su oportunidad de decirle a Android qué tan grande quiere que su vista personalizada dependa de las restricciones de diseño proporcionadas por el padre; también es la oportunidad de su vista personalizada para aprender cuáles son esas restricciones de diseño (en caso de que desee comportarse de manera diferente en una match_parentsituación que en una wrap_contentsituación). Estas restricciones se agrupan en los MeasureSpecvalores que se pasan al método. Aquí hay una correlación aproximada de los valores de modo:

  • EXACTAMENTE significa que el valor layout_widtho layout_heightse estableció en un valor específico. Probablemente debería hacer su vista de este tamaño. Esto también se puede activar cuando match_parentse usa, para establecer el tamaño exactamente en la vista principal (esto depende del diseño en el marco).
  • AT_MOST generalmente significa que el valor layout_widtho layout_heightse estableció en match_parento wrap_contentdonde se necesita un tamaño máximo (esto depende del diseño en el marco), y el tamaño de la dimensión principal es el valor. No debe ser más grande que este tamaño.
  • NO ESPECIFICADO normalmente significa que el valor layout_widtho layout_heightse estableció wrap_contentsin restricciones. Puedes ser del tamaño que quieras. Algunos diseños también usan esta devolución de llamada para determinar el tamaño deseado antes de determinar qué especificaciones realmente le pasarán nuevamente en una solicitud de segunda medida.

El contrato que existe onMeasure()es que setMeasuredDimension() DEBE llamarse al final con el tamaño que le gustaría que fuera la vista. Todas las implementaciones de framework llaman a este método, incluida la implementación predeterminada que se encuentra en View, por lo que es seguro llamar superen su lugar si se ajusta a su caso de uso.

De acuerdo, dado que el marco aplica una implementación predeterminada, puede que no sea necesario que anule este método, pero puede ver recorte en los casos en que el espacio de visualización sea más pequeño que su contenido si no lo hace, y si diseña su vista personalizada con wrap_contenten ambas direcciones, es posible que su vista no se muestre porque el marco no sabe qué tan grande es.

En general, si está anulando Viewy no otro widget existente, probablemente sea una buena idea proporcionar una implementación, incluso si es tan simple como algo como esto:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    int desiredWidth = 100;
    int desiredHeight = 100;

    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    int width;
    int height;

    //Measure Width
    if (widthMode == MeasureSpec.EXACTLY) {
        //Must be this size
        width = widthSize;
    } else if (widthMode == MeasureSpec.AT_MOST) {
        //Can't be bigger than...
        width = Math.min(desiredWidth, widthSize);
    } else {
        //Be whatever you want
        width = desiredWidth;
    }

    //Measure Height
    if (heightMode == MeasureSpec.EXACTLY) {
        //Must be this size
        height = heightSize;
    } else if (heightMode == MeasureSpec.AT_MOST) {
        //Can't be bigger than...
        height = Math.min(desiredHeight, heightSize);
    } else {
        //Be whatever you want
        height = desiredHeight;
    }

    //MUST CALL THIS
    setMeasuredDimension(width, height);
}

Espero que ayude.

Devunwired
fuente
1
Hola @Devunwired buena explicación lo mejor que he leído hasta ahora. Su explicación respondió muchas preguntas que tenía y despejó algunas dudas, pero aún queda una que es: Si mi vista personalizada está dentro de un ViewGroup junto con algunas otras Vistas (no importa qué tipos) ese ViewGroup obtendrá a todos sus hijos un para cada una de ellas para determinar su restricción LayoutParams y pedirle a cada niño que se mida de acuerdo con sus restricciones?
faraón
47
Tenga en cuenta que este código no funcionará si anula onMeasure de cualquier subclase de ViewGroup. Sus subvistas no se mostrarán y todas tendrán un tamaño de 0x0. Si necesita anular onMeasure de un ViewGroup personalizado, cambiar widthMode, widthSize, heightMode y heightSize, compílelos de nuevo a measureSpecs usando MeasureSpec.makeMeasureSpec y pase los enteros resultantes a super.onMeasure.
Alexey
1
Fantástica respuesta. Tenga en cuenta que, según la documentación de Google, es responsabilidad de la Vista manejar el relleno.
jonstaff
44
Sobre c ** p complicado que hace que Android sea un sistema de diseño doloroso para trabajar. Podrían haber tenido getParent (). Get *** () ...
Oliver Dixon
2
Hay métodos auxiliares en la Viewclase, llamados resolveSizeAndStatey resolveSize, que deberían hacer lo que hacen las cláusulas 'if': los encuentro útiles, especialmente si tiene que escribir esos IF a menudo.
stan0
5

en realidad, su respuesta no está completa ya que los valores también dependen del contenedor de envoltura. En el caso de diseños relativos o lineales, los valores se comportan así:

  • EXACTAMENTE match_parent es EXACTAMENTE + tamaño del padre
  • AT_MOST wrap_content produce una AT_MOST MeasureSpec
  • NO ESPECIFICADO nunca activado

En el caso de una vista de desplazamiento horizontal, su código funcionará.

Florian
fuente
57
Si cree que alguna respuesta aquí está incompleta, agréguela en lugar de dar una respuesta parcial.
Michaël
1
Es bueno para vincular esto a cómo funcionan los diseños, pero en mi caso onMeasure se llama tres veces para mi vista personalizada. La vista en cuestión tenía una altura wrap_content y un ancho ponderado (ancho = 0, peso = 1). La primera llamada había NO ESPECIFICADO / NO ESPECIFICADO, la segunda tenía AT_MOST / EXACTLY y la tercera tenía EXACTLY / EXACTLY.
William T. Mallard
0

Si no necesita cambiar algo en Medición, no es absolutamente necesario que lo anule.

El código Devunwired (la respuesta seleccionada y más votada aquí) es casi idéntico a lo que la implementación del SDK ya hace por usted (y lo comprobé, lo había hecho desde 2009).

Puede consultar el método onMeasure aquí :

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

Reemplazar el código SDK para ser reemplazado por el mismo código no tiene sentido.

La pieza de este documento oficial que afirma que "el valor predeterminado de onMeasure () siempre establecerá un tamaño de 100x100" está mal.

David Refaeli
fuente