Google se compromete a impulsar la igualdad racial para las comunidades afrodescendientes. Obtén información al respecto.

Diseño personalizado

La parte más importante de una vista personalizada es su apariencia. El diseño personalizado puede ser fácil o complejo según las necesidades de tu aplicación. En esta lección, se abarcan algunas de las operaciones más comunes.

Fuera de esta lección, encontrarás más información relacionada en Lienzo y elementos de diseño.

Cómo anular onDraw()

El paso más importante al diseñar una vista personalizada es anular el método onDraw(). El parámetro de onDraw() es un objeto Canvas que la vista puede usar para diseñar. La clase Canvas define métodos para diseñar texto, líneas, mapas de bits y muchas otras primitivas gráficas. Puedes usar estos métodos en onDraw() para crear tu interfaz de usuario (IU) personalizada.

Sin embargo, antes de llamar a cualquier método de diseño, es necesario crear un objeto Paint. En la siguiente sección, se analiza Paint con más detalle.

Cómo crear objetos de diseño

El marco de trabajo android.graphics divide el diseño en dos áreas:

  • Qué diseñar, administrado por Canvas
  • Cómo diseñar, manejado por Paint

Por ejemplo, Canvas proporciona un método para dibujar una línea, mientras que Paint proporciona métodos para definir el color de esa línea. Canvas tiene un método para dibujar un rectángulo, mientras que Paint define si se debe rellenar ese rectángulo con un color o dejarlo vacío. En pocas palabras, Canvas define las formas que puedes diseñar en la pantalla, mientras que Paint define el color, el estilo, la fuente y demás aspectos de cada forma que diseñas.

Por lo tanto, antes de empezar a diseñar, debes crear uno o más objetos Paint. El ejemplo de PieChart hace esto en un método denominado init, que se llama en el constructor desde Java, pero que se puede inicializar en línea en Kotlin:

Kotlin

    private val textPaint = Paint(ANTI_ALIAS_FLAG).apply {
        color = textColor
        if (textHeight == 0f) {
            textHeight = textSize
        } else {
            textSize = textHeight
        }
    }

    private val piePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        style = Paint.Style.FILL
        textSize = textHeight
    }

    private val shadowPaint = Paint(0).apply {
        color = 0x101010
        maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.NORMAL)
    }
    

Java

    private void init() {
       textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
       textPaint.setColor(textColor);
       if (textHeight == 0) {
           textHeight = textPaint.getTextSize();
       } else {
           textPaint.setTextSize(textHeight);
       }

       piePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
       piePaint.setStyle(Paint.Style.FILL);
       piePaint.setTextSize(textHeight);

       shadowPaint = new Paint(0);
       shadowPaint.setColor(0xff101010);
       shadowPaint.setMaskFilter(new BlurMaskFilter(8, BlurMaskFilter.Blur.NORMAL));

       ...
    

La creación de objetos con anticipación es una optimización importante. Las vistas se vuelven a diseñar con mucha frecuencia y muchos objetos de diseño requieren una costosa inicialización. La creación de objetos de diseño dentro del método onDraw() reduce considerablemente el rendimiento y puede hacer que la IU sea lenta.

Cómo controlar los eventos de diseño

Para diseñar correctamente tu vista personalizada, debes conocer el tamaño. Las vistas personalizadas complejas a menudo necesitan realizar múltiples cálculos de diseño según el tamaño y la forma del área en la pantalla. Nunca debes hacer suposiciones sobre el tamaño de la vista en la pantalla. Incluso si solo una app usa tu vista, esa app debe controlar diferentes tamaños de pantalla, múltiples densidades de pantalla y varias relaciones de aspecto tanto en modo vertical como horizontal.

Aunque View tiene muchos métodos para controlar la medición, la mayoría no requiere la anulación. Si la vista no necesita un control especial sobre el tamaño, solo debes anular un método: onSizeChanged().

Se llama a onSizeChanged() cuando se le asigna a la vista un tamaño por primera vez, y nuevamente si el tamaño de la vista cambia por algún motivo. Calcula las posiciones, las dimensiones y cualquier otro valor relacionado con el tamaño de la vista en onSizeChanged(), en lugar de volver a calcularlos cada vez que diseñes. En el ejemplo de PieChart, onSizeChanged() es el lugar donde la vista PieChart calcula el rectángulo delimitador del gráfico circular y la posición relativa de la etiqueta de texto, además de otros elementos visuales.

Cuando se le asigna un tamaño a la vista, el administrador de diseño asume que el tamaño incluye todo el relleno de la vista. Debes controlar los valores de relleno cuando calcules el tamaño de la vista. En este fragmento de PieChart.onSizeChanged(), se muestra cómo hacerlo:

Kotlin

    // Account for padding
    var xpad = (paddingLeft + paddingRight).toFloat()
    val ypad = (paddingTop + paddingBottom).toFloat()

    // Account for the label
    if (showText) xpad += textWidth

    val ww = w.toFloat() - xpad
    val hh = h.toFloat() - ypad

    // Figure out how big we can make the pie.
    val diameter = Math.min(ww, hh)
    

Java

    // Account for padding
    float xpad = (float)(getPaddingLeft() + getPaddingRight());
    float ypad = (float)(getPaddingTop() + getPaddingBottom());

    // Account for the label
    if (showText) xpad += textWidth;

    float ww = (float)w - xpad;
    float hh = (float)h - ypad;

    // Figure out how big we can make the pie.
    float diameter = Math.min(ww, hh);
    

Si necesitas un control más preciso sobre los parámetros de diseño de la vista, implementa onMeasure(). Los parámetros de este método son valores View.MeasureSpec que indican el tamaño deseado de la vista y si ese tamaño es un máximo estricto o solo una sugerencia. Como optimización, estos valores se almacenan como números enteros empaquetados y, para desempacar la información, debes usar los métodos estáticos de View.MeasureSpec.

A continuación, se incluye un ejemplo de implementación de onMeasure(). En esta implementación, PieChart intenta que el área sea lo suficientemente grande como para que el gráfico sea tan grande como su etiqueta:

Kotlin

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        // Try for a width based on our minimum
        val minw: Int = paddingLeft + paddingRight + suggestedMinimumWidth
        val w: Int = View.resolveSizeAndState(minw, widthMeasureSpec, 1)

        // Whatever the width ends up being, ask for a height that would let the pie
        // get as big as it can
        val minh: Int = View.MeasureSpec.getSize(w) - textWidth.toInt() + paddingBottom + paddingTop
        val h: Int = View.resolveSizeAndState(
                View.MeasureSpec.getSize(w) - textWidth.toInt(),
                heightMeasureSpec,
                0
        )

        setMeasuredDimension(w, h)
    }
    

