自訂檢視區塊最重要的部分是外觀。自訂繪圖可簡單也可複雜,視應用程式需求而定。本文涵蓋一些最常見的作業。
詳情請參閱「可繪項目總覽」。
覆寫 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() 擷取檢視區塊。