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