Ở 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:
- Khai báo các thuộc tính tùy chỉnh trong file attrs.xml trong thư mục values
- Tạo class kế thừa từ View và xử lý việc vẽ giao diện
- 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ị.