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

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

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

สำหรับข้อมูลเพิ่มเติม โปรดดู ภาพรวมของเนื้อหาที่ถอนออกได้

ลบล้าง 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 มีหลายวิธีในการจัดการการวัดผล ซึ่งส่วนใหญ่ไม่จำเป็นต้อง ถูกลบล้าง หากมุมมองของคุณไม่จำเป็นต้องมีการควบคุมเป็นพิเศษเกี่ยวกับขนาด ให้ ลบล้างเมธอดเดียว 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() เปลี่ยนว่าจะให้เติมรูปภาพ เติมขอบ หรือทำทั้งสองอย่างโดยการเรียก 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 ออบเจ็กต์และ ลำดับชั้นการแสดงผล คุณสามารถรวมเอฟเฟกต์เป็นเอฟเฟกต์ลูกโซ่ ซึ่งประกอบด้วย เอฟเฟ็กต์ภายในและภายนอก หรือเอฟเฟกต์ผสม การสนับสนุนสำหรับฟีเจอร์นี้ จะแตกต่างกันไปตามกำลังในการประมวลผลของอุปกรณ์

คุณยังใช้เอฟเฟกต์กับ RenderNode สำหรับ View โดยการโทร View.setRenderEffect(RenderEffect)

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

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

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