Tổng quan kiến trúc hiển thị
Hệ thống UI của Android hoạt động dựa trên cơ chế kết hợp giữa ViewRootImpl, Choreographer và Surface. Quá trình render một màn hình bắt đầu khi ViewRootImpl nhận tín hiệu VSYNC từ Choreographer, khởi động chuỗi xử lý qua ba giai đoạn chính: đo đạc (Measure), bố trí (Layout) và vẽ (Draw). Dữ liệu sau khi xử lý sẽ được gửi tới SurfaceFlinger để tổng hợp và hiển thị lên màn hình.
Chi tiết giai đoạn đo đạc (Measure)
Giai đoạn này xác định kích thước của View và View con. Hệ thống sử dụng lớp MeasureSpec để đóng gói thông tin về kích thước và mode đo lường thành một giá trị nguyên 32-bit.
Cơ chế MeasureSpec
Mỗi MeasureSpec bao gồm 2 bit cao nhất lưu trữ mode (chế độ đo) và 30 bit thấp nhất lưu trữ size (kích thước). Có 3 chế độ chính:
- UNSPECIFIED: Cha không giới hạn kích thước, con có thể tùy ý (thường dùng trong ListView, ScrollView).
- EXACTLY: Cha quy định kích thước chính xác (ví dụ:
match_parenthoặc giá trị cụ thể 100dp). - AT_MOST: Cha quy định kích thước tối đa, con không được vượt quá (ví dụ:
wrap_content).
Để minh họa cách tính toán thông số này, ta có thể tham khảo logic mô phỏng sau:
public class DimensionHelper {
private static final int BIT_SHIFT = 30;
private static final int MODE_MASK = 0x3 << BIT_SHIFT;
public static final int MODE_UNSPECIFIED = 0 << BIT_SHIFT;
public static final int MODE_EXACTLY = 1 << BIT_SHIFT;
public static final int MODE_AT_MOST = 2 << BIT_SHIFT;
public static int createSpec(int rawSize, int modeType) {
return (rawSize & ~MODE_MASK) | (modeType & MODE_MASK);
}
public static int getSpecSize(int packedValue) {
return packedValue & ~MODE_MASK;
}
public static int getSpecMode(int packedValue) {
return packedValue & MODE_MASK;
}
}
Trong quá trình đo đạc, ViewGroup sẽ duyệt qua các View con và tính toán MeasureSpec phù hợp dựa trên LayoutParams của chúng.
// Logic đơn giản hóa việc đo đạc View con
void measureChild(View child, int parentWidthSpec, int parentHeightSpec) {
LayoutParams params = child.getLayoutParams();
int childWidthSpec = calculateChildSpec(parentWidthSpec, params.width);
int childHeightSpec = calculateChildSpec(parentHeightSpec, params.height);
child.measure(childWidthSpec, childHeightSpec);
}
Chi tiết giai đoạn bố trí (Layout)
Sau khi đã có kích thước, giai đoạn Layout xác định vị trí tọa độ (trái, trên, phải, dưới) của View trên màn hình. Phương thức layout(l, t, r, b) được gọi để gán tọa độ, sau đó gọi onLayout() để ViewGroup sắp xếp các con.
public void assignPosition(int left, int top, int right, int bottom) {
boolean positionChanged = setBounds(left, top, right, bottom);
if (positionChanged || isLayoutRequired()) {
onLayout(positionChanged, left, top, right, bottom);
clearLayoutFlag();
}
}
Chi tiết giai đoạn vẽ (Draw)
Đây là bước nội dung thực sự được hiển thị. Quy trình vẽ diễn ra theo thứ tự sau:
- Vẽ nền (
drawBackground). - Lưu trạng thái Canvas (nếu cần).
- Vẽ nội dung chính (
onDraw). - Vẽ các View con (
dispatchDraw). - Vẽ các trang trí như thanh cuộn (
onDrawForeground).
public void renderView(Canvas canvas) {
drawBackground(canvas);
// Vẽ nội dung do tự implement
if (!isOpaque()) {
onDraw(canvas);
}
// ủy quyền vẽ cho các View con
dispatchDraw(canvas);
// Vẽ các lớp phủ phía trên
drawDecorations(canvas);
}
Tăng tốc phần cứng (Hardware Acceleration)
Android hỗ trợ chuyển đổi tác vụ vẽ từ CPU sang GPU để tăng hiệu năng. Khi bật chế độ này, các thao tác vẽ Canvas được chuyển đổi thành các lệnh OpenGL ES.
- Software Rendering: Sử dụng CPU để tính toán từng pixel, chậm hơn nhưng tương thích tốt hơn với các hiệu ứng phức tạp không hỗ trợ GPU.
- Hardware Acceleration: Sử dụng GPU, xử lý song song các thao tác hình ảnh, mượt mà hơn cho animation.
Cấu hình trong Manifest:
<application android:hardwareAccelerated="true">
<activity android:name=".MainActivity" />
</application>
Hoặc bật/tắt bằng code:
// Bắt buộc dùng phần mềm vẽ cho View cụ thể
myView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
Thực hành triển khai Custom View
Dưới đây là ví dụ về một CardView tùy chỉnh vẽ một hình chữ nhật bo góc thay vì hình tròn đơn giản để thay đổi logic so với ví dụ gốc.
public class CardViewWidget extends View {
private Paint fillPaint;
private RectF rectBounds;
private float cornerRadius = 20f;
private int cardColor = Color.BLUE;
public CardViewWidget(Context ctx, AttributeSet attrs) {
super(ctx, attrs);
setupWidget();
}
private void setupWidget() {
fillPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
fillPaint.setColor(cardColor);
rectBounds = new RectF();
}
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
int desiredW = 200 + getPaddingLeft() + getPaddingRight();
int desiredH = 100 + getPaddingTop() + getPaddingBottom();
int finalW = resolveSize(desiredW, widthSpec);
int finalH = resolveSize(desiredH, heightSpec);
setMeasuredDimension(finalW, finalH);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
rectBounds.set(
getPaddingLeft(),
getPaddingTop(),
getWidth() - getPaddingRight(),
getHeight() - getPaddingBottom()
);
canvas.drawRoundRect(rectBounds, cornerRadius, cornerRadius, fillPaint);
}
}
Thực hành triển khai Custom ViewGroup
Ví dụ về một WrapContainer xếp các con theo dòng ngang, tự động xuống dòng khi hết chỗ.
public class WrapContainer extends ViewGroup {
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
int maxW = MeasureSpec.getSize(widthSpec);
int modeW = MeasureSpec.getMode(widthSpec);
measureChildren(widthSpec, heightSpec);
int totalW = 0;
int totalH = 0;
int currentRowW = 0;
int currentRowH = 0;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) continue;
int childW = child.getMeasuredWidth();
int childH = child.getMeasuredHeight();
if (currentRowW + childW > maxW) {
// Xuống dòng
totalW = Math.max(totalW, currentRowW);
totalH += currentRowH;
currentRowW = childW;
currentRowH = childH;
} else {
currentRowW += childW;
currentRowH = Math.max(currentRowH, childH);
}
}
// Cộng thêm dòng cuối cùng
totalW = Math.max(totalW, currentRowW);
totalH += currentRowH;
int finalW = (modeW == MeasureSpec.EXACTLY) ? maxW : totalW;
setMeasuredDimension(finalW, totalH + getPaddingTop() + getPaddingBottom());
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int containerW = getWidth();
int curX = getPaddingLeft();
int curY = getPaddingTop();
int rowH = 0;
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) continue;
int w = child.getMeasuredWidth();
int h = child.getMeasuredHeight();
if (curX + w > containerW) {
curX = getPaddingLeft();
curY += rowH;
rowH = 0;
}
child.layout(curX, curY, curX + w, curY + h);
curX += w;
rowH = Math.max(rowH, h);
}
}
}
Chiến lược tối ưu hiệu năng
Để đảm bảo UI mượt mà (60fps), cần chú ý các vấn đề sau:
- Giảm Overdraw (Vẽ thừa): Tránh việc vẽ đè lớp lên nhau quá nhiều. Sử dụng
canvas.clipRect()để giới hạn vùng vẽ hoặc gánandroid:background="@null"cho các View nền không cần thiết. - Phẳng hóa cấu trúc Layout: Sử dụng
ConstraintLayoutthay vì lồng ghép quá nhiềuLinearLayouthoặcRelativeLayout. Cấu trúc sâu làm tăng thời gian đo đạc và bố trí. - Tải chậm (Lazy Loading): Sử dụng
ViewStubcho các UI hiếm khi hiển thị.
<ViewStub
android:id="@+id/loadingViewStub"
android:layout="@layout/layout_loading_panel"
android:inflatedId="@+id/loadingPanel" />
// Chỉ hiện khi cần thiết
ViewStub stub = findViewById(R.id.loadingViewStub);
if (stub != null) {
View inflatedView = stub.inflate();
}