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