- Cùng tìm hiểu Canvas trong Android (Phần 1)
- Custom attributes view trong Android (Phần 2)
- Thêm animation cho custom view Android (serie Canvas phần 3)
Canvas trong Android và tạo Custom view
Chào mọi người, lâu rồi mình chưa viết bài viết mới vì lí do mình chưa kịp học, cũng chưa có trong đầu cái gì mà viết bài cả. Cùng tìm hiểu với mình một chủ đề khác trong Android đó là Canvas.
Bạn có thể tưởng tượng đơn giản như sau. Canvas giúp chúng ta vẽ những hình đơn giản lên không gian 2 chiều, cụ thể như một đoạn text, các điểm, hình vuông, chữ nhật, hình tròn, oval…
Các trường hợp sử dụng Canvas thì rất nhiều, đặc biệt khi tạo các customview, animation đẹp, chưa có thư viện nào có thì bạn có thể dùng Canvas.
Trong bài này mình xin giới thiệu cho các bạn các dùng và một số chú ý để giúp bạn nhanh hiểu hơn, vì mình mới vào cũng gặp mấy lỗi này.
Để vẽ một Canvas thì bạn phải đưa nó vào trong một view, do đó chúng ta cần tạo một class mới và thừa kết View hoặc EditText, TextView… vì đều thừa kế từ class View, tùy thuộc vào mục đích tạo ra view đó là gì. Đó là chúng ta đang tạo Custom view trong Android.
Trong project thì bạn tạo cho mình 1 class mang tên TestCanvas.
public class TestCanvas extends View { public TestCanvas(Context context) { super(context); } public TestCanvas(Context context, AttributeSet attrs) { super(context, attrs); } public TestCanvas(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public TestCanvas(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } }
Sau đó bạn thêm vào layout để có thể hiển thị được Custom view như là bạn thêm một component khác vào trong layout TextView, ImageView.
<com.fadbg.tuan.testcanvas.Canvas.TestCanvas android:layout_width="match_parent" android:layout_height="match_parent" />
Lưu ý: Trong bài này mình chỉ làm đơn giản nhất nên mình không định nghĩa thêm các attributtes khác, chỉ đơn giản là hiển thị thôi, ở bài sau mình sẽ làm rõ hơn về phần này.
Mỗi view này cần được vẽ lại bằng cách bạn overide hàm onDraw(Canvas canvas)
. Trước hiết bạn cần tìm hiểu về lifecycle của một View trong Android.
Nguồn proandroiddev.com
onMeasure
Trong hàm này các view sẽ biết được kích thước của nó, bạn cần xác định chính xác kích thước của view để có thể tính toán được các thành phần và vị trí trong view. Khi bạn thừa kế hàm này thì bạn phải gọi hàm setMeasuredDimension(int width, int height) để set lại kích thước cho view.
Trước hết bạn cần tính toán được kích thước mà bạn mong muốn cho view (desired size) bao gồm width và height. Sau đó trong onMeasure bạn sẽ có được kích thước của view đó và chế độ của từng kích thước (widthMode và heightMode).
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); }
Trong đo widthMode và heightMode có các giá trị đó là:
MeasureSpec.UNSPECIFIED: bạn có thể hiểu kích thước của view sẽ không bị giới hạn, bạn muốn nó lớn bao nhiêu nó sẽ lớn bấy nhiêu. Ví dụ view cha là match_parent hoặc wrap_content thì view con sẽ không hề bị giới hạn, view con to thì view cha cũng to theo.
MeasureSpec.EXACTLY: các layout bên ngoài đã giới hạn kích thước cho view đó, ví dụ view bên ngoài là RelativeLayout và có kích thước 150*150 thì Custom View cũng không thể có kích thước lớn hơn được mà vẫn phải bị bao bọc với kích thước đó (bao gồm cả padding).
MeasureSpec.AT_MOST: kích thước của view sẽ lớn đến một kích thước mà nó quy định.
int width; if (widthMode == MeasureSpec.EXACTLY) { width = widthSize; } else if (widthMode == MeasureSpec.AT_MOST) { width = Math.min(desiredWidth, widthSize); } else { width = desiredWidth; }
Và cuối cùng đưa width và height vào trong setMeasuredDimension để có thể xác định được kích thước cuối cùng của view, hãy kiểm tra các kích thước này xem có hợp lý và phù hợp với view của bạn hay không.
onDraw
Đây là hàm gần như quan trọng nhất, trong đây mình xin giới thiệu tiếp về Canvas và thêm một đối tượng mới không kém quan trọng để thiết kế view của bạn đó là Paint. Canvas thể hiện cách bạn vẽ cái gì, còn Paint thể hiện bạn vẽ như thế nào. Một ví dụ mà mình đọc được ngay từ bài hướng dẫn trên trang của developer android: Canvas cung cấp cho bạn phương thức để vẽ một đường thẳng còn Paint cho bạn cách tô màu nó, bạn vẽ một hình vuông, Paint sẽ cho bạn cách để tô màu cho nó hay có thể để nó trống bên trong, tô viền, đổ bóng…
Trước khi vẽ bất cứ cái gì thì bạn cần xác định sẵn một đối tượng Paint tương ứng với mỗi phần tử mà bạn sẽ vẽ.
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setColor(getResources().getColor(R.color.colorAccent));
Để vẽ một hình bất kỳ thì bên trong hàm onDraw bạn cần gọi các canvas.draw…. để vẽ hình tương ứng, cụ thể như sau:
drawText() để vẽ một text.
drawRect() để vẽ hình vuông, hình chữ nhật.
drawOval() để vẽ hình oval.
drawArc() để vẽ hình quạt.
drawBitmap() để vẽ ảnh bitmap.
… và rất nhiều hàm khác nữa, ở đây mình xin liệt kê những thứ cơ bản nhất thôi
Một lưu ý cực kỳ quan trọng. Hàm onDraw sẽ được gọi liên tục khi bạn thao tác bất kỳ gì khiến màn hình thay đổi, và để giữ nó luôn được mượt mà, cụ thể là 60 FPS thì bằng cách đưa các hàm khởi tạo này vào trong một hàm riêng và gọi nó trong constructor, để nó không phải khởi tạo đi khởi tạo lại nhiều lần, tăng đốc độ UI lên.
Bây giờ mình muốn vẽ một hình pacman phiên bản thấp nhất, demo như sau:
Bạn có thể thấy đây là một hình cánh quạt với một góc quét tầm 300 độ (trừ cái miệng chiếm 60 độ) do vậy mình sẽ dùng hàm drawArc() để vẽ.
Hàm drawArc() được định nghĩa như sau: drawArc(float left,
Từ từ, các kích thước left, top, right, bottom kia là gì, chắc chắn nếu bạn mới học canvas thì bạn sẽ khá bỡ ngỡ với mấy cái này.
Giả sử kích thước custom view của bạn như hình trên, với view của bạn sẽ nằm trong một khối hình chữ nhật gồm trục Oxy và hình bên trong là vị trí bạn cần vẽ.
Do đó, giả sử bạn cần có hình pacman này thì nó sẽ nằm trong hình vuông có kích thước cạnh là 300, và cách lề trái là 100, top là 100 thì bạn sẽ có các kích thước như sau:
int square = 300; float top = 100; float left = 100; float right = left + square; float bottom = top + square;
Trong canvas, trục tọa độ như hình bên dưới, cố gắng nhớ nhé mọi người.
Ta cần thêm góc bắt đầu và góc quay, theo như hình bên trên thì mình sẽ cho quay ở góc 30 độ và tới góc 330 độ, do đó góc bắt đầu sẽ là 30 và góc quay là 300 độ, truyền đầy đủ tham số vào thì kết quả bạn sẽ được như hình của mình (màu có thể khác nhé).
Bây giờ mình muốn tiếp tục vẽ mắt cho con pacman này, với mắt màu vàng (mình nghĩ màu vàng sẽ hợp).
Mắt mình làm đơn giản đó là một hình tròn và được tô màu vàng. Định nghĩa của hàm drawCircle() như sau: drawCircle (float cx, float cy, float radius, Paint paint) trong đó cx, cy là tâm của hình tròn, radius là bán kinh của hình tròn.
float cx = left + 100; float cy = top + 70; float radius = 25;
Cuối cùng ta được thành quả như sau:
Mình mới chỉ lấy ví dụ là các view này đang có kích thước xác định nhưng thực thế nó được sử dụng đi sử dụng lại tại nhiều vị trí trong app nên không thể có kích thước chính xác như trên được, do đó bạn hãy dùng hàm getHeight() và getWidth() để lấy ra kích thước view, sau đó dùng nó để tính toán ra kích thước các phần tử khác nhau.
Ví dụ bạn muốn cho hình con pacman này nằm trọn trong view, không hề có khoảng cách với viền thì bạn có thể đặt cho nó kích thước left = 0, top = 0, right = getWidth() và bottom = getWidth()… tương tự với hình mắt.
Trong bài sau mình sẽ đi kỹ hơn vào Canvas, và các hàm khác để có thể tạo custom view linh động hơn, chúc các bạn trở thành một designer kiêm coder đa tài.
Full code:
<com.fadbg.tuan.demoCanvas.TestCanvas android:layout_width="match_parent" android:layout_height="match_parent" />
public class TestCanvas extends View { Paint pacmanPaint; Paint eyeOfPacmanPaint; public TestCanvas(Context context) { super(context); init(); } public TestCanvas(Context context, AttributeSet attrs) { super(context, attrs); init(); } public TestCanvas(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } public TestCanvas(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(); } private void init() { pacmanPaint = new Paint(Paint.ANTI_ALIAS_FLAG); pacmanPaint.setColor(getResources().getColor(R.color.colorPrimary)); eyeOfPacmanPaint = new Paint(Paint.ANTI_ALIAS_FLAG); eyeOfPacmanPaint.setColor(getResources().getColor(R.color.colorAccent)); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int square = 300; float top = 100; float left = 100; float right = left + square; float bottom = top + square; canvas.drawArc(left, top, right, bottom, 30, 300, true, pacmanPaint); float cx = left + 180; float cy = top + 70; float radius = 25; canvas.drawCircle(cx, cy, radius, eyeOfPacmanPaint); } }