Phần quan trọng nhất của thành phần hiển thị tuỳ chỉnh là hình thức. Thao tác vẽ tuỳ chỉnh có thể dễ hoặc phức tạp tuỳ theo nhu cầu của ứng dụng. Tài liệu này trình bày một số thao tác phổ biến nhất.
Để biết thêm thông tin, hãy xem bài viết Tổng quan về đối tượng có thể vẽ.
Ghi đè onDraw()
Bước quan trọng nhất trong quá trình vẽ khung hiển thị tuỳ chỉnh là ghi đè phương thức onDraw()
. Tham số cho onDraw()
là một đối tượng Canvas
mà khung hiển thị có thể dùng để tự vẽ. Lớp Canvas
xác định các phương thức để vẽ văn bản, đường kẻ, bitmap và nhiều dữ liệu đồ hoạ gốc khác. Bạn có thể sử dụng các phương thức này trong onDraw()
để tạo giao diện người dùng (UI) tuỳ chỉnh.
Hãy bắt đầu bằng cách tạo đối tượng Paint
.
Phần tiếp theo sẽ thảo luận chi tiết hơn về Paint
.
Tạo đối tượng vẽ
Khung android.graphics
chia bản vẽ thành 2 khu vực:
- Nội dung cần vẽ, do
Canvas
xử lý. - Cách vẽ, do
Paint
xử lý.
Ví dụ: Canvas
cung cấp một phương thức để vẽ một đường, và Paint
cung cấp các phương thức để xác định màu của đường đó.
Canvas
có một phương thức để vẽ một hình chữ nhật và Paint
xác định xem nên tô màu cho hình chữ nhật đó hay để trống.
Canvas
xác định các hình dạng mà bạn có thể vẽ trên màn hình, còn Paint
xác định màu sắc, kiểu, phông chữ, v.v. của mỗi hình dạng bạn vẽ.
Trước khi bạn vẽ bất cứ thứ gì, hãy tạo một hoặc nhiều đối tượng Paint
. Ví dụ sau đây thực hiện việc này trong một phương thức có tên là init
. Phương thức này được gọi từ hàm khởi tạo trong Java, nhưng có thể được khởi tạo cùng dòng trong 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)); ... }
Việc tạo trước các đối tượng là một hoạt động tối ưu hoá quan trọng. Khung hiển thị được vẽ lại thường xuyên và nhiều đối tượng vẽ yêu cầu khởi chạy tốn kém.
Việc tạo đối tượng vẽ trong phương thức onDraw()
làm giảm đáng kể hiệu suất và có thể khiến giao diện người dùng hoạt động chậm.
Xử lý sự kiện bố cục
Để vẽ khung hiển thị tuỳ chỉnh đúng cách, hãy tìm hiểu kích thước của khung hiển thị đó. Các khung hiển thị tuỳ chỉnh phức tạp thường cần thực hiện nhiều phép tính bố cục tuỳ thuộc vào kích thước và hình dạng của khu vực trên màn hình. Đừng giả định về kích thước của thành phần hiển thị trên màn hình. Ngay cả khi chỉ một ứng dụng sử dụng khung hiển thị của bạn, thì ứng dụng đó vẫn cần phải xử lý nhiều kích thước màn hình, nhiều mật độ màn hình và nhiều tỷ lệ khung hình ở cả chế độ dọc và ngang.
Mặc dù View
có nhiều phương thức để xử lý hoạt động đo lường, nhưng hầu hết các phương thức này đều không cần bị ghi đè. Nếu khung hiển thị của bạn không cần quyền kiểm soát đặc biệt đối với kích thước, thì bạn chỉ ghi đè một phương thức: onSizeChanged()
.
onSizeChanged()
được gọi khi khung hiển thị được chỉ định kích thước lần đầu và gọi lại nếu kích thước của khung hiển thị thay đổi vì bất kỳ lý do gì. Tính toán vị trí, kích thước và mọi giá trị khác liên quan đến kích thước của thành phần hiển thị trong onSizeChanged()
, thay vì tính toán lại mỗi lần bạn vẽ.
Trong ví dụ sau, onSizeChanged()
là nơi khung hiển thị tính toán hình chữ nhật giới hạn của biểu đồ và vị trí tương đối của nhãn văn bản cũng như các thành phần hình ảnh khác.
Khi khung hiển thị của bạn được chỉ định một kích thước, trình quản lý bố cục sẽ giả định rằng kích thước đó có bao gồm khoảng đệm của khung hiển thị. Xử lý các giá trị khoảng đệm khi bạn tính toán kích thước của khung hiển thị. Dưới đây là đoạn mã từ onSizeChanged()
cho thấy cách thực hiện việc này:
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); }
Nếu bạn cần kiểm soát chặt chẽ hơn các tham số bố cục của khung hiển thị, hãy triển khai onMeasure()
.
Các tham số của phương thức này là các giá trị View.MeasureSpec
cho bạn biết thành phần mẹ của khung hiển thị gốc muốn kích thước của khung hiển thị như thế nào và liệu kích thước đó là tối đa cố định hay chỉ là một đề xuất. Để tối ưu hoá, các giá trị này được lưu trữ dưới dạng số nguyên đóng gói và bạn sẽ sử dụng các phương thức tĩnh của View.MeasureSpec
để giải nén thông tin được lưu trữ trong mỗi số nguyên.
Dưới đây là ví dụ về cách triển khai onMeasure()
. Trong quá trình triển khai này, hệ thống cố gắng tạo diện tích đủ lớn để làm cho biểu đồ lớn bằng nhãn của biểu đồ:
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); }
Có 3 điều quan trọng cần lưu ý trong mã này:
- Các phép tính có tính đến khoảng đệm của khung hiển thị. Như đã đề cập trước đó, đây là trách nhiệm của khung hiển thị.
- Phương thức trợ giúp
resolveSizeAndState()
được dùng để tạo các giá trị chiều rộng và chiều cao cuối cùng. Trình trợ giúp này sẽ trả về một giá trịView.MeasureSpec
thích hợp bằng cách so sánh kích thước cần thiết của khung hiển thị với giá trị được chuyển vàoonMeasure()
. onMeasure()
không có giá trị trả về. Thay vào đó, phương thức này sẽ thông báo kết quả bằng cách gọisetMeasuredDimension()
. Bạn bắt buộc phải gọi phương thức này. Nếu bạn bỏ qua lệnh gọi này, lớpView
sẽ gửi ra một ngoại lệ trong thời gian chạy.
Ngang nhau
Sau khi xác định việc tạo đối tượng và mã đo lường, bạn có thể triển khai onDraw()
. Mỗi khung hiển thị triển khai onDraw()
theo cách khác nhau, nhưng có một số thao tác phổ biến mà hầu hết các khung hiển thị đều có chung:
- Vẽ văn bản bằng
drawText()
. Chỉ định kiểu chữ bằng cách gọisetTypeface()
và màu văn bản bằng cách gọisetColor()
. - Vẽ các hình dạng gốc bằng
drawRect()
,drawOval()
vàdrawArc()
. Thay đổi xem hình dạng đã được tô nền, đường viền hay cả hai bằng cách gọisetStyle()
. - Vẽ các hình dạng phức tạp hơn bằng cách sử dụng lớp
Path
. Xác định hình dạng bằng cách thêm các đường kẻ và đường cong vào đối tượngPath
, sau đó vẽ hình dạng đó bằngdrawPath()
. Tương tự như với các hình dạng nguyên thuỷ, đường dẫn có thể được vẽ đường viền, tô màu nền hoặc cả hai, tuỳ thuộc vàosetStyle()
. -
Xác định màu nền chuyển màu bằng cách tạo các đối tượng
LinearGradient
. GọisetShader()
để sử dụngLinearGradient
trên các hình được tô màu nền. - Vẽ bitmap bằng
drawBitmap()
.
Đoạn mã sau đây kết hợp văn bản, đường kẻ và hình dạng:
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; }
Áp dụng hiệu ứng đồ hoạ
Android 12 (API cấp 31) thêm lớp RenderEffect
, áp dụng các hiệu ứng đồ hoạ phổ biến như làm mờ, bộ lọc màu, hiệu ứng chương trình đổ bóng Android, v.v. cho các đối tượng View
và hệ phân cấp hiển thị. Bạn có thể kết hợp các hiệu ứng dưới dạng hiệu ứng chuỗi, bao gồm hiệu ứng bên trong và bên ngoài hoặc hiệu ứng kết hợp. Khả năng hỗ trợ cho tính năng này khác nhau tuỳ thuộc vào công suất xử lý của thiết bị.
Bạn cũng có thể áp dụng các hiệu ứng cho RenderNode
cơ bản của View
bằng cách gọi View.setRenderEffect(RenderEffect)
.
Để triển khai một đối tượng RenderEffect
, hãy làm như sau:
view.setRenderEffect(RenderEffect.createBlurEffect(radiusX, radiusY, SHADER_TILE_MODE))
Bạn có thể tạo khung hiển thị theo phương thức lập trình hoặc tăng cường từ bố cục XML và truy xuất khung hiển thị đó bằng cách sử dụng tính năng Liên kết khung hiển thị hoặc
findViewById()
.