Crear un dibujo personalizado

Prueba el método de Compose
Jetpack Compose es el kit de herramientas de IU recomendado para Android. Obtén información para trabajar con diseños en Compose.

La parte más importante de una vista personalizada es su apariencia. Dibujo personalizado puede ser fácil o complejo según las necesidades de tu aplicación. Este documento que abarca algunas de las operaciones más comunes.

Para obtener más información, consulta Descripción general de los elementos de diseño.

Cómo anular onDraw()

El paso más importante al dibujar una vista personalizada es anular el onDraw() . El parámetro para onDraw() es un Canvas que la vista puede usar para dibujarse por sí misma. La clase Canvas define métodos para dibujar texto, líneas, mapas de bits y muchos otros gráficos primitivas. Puedes usar estos métodos en onDraw() para crear tu una interfaz de usuario (IU) personalizada.

Para comenzar, crea un Paint. En la siguiente sección, se analiza Paint con más detalle.

Cómo crear objetos de dibujo

El android.graphics framework divide el dibujo en dos áreas:

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

Por ejemplo, Canvas proporciona un método para dibujar una línea. Paint proporciona métodos para definir el color de esa línea. Canvas tiene un método para dibujar un rectángulo y Paint. define si se debe rellenar ese rectángulo con un color o dejarlo vacío. Canvas define formas que puedes dibujar en la pantalla. Paint define el color, el estilo, la fuente y otros aspectos de cada forma. dibujes.

Antes de dibujar algo, crea uno o más objetos Paint. El El siguiente ejemplo hace esto en un método llamado init. Este método es llamado desde el constructor desde Java, pero se puede inicializar en línea en Kotlin

Kotlin

@ColorInt
private var textColor    // Obtained from style attributes.

@Dimension
private var textHeight   // Obtained from style attributes.

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 Paint textPaint;
private Paint piePaint;
private Paint shadowPaint;

@ColorInt
private int textColor;       // Obtained from style attributes.

@Dimension
private float textHeight;    // Obtained from style attributes.

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 tienen las siguientes características: se vuelve a dibujar con frecuencia, y muchos objetos de dibujo requieren una costosa inicialización. Crear objetos de dibujo dentro de tu método onDraw() de manera significativa reduce el rendimiento y puede ralentizar la IU.

Cómo controlar eventos de diseño

Para dibujar correctamente tu vista personalizada, averigua de qué tamaño es. Complejidad personalizada las vistas a menudo deben realizar varios cálculos de diseño según el tamaño y la forma de su área en la pantalla. Nunca hagas suposiciones sobre el tamaño de tu en la pantalla. Incluso si solo una app usa tu vista, esa app debe controlar diferentes tamaños de pantalla, varias densidades de pantalla y diversos aspectos relaciones de aspecto en modo vertical y horizontal.

Aunque View tiene muchos métodos para manejar las mediciones, la mayoría de ellos no necesitan anulada. Si tu vista no necesita un control especial sobre el tamaño, solo anular un método: onSizeChanged()

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

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

Kotlin

private val showText    // Obtained from styled attributes.
private val textWidth   // Obtained from styled attributes.

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    super.onSizeChanged(w, h, oldw, oldh)
    // Account for padding.
    var xpad = (paddingLeft + paddingRight).toFloat()
    val ypad = (paddingTop + paddingBottom).toFloat()

    // Account for the label.
    if (showText) xpad += textWidth.toFloat()
    val ww = w.toFloat() - xpad
    val hh = h.toFloat() - ypad

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

Java

private Boolean showText;    // Obtained from styled attributes.
private int textWidth;       // Obtained from styled attributes.

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    // 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 you can make the pie.
    float diameter = Math.min(ww, hh);
}

Si necesitas un control más detallado de los parámetros de diseño de la vista, implementa onMeasure() Los parámetros de este método son View.MeasureSpec valores que indican el tamaño que quiere que tenga el publicador superior 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 se usan los métodos estáticos View.MeasureSpec para descomprimir la información almacenada en cada número entero.

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

Kotlin

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

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

    setMeasuredDimension(w, h)
}

