Tạo Custom View trong Android

Ở bài viết trước, chúng ta đã làm quen với khái niệm về custom View. Trong bài này, chúng ta sẽ thực hành bằng cách tạo một custom View hiển thị hình ảnh kèm theo nhãn văn bản bên dưới. Hãy cùng theo dõi các bước triển khai chi tiết.

Quy trình thực hiện gồm 3 bước chính:

  1. Khai báo các thuộc tính tùy chỉnh trong file attrs.xml trong thư mục values
  2. Tạo class kế thừa từ View và xử lý việc vẽ giao diện
  3. Sử dụng custom view trong file layout

Bắt đầu với bước đầu tiên:

Bước 1: Tạo file attrs.xml trong thư mục res/values

<?xml version="1.0" encoding="utf-8"?>
<resources>
    
    <attr name="labelText" format="string" />
    <attr name="labelColor" format="color" />
    <attr name="labelSize" format="dimension" />
    
    <attr name="srcImage" format="reference" />
    <attr name="scaleMode">
        <enum name="fillXY" value="0" />
        <enum name="center" value="1" />
    </attr>
    
    <declare-styleable name="BannerViewStyle">
        <attr name="labelText" />  
        <attr name="labelColor" />  
        <attr name="labelSize" />
        <attr name="srcImage" />
        <attr name="scaleMode" />
    </declare-styleable>
    
</resources>

Lưu ý: Thuộc tính format xác định kiểu dữ liệu của tham số (string, color, dimension, reference,...)

Bước 2: Tạo class BannerView kế thừa từ View

public class BannerView extends View {
    
    // Vùng chứa hình ảnh
    Rect bitmapArea;
    // Vùng chứa văn bản nhãn
    Rect labelArea;
    // Đối tượng Paint để vẽ
    Paint drawPaint;
    // Nội dung văn bản nhãn
    String labelText;
    // Màu sắc văn bản nhãn
    int labelColor;
    // Kích thước văn bản nhãn
    int labelSize;
    // Tài nguyên hình ảnh
    Bitmap srcBitmap;
    // Chế độ hiển thị hình ảnh
    int scaleMode;
    
    int measuredWidth = 0;
    int measuredHeight = 0;
    
    public BannerView(Context context) {
        this(context, null);
    }

    public BannerView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public BannerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        // Lấy các thuộc tính tùy chỉnh đã khai báo
        TypedArray styledAttrs = context.getTheme().obtainStyledAttributes(
            attrs, R.styleable.BannerViewStyle, defStyleAttr, 0);
        int attrCount = styledAttrs.getIndexCount();
        
        for(int i = 0; i < attrCount; i++){
            int attrIndex = styledAttrs.getIndex(i);
            switch (attrIndex) {
                case R.styleable.BannerViewStyle_labelText:
                    labelText = styledAttrs.getString(attrIndex);
                    break;
                case R.styleable.BannerViewStyle_labelColor:
                    labelColor = styledAttrs.getColor(attrIndex, Color.RED);        
                    break;
                case R.styleable.BannerViewStyle_labelSize:
                    labelSize = styledAttrs.getDimensionPixelSize(attrIndex, 
                        (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,  
                        16, getResources().getDisplayMetrics())); 
                    break;
                case R.styleable.BannerViewStyle_srcImage:
                    srcBitmap = BitmapFactory.decodeResource(getResources(), 
                        styledAttrs.getResourceId(attrIndex, 0));
                    break;
                case R.styleable.BannerViewStyle_scaleMode:
                    scaleMode = styledAttrs.getInt(attrIndex, 0);
                    break;
            }
        }
        styledAttrs.recycle();
        
