สร้างภาพวาดที่กำหนดเอง

ลองใช้วิธีแบบ Compose
Jetpack Compose เป็นชุดเครื่องมือ UI ที่แนะนำสำหรับ Android ดูวิธีทำงานกับเลย์เอาต์ใน Compose

ส่วนที่สำคัญที่สุดของมุมมองที่กำหนดเองคือลักษณะที่ปรากฏ การวาดภาพที่กำหนดเอง อาจง่ายหรือซับซ้อนก็ได้ตามความต้องการของแอปพลิเคชัน เอกสารนี้ ครอบคลุมการดำเนินการที่พบบ่อยที่สุดบางอย่าง

ดูข้อมูลเพิ่มเติมได้ที่ ภาพรวมของ Drawable

ลบล้าง onDraw()

ขั้นตอนที่สำคัญที่สุดในการวาดวิวที่กำหนดเองคือการลบล้างเมธอด onDraw() พารามิเตอร์ของ onDraw() คือออบเจ็กต์ Canvas ที่มุมมองใช้ในการวาดตัวเองได้ คลาส Canvas กำหนดเมธอดสำหรับการวาดข้อความ เส้น บิตแมป และกราฟิกอื่นๆ อีกมากมาย คุณสามารถใช้วิธีการเหล่านี้ใน onDraw() เพื่อสร้าง อินเทอร์เฟซผู้ใช้ (UI) ที่กำหนดเอง

เริ่มต้นด้วยการสร้างออบเจ็กต์ Paint ส่วนถัดไปจะอธิบายPaintโดยละเอียด

สร้างออบเจ็กต์ภาพวาด

เฟรมเวิร์ก android.graphics แบ่งการวาดภาพออกเป็น 2 ส่วน ดังนี้

  • สิ่งที่ต้องวาด ซึ่งจัดการโดย Canvas
  • วิธีวาดภาพ จัดการโดย Paint

เช่น Canvas มีเมธอดในการวาดเส้น และ Paint มีเมธอดในการกำหนดสีของเส้นนั้น Canvas มีวิธีวาดสี่เหลี่ยมผืนผ้า และ Paint กำหนดว่าจะเติมสีในสี่เหลี่ยมผืนผ้านั้นหรือปล่อยว่างไว้ Canvas กำหนดรูปร่างที่คุณวาดบนหน้าจอได้ และ Paint กำหนดสี สไตล์ แบบอักษร และอื่นๆ ของแต่ละรูปร่างที่คุณวาด

ก่อนวาดสิ่งใด ให้สร้างPaintออบเจ็กต์อย่างน้อย 1 รายการ ตัวอย่างต่อไปนี้จะดำเนินการนี้ในเมธอดที่ชื่อ 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 จะมีหลายวิธีในการจัดการการวัดผล แต่ส่วนใหญ่ไม่จำเป็นต้อง ลบล้าง หาก View ไม่จำเป็นต้องมีการควบคุมขนาดเป็นพิเศษ ให้ ลบล้างเมธอดเพียง 1 รายการ ดังนี้ 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);
}

สิ่งสำคัญ 3 อย่างที่ควรทราบในโค้ดนี้มีดังนี้

  • การคำนวณจะพิจารณาการเว้นวรรคของมุมมอง ดังที่กล่าวไว้ ก่อนหน้านี้ นี่คือความรับผิดชอบของมุมมอง
  • เมธอดตัวช่วย resolveSizeAndState() ใช้เพื่อสร้างค่าความกว้างและความสูงสุดท้าย ตัวช่วยนี้จะแสดงผลค่า View.MeasureSpec ที่เหมาะสมโดยการเปรียบเทียบขนาดที่ต้องการของมุมมองกับค่าที่ส่งไปยัง onMeasure()
  • onMeasure() ไม่มีค่าที่ส่งคืน แต่เมธอดจะ สื่อสารผลลัพธ์โดยการเรียก setMeasuredDimension() ต้องเรียกใช้เมธอดนี้ หากคุณละเว้นการเรียกนี้ คลาส View จะแสดงข้อยกเว้นรันไทม์

วาด

หลังจากกำหนดโค้ดการสร้างออบเจ็กต์และการวัดแล้ว คุณจะติดตั้งใช้งาน onDraw() ได้ แต่ละมุมมองจะใช้ onDraw() แตกต่างกัน แต่มีการดำเนินการทั่วไปบางอย่างที่มุมมองส่วนใหญ่ใช้ร่วมกัน ดังนี้

  • วาดข้อความโดยใช้ drawText() ระบุแบบอักษรโดยเรียก setTypeface() และสีข้อความโดยเรียก setColor()
  • วาดรูปร่างพื้นฐานโดยใช้ drawRect() drawOval() และ drawArc() เปลี่ยนว่าจะให้รูปทรงเป็นแบบเติมสี แบบร่าง หรือทั้ง 2 อย่างโดยเรียกใช้ setStyle()
  • วาดรูปร่างที่ซับซ้อนมากขึ้นโดยใช้คลาส Path กำหนดรูปร่างโดยการเพิ่มเส้นและเส้นโค้งไปยังPath ออบเจ็กต์ จากนั้นวาดรูปร่างโดยใช้ drawPath() เช่นเดียวกับรูปร่างพื้นฐาน คุณสามารถกำหนดเส้นขอบ เติมสี หรือทั้ง 2 อย่างให้กับเส้นทางได้ โดยขึ้นอยู่กับ 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 คลาส ซึ่งใช้เอฟเฟกต์กราฟิกทั่วไป เช่น เบลอ ฟิลเตอร์สี เอฟเฟกต์ Shader ของ Android และอื่นๆ กับออบเจ็กต์ Viewและลำดับชั้นการแสดงผล คุณสามารถรวมเอฟเฟกต์เป็นเอฟเฟกต์แบบลูกโซ่ ซึ่งประกอบด้วย เอฟเฟกต์ด้านในและด้านนอก หรือเอฟเฟกต์แบบผสม การรองรับฟีเจอร์นี้ จะแตกต่างกันไปตามกำลังการประมวลผลของอุปกรณ์

นอกจากนี้ คุณยังใช้เอฟเฟกต์กับRenderNodeพื้นฐานสำหรับViewได้ด้วยโดยเรียกใช้ View.setRenderEffect(RenderEffect)

หากต้องการใช้RenderEffectออบเจ็กต์ ให้ทำดังนี้

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

คุณสามารถสร้างมุมมองโดยอัตโนมัติหรือขยายจากเลย์เอาต์ XML และ เรียกข้อมูลโดยใช้การเชื่อมโยงมุมมอง หรือ findViewById()