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:
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 deView.MeasureSpec
apropiado mediante la comparación del tamaño deseado de la vista con la especificación pasada aonMeasure()
. onMeasure()
no tiene valor de retorno. En su lugar, 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.
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 asetTypeface()
. Para especificar el color del texto, llama asetColor()
. - Diseña formas básicas con
drawRect()
,drawOval()
ydrawArc()
. Para usar formas rellenas, contorneadas o ambas, llama asetStyle()
. - Diseña 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, puedes delinear o rellenar las rutas, o realizar ambas acciones, según elsetStyle()
. -
Define los rellenos de gradientes mediante la creación de objetos
LinearGradient
. Llama asetShader()
para usar tuLinearGradient
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); }