自訂檢視區塊最重要的部分是其外觀。自訂繪圖可以輕鬆或複雜,以滿足應用程式的需求。本文件會說明一些最常見的作業。
詳情請參閱「可繪項目總覽」。
覆寫 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()
擷取檢視畫面。