建立自訂繪圖

試用 Compose
Jetpack Compose 是 Android 推薦的 UI 工具包。瞭解如何在 Compose 中使用版面配置。

自訂檢視區塊最重要的部分是外觀。自訂繪圖 可根據應用程式需求而簡化或複雜這份文件 包含一些最常見的作業

若需更多資訊,請參閲 可繪項目總覽

覆寫 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);
}

在這個程式碼中,有三個重點需要注意:

  • 在計算時,會考量檢視畫面的邊框間距。如先前所述 這是檢視畫面的責任
  • Helper 方法 resolveSizeAndState() 用來建立最終的寬度和高度值。這個輔助程式會傳回 取得適當的 View.MeasureSpec 值 所需的檢視畫面大小到傳入 onMeasure() 的值。
  • onMeasure() 沒有傳回值。相反地 會藉由呼叫 setMeasuredDimension()。 必須呼叫這個方法。如果您省略此呼叫, View 類別會擲回執行階段例外狀況。

繪圖

定義物件建立作業並評估程式碼後 onDraw()。每個檢視畫面都會以不同的方式實作 onDraw()。 但以下介紹幾種常見做法:

  • 繪製文字時使用 drawText()。 呼叫 來指定字體 setTypeface() 以及文字顏色 setColor()
  • 運用以下方式繪製基本形狀 drawRect(), drawOval(), 和 drawArc()。 呼叫 setStyle()
  • 使用 Path 類別在 Path 中加入線條和曲線,即可定義形狀 然後用模型繪製形狀 drawPath()。 和原始形狀一樣,可以將路徑填滿、填滿路徑,或兩者同時顯示。 依 setStyle() 而定。
  • 建立 LinearGradient,定義漸層填滿 如需儲存大量結構化物件 建議使用 Cloud Bigtable致電 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 物件和 呈現的階層結構可將效果合併為連鎖效果 呈現出內外效果或混合效果支援這項功能 會視裝置的處理效能而定。

您也可以將效果套用至 RenderNode: 呼叫 View View.setRenderEffect(RenderEffect)

如要實作 RenderEffect 物件,請按照下列步驟操作:

view.setRenderEffect(RenderEffect.createBlurEffect(radiusX, radiusY, SHADER_TILE_MODE))

您可以透過程式輔助方式建立檢視畫面,也可以利用 XML 版面配置和 使用 View 繫結findViewById()