Java

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

   // Whatever the width is, ask for a height that lets the pie get as big as it
   // can.
   int minh = MeasureSpec.getSize(w) - (int)textWidth + getPaddingBottom() + getPaddingTop();
   int h = resolveSizeAndState(minh, 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, 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 devuelve un valor de View.MeasureSpec apropiado comparando el el tamaño necesario de la vista al valor que se pasa a onMeasure().
  • onMeasure() no tiene valor de retorno. En cambio, el método comunica sus resultados llamando setMeasuredDimension() La llamada a este método es obligatoria. Si omites esta llamada, el La clase View arroja una excepción de tiempo de ejecución.

Generación

Después de definir 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:

  • Dibuja el texto con drawText() Especifica el tipo de letra llamando setTypeface() y el color del texto llamando setColor()
  • Dibujar formas primitivas con drawRect(), drawOval(), y drawArc() Cambia si las formas están rellenas, contorneadas o ambas llamando a setStyle()
  • Dibuja formas más complejas con el Path . Para definir una forma, agrega líneas y curvas a un objeto Path. objeto, luego dibuja la forma usando drawPath() Al igual que con las formas primitivas, las rutas se pueden delinear o rellenar, o ambas. según setStyle().
  • Define los rellenos de gradientes creando LinearGradient. objetos. Llamada setShader() para usar tu LinearGradient en formas rellenas.
  • Dibuja mapas de bits con drawBitmap()

El siguiente código dibuja una combinación de texto, líneas y formas:

Kotlin

private val data = mutableListOf<Item>() // A list of items that are displayed.

private var shadowBounds = RectF()       // Calculated in onSizeChanged.
private var pointerRadius: Float = 2f    // Obtained from styled attributes.
private var pointerX: Float = 0f         // Calculated in onSizeChanged.
private var pointerY: Float = 0f         // Calculated in onSizeChanged.
private var textX: Float = 0f            // Calculated in onSizeChanged.
private var textY: Float = 0f            // Calculated in onSizeChanged.
private var bounds = RectF()             // Calculated in onSizeChanged.
private var currentItem: Int = 0         // The index of the currently selected item.

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

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

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

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

        // Draw the pointer.
        drawLine(textX, pointerY, pointerX, pointerY, textPaint)
        drawCircle(pointerX, pointerY, pointerRadius, textPaint)
    }
}

// Maintains the state for a data item.
private data class Item(
    var label: String,      
    var value: Float = 0f,

    @ColorInt
    var color: Int = 0,

    // Computed values.
    var startAngle: Float = 0f,
    var endAngle: Float = 0f,

    var shader: Shader
)

Java

private List<Item> data = new ArrayList<Item>();  // A list of items that are displayed.

private RectF shadowBounds;                       // Calculated in onSizeChanged.
private float pointerRadius;                      // Obtained from styled attributes.
private float pointerX;                           // Calculated in onSizeChanged.
private float pointerY;                           // Calculated in onSizeChanged.
private float textX;                              // Calculated in onSizeChanged.
private float textY;                              // Calculated in onSizeChanged.
private RectF bounds;                             // Calculated in onSizeChanged.
private int currentItem = 0;                      // The index of the currently selected item.

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

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

    // Draw the label text.
    canvas.drawText(data.get(currentItem).label, 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, pointerRadius, textPaint);
}

// Maintains the state for a data item.
private class Item {
    public String label;
    public float value;
    @ColorInt
    public int color;

    // Computed values.
    public int startAngle;
    public int endAngle;

    public Shader shader;
}    

Aplicar efectos gráficos

Android 12 (nivel de API 31) incorpora la RenderEffect que aplica efectos de gráficos comunes, como desenfoques, filtros de color, Los efectos de sombreador de Android y mucho más View objetos y y las jerarquías correspondientes. Puedes combinar efectos como efectos de cadena, que consisten de un efecto interno y externo, o efectos combinados. Compatibilidad con esta función varía según la potencia de procesamiento del dispositivo.

También puedes aplicar efectos al modelo RenderNode para un View llamando View.setRenderEffect(RenderEffect)

Para implementar un objeto RenderEffect, haz lo siguiente:

view.setRenderEffect(RenderEffect.createBlurEffect(radiusX, radiusY, SHADER_TILE_MODE))

Puedes crear la vista de manera programática o ampliarla a partir de un diseño XML y Recupérala con la Vinculación de vistas . findViewById()