Java

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       // Try for a width based on our minimum
       int minw = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth();
       int w = resolveSizeAndState(minw, widthMeasureSpec, 1);

       // Whatever the width ends up being, ask for a height that would let the pie
       // get as big as it can
       int minh = MeasureSpec.getSize(w) - (int)mTextWidth + getPaddingBottom() + getPaddingTop();
       int h = resolveSizeAndState(MeasureSpec.getSize(w) - (int)mTextWidth, heightMeasureSpec, 0);

       setMeasuredDimension(w, h);
    }
    

Hay tres aspectos importantes que debes tener en cuenta en este código:

  • Los cálculos tienen en cuenta el relleno de la vista. Como se mencionó anteriormente, esto es responsabilidad de la vista.
  • El método auxiliar resolveSizeAndState() se usa para crear los valores finales de ancho y alto. Este ayudante muestra un valor de View.MeasureSpec apropiado mediante la comparación del tamaño deseado de la vista con la especificación pasada a onMeasure().
  • onMeasure() no tiene valor de retorno. En su lugar, el método comunica los resultados llamando a setMeasuredDimension(). Llamar a este método es obligatorio. Si omites esta llamada, la clase View genera una excepción de tiempo de ejecución.

Cómo implementar el diseño

Una vez que hayas definido el código de medición y creación del objeto, puedes implementar onDraw(). Cada vista implementa onDraw() de manera diferente, pero hay algunas operaciones comunes que comparten la mayoría de las vistas:

  • Diseña el texto con drawText(). Para especificar el tipo de letra, llama a setTypeface(). Para especificar el color del texto, llama a setColor().
  • Diseña formas básicas con drawRect(), drawOval() y drawArc(). Para usar formas rellenas, contorneadas o ambas, llama a setStyle().
  • Diseña formas más complejas con la clase Path. Para definir una forma, agrega líneas y curvas a un objeto Path y, luego, dibuja la forma con drawPath(). Al igual que con las formas primitivas, puedes delinear o rellenar las rutas, o realizar ambas acciones, según el setStyle().
  • Define los rellenos de gradientes mediante la creación de objetos LinearGradient. Llama a setShader() para usar tu LinearGradient en formas rellenas.
  • Diseña mapas de bits con drawBitmap().

Por ejemplo, este es el código que diseña PieChart. Usa una mezcla de texto, líneas y formas.

Kotlin

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        canvas.apply {
            // Draw the shadow
            drawOval(shadowBounds, shadowPaint)

            // Draw the label text
            drawText(data[mCurrentItem].mLabel, textX, textY, textPaint)

            // Draw the pie slices
            data.forEach {
                piePaint.shader = it.mShader
                drawArc(bounds,
                        360 - it.endAngle,
                        it.endAngle - it.startAngle,
                        true, piePaint)
            }

            // Draw the pointer
            drawLine(textX, pointerY, pointerX, pointerY, textPaint)
            drawCircle(pointerX, pointerY, pointerSize, mTextPaint)
        }
    }
    

Java

    protected void onDraw(Canvas canvas) {
       super.onDraw(canvas);

       // Draw the shadow
       canvas.drawOval(
               shadowBounds,
               shadowPaint
       );

       // Draw the label text
       canvas.drawText(data.get(currentItem).mLabel, textX, textY, textPaint);

       // Draw the pie slices
       for (int i = 0; i < data.size(); ++i) {
           Item it = data.get(i);
           piePaint.setShader(it.shader);
           canvas.drawArc(bounds,
                   360 - it.endAngle,
                   it.endAngle - it.startAngle,
                   true, piePaint);
       }

       // Draw the pointer
       canvas.drawLine(textX, pointerY, pointerX, pointerY, textPaint);
       canvas.drawCircle(pointerX, pointerY, pointerSize, mTextPaint);
    }