自定义视图最重要的部分是其外观。根据应用程序的需求,自定义绘图可以很简单也可以很复杂。本文档涵盖了一些最常见的操作。
有关更多信息,请参阅 可绘制对象概述。
覆盖 onDraw()
绘制自定义视图最重要的步骤是覆盖 onDraw()
方法。 onDraw()
的参数是一个 Canvas
对象,视图可以使用它来绘制自身。 Canvas
类定义了用于绘制文本、线条、位图和许多其他图形原语的方法。您可以在 onDraw()
中使用这些方法来创建自定义用户界面 (UI)。
首先创建一个 Paint
对象。下一节将更详细地讨论 Paint
。
创建绘图对象
android.graphics
框架将绘图分为两个区域
- 绘制什么,由
Canvas
处理。 - 如何绘制,由
Paint
处理。
例如, Canvas
提供了一个绘制线条的方法,而 Paint
提供了定义该线条颜色的方法。 Canvas
有一个绘制矩形的方法,而 Paint
定义了是否用颜色填充该矩形或将其保留为空。 Canvas
定义了可以在屏幕上绘制的形状,而 Paint
定义了绘制的每个形状的颜色、样式、字体等。
在绘制任何内容之前,请创建一个或多个 Paint
对象。以下示例在名为 init
的方法中执行此操作。此方法从 Java 中的构造函数调用,但它可以在 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)); ... }
提前创建对象是一项重要的优化。视图经常重新绘制,许多绘图对象需要昂贵的初始化。在 onDraw()
方法中创建绘图对象会显着降低性能,并可能使您的 UI 变得迟钝。
处理布局事件
为了正确绘制自定义视图,请找出它的尺寸。复杂的自定义视图通常需要根据其在屏幕上的区域的大小和形状执行多个布局计算。永远不要假设视图在屏幕上的大小。即使只有一个应用使用您的视图,该应用也需要处理不同的屏幕尺寸、多个屏幕密度以及纵向和横向模式下的各种纵横比。
尽管 View
有许多用于处理测量的的方法,但其中大多数不需要被覆盖。如果您的视图不需要对自身的大小进行特殊控制,则只需覆盖一个方法: onSizeChanged()
。
onSizeChanged()
在您的视图第一次分配大小时调用,如果您的视图的大小因任何原因发生更改,则再次调用。在 onSizeChanged()
中计算与视图大小相关的职位、尺寸和任何其他值,而不是在每次绘制时重新计算它们。在以下示例中, onSizeChanged()
是视图计算图表边界矩形以及文本标签和其他视觉元素的相对位置的地方。
当您的视图被分配大小时,布局管理器假设该大小包括视图的填充。在计算视图大小时处理填充值。以下是从 onSizeChanged()
中的代码片段,显示了如何执行此操作
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); }
如果您需要更精细地控制视图的布局参数,请实现 onMeasure()
。此方法的参数是 View.MeasureSpec
值,这些值告诉您视图的父级希望视图有多大,以及该大小是否为硬性最大值或只是一个建议。作为优化,这些值存储为打包的整数,您可以使用 View.MeasureSpec
的静态方法来解包存储在每个整数中的信息。
这是一个 onMeasure()
的示例实现。在此实现中,它试图使其区域足够大,以使图表与其标签一样大
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); }
在此代码中有三件重要的事情需要注意
- 计算考虑了视图的填充。如前所述,这是视图的责任。
- 辅助方法
resolveSizeAndState()
用于创建最终的宽度和高度值。此辅助程序通过将视图所需的大小与传递到onMeasure()
的值进行比较,返回适当的View.MeasureSpec
值。 onMeasure()
没有返回值。相反,该方法通过调用setMeasuredDimension()
来传达其结果。调用此方法是强制性的。如果您省略此调用,则View
类会抛出运行时异常。
绘制
定义对象创建和测量代码后,您可以实现 onDraw()
。每个视图都以不同的方式实现 onDraw()
,但大多数视图共享一些常见操作
- 使用
drawText()
绘制文本。通过调用setTypeface()
指定字体,并通过调用setColor()
指定文本颜色。 - 使用
drawRect()
、drawOval()
和drawArc()
绘制基本形状。通过调用setStyle()
更改形状是填充、轮廓还是两者兼而有之。 - 使用
Path
类绘制更复杂的形状。通过向Path
对象添加线条和曲线来定义形状,然后使用drawPath()
绘制形状。与基本形状一样,路径可以是轮廓、填充或两者兼而有之,具体取决于setStyle()
。 - 通过创建
LinearGradient
对象来定义渐变填充。调用setShader()
以在填充形状上使用您的LinearGradient
。 - 使用
drawBitmap()
绘制位图。
以下代码绘制了文本、线条和形状的混合
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; }
应用图形效果
Android 12(API 级别 31)添加了 RenderEffect
类,该类将常见的图形效果(例如模糊、颜色滤镜、Android 着色器效果等)应用于 View
对象和渲染层次结构。您可以将效果组合为链式效果,链式效果由内部效果和外部效果组成,或者混合效果。对该功能的支持因设备处理能力而异。
您还可以通过调用 View.setRenderEffect(RenderEffect)
将效果应用于 RenderNode
的底层 View
。
要实现 RenderEffect
对象,请执行以下操作
view.setRenderEffect(RenderEffect.createBlurEffect(radiusX, radiusY, SHADER_TILE_MODE))
您可以以编程方式创建视图,或从 XML 布局中加载它,并使用 视图绑定 或 findViewById()
检索它。