自訂檢視區塊最重要的部分是外觀。自訂繪圖可根據應用程式需求而簡單或複雜。本文件涵蓋一些最常見的作業。
詳情請參閱「可繪項目總覽」。
覆寫 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)
,將效果套用至 View
的基礎 RenderNode
。
如要實作 RenderEffect
物件,請按照下列步驟操作:
view.setRenderEffect(RenderEffect.createBlurEffect(radiusX, radiusY, SHADER_TILE_MODE))
您可以透過程式輔助方式建立檢視畫面,或從 XML 版面配置中加載,然後使用檢視區塊繫結 或
findViewById()
擷取檢視區塊。