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 este documento, se abordan 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 del diseño de una vista personalizada es anular el método onDraw()
. El parámetro para onDraw()
es un objeto Canvas
que la vista puede usar en su propio diseño. La clase Canvas
define métodos para diseñar texto, líneas, mapas de bits y muchas otras primitivas de gráficos. Puedes usar estos métodos en onDraw()
para crear tu interfaz de usuario (IU) personalizada.
Para comenzar, crea un objeto Paint
.
En la siguiente sección, se analiza Paint
con más detalle.
Crear objetos de dibujo
El framework android.graphics
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 y 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 las formas que puedes dibujar en la pantalla, y Paint
define el color, el estilo, la fuente, etc., de cada forma que dibujas.
Antes de dibujar algo, crea uno o más objetos Paint
. En el siguiente ejemplo, se hace esto en un método llamado init
. Este método se llama desde el constructor desde Java, pero se puede inicializar de forma intercalada 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 se vuelven a dibujar con frecuencia, y muchos objetos de dibujo requieren una inicialización costosa.
La creación de objetos de dibujo dentro del método onDraw()
reduce significativamente el rendimiento y puede hacer que la IU sea lenta.
Cómo controlar eventos de diseño
Para dibujar correctamente tu vista personalizada, descubre de qué tamaño es. Las vistas personalizadas complejas suelen necesitar 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 la vista en la pantalla. Incluso si solo una app usa tu vista, esta debe controlar diferentes tamaños de pantalla, varias 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, no es necesario anular la mayoría de ellos. Si la vista no necesita un control especial sobre su tamaño, solo anula un método: onSizeChanged()
.
Se llama a onSizeChanged()
cuando se le asigna un tamaño a la vista 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 dibujes.
En el siguiente ejemplo, onSizeChanged()
es donde la vista calcula el rectángulo delimitador del gráfico, la posición relativa de la etiqueta de texto y otros elementos visuales.
Cuando se le asigna un tamaño a tu vista, el administrador de diseño asume que el tamaño incluye el padding de la vista. Controla los valores de padding cuando calcules el tamaño de la vista. En este fragmento de onSizeChanged()
, se muestra cómo 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 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 te indican el tamaño que el elemento superior del elemento superior quiere que sea tu 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 debes usar los métodos estáticos de 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 esta implementación, intenta hacer 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 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ó antes, esto es responsabilidad de la vista.
- El método auxiliar
resolveSizeAndState()
se usa para crear los valores finales de ancho y altura. Este ayudante muestra un valor deView.MeasureSpec
apropiado comparando el tamaño necesario de la vista con el valor que se pasó aonMeasure()
. onMeasure()
no tiene valor de retorno. En cambio, el método comunica los resultados llamando asetMeasuredDimension()
. Llamar a este método es obligatorio. Si omites esta llamada, la claseView
genera una excepción de tiempo de ejecución.
Generación
Después de definir el código de creación y medición de tu 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 texto con
drawText()
. Para especificar el tipo de letra, llama asetTypeface()
; para el color del texto, llama asetColor()
. - Dibuja formas primitivas con
drawRect()
,drawOval()
ydrawArc()
. Llama asetStyle()
para cambiar si las formas están rellenas, contorneadas o ambas. - Dibuja formas más complejas con la clase
Path
. Para definir una forma, agrega líneas y curvas a un objetoPath
y, luego, dibuja la forma condrawPath()
. Al igual que con las formas primitivas, las rutas se pueden delinear o rellenar, o ambas, segúnsetStyle()
. -
Para definir los rellenos de gradientes, crea objetos
LinearGradient
. Llama asetShader()
para usar tuLinearGradient
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; }
Cómo aplicar efectos gráficos
Android 12 (nivel de API 31) agrega la clase RenderEffect
, que aplica efectos de gráficos comunes, como desenfoques, filtros de color, efectos de sombreador de Android y mucho más a los objetos View
y jerarquías de renderización. Puedes combinar efectos como en cadena, que constan de un efecto interno y externo, o bien efectos combinados. La compatibilidad con esta función varía según la potencia de procesamiento del dispositivo.
También puedes aplicar efectos al RenderNode
subyacente para una View
llamando a 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 desde un diseño XML y recuperarla mediante la vinculación de vistas o
findViewById()
.