自定义视图最重要的部分是外观。绘制自定义视图可能很简单,也可能很复杂,具体取决于应用的需求。这节课将介绍一些最常见的操作。
除了本课程,您还可以在画布和可绘制对象中找到其他相关信息。
替换 onDraw()
绘制自定义视图最重要的一步是替换 onDraw()
方法。onDraw()
的参数是一个 Canvas
对象,视图可以使用该对象绘制其自身。Canvas
类定义了绘制文本、线条、位图和许多其他图形基元的方法。您可以在 onDraw()
中使用这些方法创建自定义界面。
但是,在调用任何绘制方法之前,必须先创建 Paint
对象。下一节将详细介绍 Paint
。
创建绘制对象
android.graphics
框架将绘制分为两个方面:
例如,Canvas
提供绘制线条的方法,Paint
则提供定义线条颜色的方法。Canvas
具有绘制矩形的方法,Paint
则定义是在矩形中填充颜色还是留空。简而言之,Canvas
定义您可以在屏幕上绘制的形状,Paint
则定义您绘制的每个形状的颜色、样式和字体等。
因此,在绘制任何内容之前,您需要创建一个或多个Paint
对象。PieChart
示例在名为 init
的方法中执行此操作,该方法是从 Java 的构造函数调用的,但我们可以在 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)); ...
提前创建对象是一项重要的优化措施。视图会非常频繁地重新绘制,并且许多绘制对象的初始化都需要占用很多资源。在 onDraw()
方法内创建绘制对象会显著降低性能并使界面显得卡顿。
处理布局事件
为了正确绘制自定义视图,您需要知道它的大小。复杂的自定义视图通常需要根据其在屏幕上所占区域的大小和形状执行多次布局计算。不要妄自假设视图在屏幕上的大小。即使只有一个应用使用您的视图,该应用也需要处理纵向和横向模式下的不同屏幕尺寸、多种屏幕密度和各种宽高比。
尽管 View
提供多种测量处理方法,大部分方法都不需要被替换。如果您的视图不需要对其大小进行特殊控制,您只需替换一个方法,即 onSizeChanged()
。
系统会在首次为您的视图分配大小时调用 onSizeChanged()
,如果视图大小由于任何原因而改变,系统会再次调用该方法。请在 onSizeChanged()
中计算位置、尺寸以及其他与视图大小相关的任何值,而不要在每次绘制时都重新计算。在 PieChart
示例中,PieChart
视图在 onSizeChanged()
中计算饼图的边界矩形,并计算文本标签和其他视觉元素的相对位置。
为视图指定大小时,布局管理器会假定其大小包含视图的所有内边距。您必须在计算视图大小时处理内边距值。以下是PieChart.onSizeChanged()
的代码段,其中说明了如何执行此操作:
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);
如果您需要更精细地控制视图的布局参数,请实现 onMeasure()
。此方法的参数是 View.MeasureSpec
值,用于告诉您视图的父视图希望您的视图有多大,以及该大小是硬性最大值还是只是建议值。作为优化措施,这些值以打包整数形式存储,您可以使用 View.MeasureSpec
的静态方法解压缩每个整数中存储的信息。
以下是 onMeasure()
的一个实现示例。在此实现中,PieChart
尝试使其面积足够大,以使饼图大小与其标签一致:
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); }
在此代码中,有三点需要注意:
- 计算时会考虑视图的内边距。如前文所述,这由视图负责计算。
- 辅助方法
resolveSizeAndState()
用于创建最终的宽度和高度值。该辅助程序通过将视图所需大小与传递到onMeasure()
的规格进行比较,返回合适的View.MeasureSpec
值。 onMeasure()
没有返回值。而是由方法通过调用setMeasuredDimension()
传达其结果。必须调用此方法。如果省略此调用,View
类将抛出运行时异常。
绘制!
创建好对象并定义了测量代码后,您可以实现 onDraw()
。每个视图以不同方式实现 onDraw()
,但大多数视图共享一些常见的操作:
- 使用
drawText()
绘制文本。通过调用setTypeface()
指定字体,并通过调用setColor()
指定文本颜色。 - 使用
drawRect()
、drawOval()
和drawArc()
绘制基元形状。通过调用setStyle()
更改形状的填充和/或轮廓。 - 使用
Path
类绘制更复杂的形状。通过将线条和曲线添加到Path
对象以定义形状,然后使用drawPath()
绘制形状。与基元形状一样,路径可以只描绘轮廓或只进行填充,也可以两者兼具,具体取决于setStyle()
。 -
通过创建
LinearGradient
对象定义渐变填充。调用setShader()
可在填充的形状上使用LinearGradient
。 - 使用
drawBitmap()
绘制位图。
例如,以下代码绘制了 PieChart
。它组合使用了文本、线条和形状。
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); }