        bitmapArea = new Rect();
        drawPaint = new Paint();
        labelArea = new Rect();
        drawPaint.setTextSize(labelSize);
        // Tính toán kích thước vùng văn bản cần thiết
        drawPaint.getTextBounds(labelText, 0, labelText.length(), labelArea);
    }
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // Xác định chiều rộng
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        
        if(widthMode == MeasureSpec.EXACTLY){
            measuredWidth = widthSize;
        }else{
            // Chiều rộng dựa trên hình ảnh
            int desireByImage = getPaddingLeft() + getPaddingRight() + srcBitmap.getWidth();  
            // Chiều rộng dựa trên văn bản
            int desireByLabel = getPaddingLeft() + getPaddingRight() + labelArea.width();  
      
            if (widthMode == MeasureSpec.AT_MOST){
                int desire = Math.max(desireByImage, desireByLabel);
                measuredWidth = Math.min(desire, widthSize);
            }  
        }
        
        // Xác định chiều cao
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        
        if(heightMode == MeasureSpec.EXACTLY){
            measuredHeight = heightSize;
        }else{
            int desire = getPaddingTop() + getPaddingBottom() + 
                srcBitmap.getHeight() + labelArea.height();  
            if (heightMode == MeasureSpec.AT_MOST){
                measuredHeight = Math.min(desire, heightSize);
            }  
        }
        setMeasuredDimension(measuredWidth, measuredHeight);
    }
    
    @Override
    protected void onDraw(Canvas canvas) {
        // Vẽ đường viền khung
        drawPaint.setStrokeWidth(4);
        drawPaint.setStyle(Paint.Style.STROKE);
        drawPaint.setColor(Color.CYAN);
        canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), drawPaint);  
  
        bitmapArea.left = getPaddingLeft();  
        bitmapArea.right = measuredWidth - getPaddingRight();  
        bitmapArea.top = getPaddingTop();  
        bitmapArea.bottom = measuredHeight - getPaddingBottom();  
  
        drawPaint.setColor(labelColor);  
        drawPaint.setStyle(Style.FILL);  
        
        // Xử lý trường hợp văn bản dài hơn chiều rộng view
        if (labelArea.width() > measuredWidth) {  
            TextPaint textPaint = new TextPaint(drawPaint);  
            String truncatedText = TextUtils.ellipsize(labelText, textPaint, 
                (float) measuredWidth - getPaddingLeft() - getPaddingRight(),  
                TextUtils.TruncateAt.END).toString();  
            canvas.drawText(truncatedText, getPaddingLeft(), 
                measuredHeight - getPaddingBottom(), drawPaint);
        } else {  
            // Hiển thị văn bản giữa
            canvas.drawText(labelText, measuredWidth / 2 - labelArea.width() * 1.0f / 2, 
                measuredHeight - getPaddingBottom(), drawPaint);
        }  
  
        // Điều chỉnh vùng hình ảnh do văn bản chiếm phần bên dưới
        bitmapArea.bottom -= labelArea.height();  
  
        if (scaleMode == 0) {
            canvas.drawBitmap(srcBitmap, null, bitmapArea, drawPaint);  
        } else {  
            // Tính toán vùng hiển thị giữa màn hình
            bitmapArea.left = measuredWidth / 2 - srcBitmap.getWidth() / 2;  
            bitmapArea.right = measuredWidth / 2 + srcBitmap.getWidth() / 2;  
            bitmapArea.top = (measuredHeight - labelArea.height()) / 2 - srcBitmap.getHeight() / 2;  
            bitmapArea.bottom = (measuredHeight - labelArea.height()) / 2 + srcBitmap.getHeight() / 2;  
  
            canvas.drawBitmap(srcBitmap, null, bitmapArea, drawPaint);  
        } 
    }

}

Bước 3: Sử dụng BannerView trong file layout

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res/com.example.customview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >
    
    <com.example.customview.view.BannerView  
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content"  
        android:layout_margin="10dp"  
        android:padding="10dp"  
        app:srcImage="@drawable/ic_launcher"  
        app:scaleMode="center"  
        app:labelText="xin chao android !"  
        app:labelColor="#ff0000"  
        app:labelSize="30sp" />  
  
    <com.example.customview.view.BannerView 
        android:layout_width="100dp"  
        android:layout_height="wrap_content"  
        android:layout_margin="10dp"  
        android:padding="10dp"  
        app:srcImage="@drawable/ic_launcher"  
        app:scaleMode="center"  
        app:labelText="helloworldwelcome"  
        app:labelColor="#00ff00"  
        app:labelSize="20sp" />  
  
    <com.example.customview.view.BannerView  
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content"  
        android:layout_margin="10dp"  
        android:padding="10dp"  
        app:srcImage="@drawable/landscape"  
        app:scaleMode="center"  
        app:labelText="phong canh dep"  
        app:labelColor="#ff0000"  
        app:labelSize="12sp" />

</LinearLayout>

Phân tích chi tiết các phương thức quan trọng:

Phương thức khởi tạo với AttributeSet:

TypedArray styledAttrs = context.getTheme().obtainStyledAttributes(
    attrs, R.styleable.BannerViewStyle, defStyleAttr, 0);
int attrCount = styledAttrs.getIndexCount();

for(int i = 0; i < attrCount; i++){
    int attrIndex = styledAttrs.getIndex(i);
    switch (attrIndex) {
        case R.styleable.BannerViewStyle_labelText:
            labelText = styledAttrs.getString(attrIndex);
            break;
        // ... các case khác tương tự
    }
}
styledAttrs.recycle();

Đoạn code này có nhiệm vụ đọc các giá trị thuộc tính tùy chỉnh đã được khai báo trong attrs.xml và gán giá trị từ layout vào các biến tương ứng trong custom view.

Phương thức onMeasure() xử lý việc tính toán kích thước của view dựa trên các chế độ MeasureSpec.EXACTLY, AT_MOST, và unspecified. Phương thức onDraw() thực hiện công việc vẽ giao diện lên màn hình, bao gồm vẽ đường viền, hiển thị văn bản nhãn với xử lý cắt ngắn khi văn bản quá dài, và vẽ hình ảnh với hai chế độ fillXY và center.

Như vậy, chúng ta đã hoàn thành việc tạo một custom View cơ bản trong Android. Custom View này có thể hiển thị hình ảnh kèm nhãn văn bản bên dưới với khả năng tùy chỉnh màu sắc, kích thước và chế độ hiển thị.

Thẻ: Android custom-view view UI xml-layout

Đăng vào ngày 2 tháng 7 lúc 06:16