[Android] Custom View

Bất cứ ai có smartphone ngày nay đều sử dụng app mỗi ngày. Và mặc dù app rất đa dạng nhưng nhìn chung giao diện hầu như là giống nhau về mặt thiết kế. Đó là lý do vì sao đa số khách hàng khi đặt làm app có những yêu cầu riêng về nhưng thiết kế giao diện đặc biệt và “không đụng hàng” các app khác để sản phẩm của họ là độc nhất và có dấu ấn riêng.

Nếu như một tính năng cụ thể yêu cầu cần có giao diện được tùy biến(customized) mà không có sẵn trong thư viện của SDK thì chúng ta buộc phải hoặc tìm các thư viện có hỗ trợ giao diện đó hoặc phải tự vẽ chúng!

Tất nhiên việc học vẽ custom view sẽ tốn nhiều thời gian để tìm hiểu và hoàn thành nhưng đây là việc làm cần thiết để chúng ta có thể hiểu sâu hơn về cách cài đặt và hoạt động của view trong android.

Và trên hết nếu không còn cách nào dễ hơn để giải quyết vấn đề khi làm một chức năng cụ thể thì custom view thực sự giúp bạn nhưng thứ sau:

  • Hiệu năng. Nếu bạn có nhiều views trong layout thì việc vẽ custom view thành một single view sẽ giúp layout nhẹ hơn và cải thiện hiệu năng của app
  • Nếu view có quá nhiều tầng(hierachy) thì sẽ khó khi bảo trì và cài đặt. Custom view giúp giảm tải việc thêm chồng view không cần thiết.

Bài viết này sẽ giúp bạn một chút tìm hiểu về custom view, về cấu trúc tổng thể, cách cài đặt và những tip để tránh những lỗi thường gặp.

Đầu tiên chúng ta sẽ tìm hiểu về View lifecycle. Không rõ vì lý do gì mà Google không cung cấp tài liệu chính thức về lifecycle của view, việc này cũng gây ra không ít hiểu lầm giữa các developer và điều đó thường gây ra các vấn đề về lỗi.

Constructor

Tất cả view đều bắt đầu vòng đời của nó từ một constructor. Constructor là nơi chúng ta khai báo và cung cấp nhưng thứ cần thiết cho việc vẽ : thiết lập các giá trị khởi đầu, các tham số mặc định, giá trị tính toán v..v..

Một trong những cách setup khác đó là sử dụng AttributeSet interface. Đây là cách tiếp cận phổ biến và dễ dàng trong Android bằng cách thiết lập các tham số static.

Đầu tiên ta cần tạo file attrs.xml theo đường dẫn như hình sau. Trong file này sẽ chứa các thuộc tính cài đặt cho các custom views. Như hình dưới đây thì ở trong file attrs.xml ta có một custom view tên PagerIndicatorView có một attribute là piv_count ở đây là số lượng của indicator và có kiểu integer.

Trong class CustomView extend từ View sẽ có nhiều constructor, ta lưu ý đến constructor có chứa tham số AttributeSet. Ta lấy danh sách các attribute từ đây và value của nó (được khai báo trong xml của view).

public PageIndicatorView(Context context, AttributeSet attrs) {
    super(context, attrs);
    TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.PageIndicatorView);
    int count = typedArray.getInt(R.styleable.PageIndicatorView_piv_count,0);
    typedArray.recycle();
}

Ghi nhớ:

  • Khi khai báo các attribute của custom views. Để tránh nhầm lẫn và conflict với các attribute name đã có sẵn trong SDK. Ta thường đặt thêm một prefix (thường là các chữ cái đầu của custom view) để dễ dàng cho việc tìm kiếm. Trong ví dụ trên là đặt attribute cho PagerIndicatorView nên prefix đơn giản sẽ là piv_.
  • Nếu sử dụng Android Studio, Lint sẽ khuyên bạn call recycle() khi đã xong việc với attribute. Lý do là để bỏ những rằng buộc không cần thiết đến với các dữ liệu không được sử dụng lại nữa (Vì mục đích của ta chỉ cần lấy được các giá trị của attribute).

OnAttachedToWindow

Sau khi parent view gọi addView(View) thì custom view sẽ được attach vào window. Ở giai đoạn này, custom view sẽ biết được vị trí các view ở xung quanh nó. Lúc này ta có thể findViewById được và lưu vào global reference (nếu cần).

OnMeasure

Lúc này custom view đang trong giai đoạn tìm ra kích thước(size) của nó. Đây là một method quan trọng, trong hầu hết trường hợp ta thường chỉ định(hoặc có thể can thiệp thay đổi) kích thước của custom view mong muốn khi hiển thị trên layout. Khi overriding method này, cần lưu ý đến method setMesuredDimension(int width, int height)

Khi tinh chỉnh kích thước của custom view ta cần lưu ý, custom view có thể đã có kích thước cụ thể được đặt ở trên file layout.xml hoặc được chỉ định bằng code. Để có thể tính toán được, ta cần thực hiện các bước sau:

  1. Tính toán kích thước mong muốn của custom view (width và height)
  2. Lấy MeasureSpec (size và mode của width,height) của custom view.
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);
}
  1. Check MeasureSpec mode được chỉ định và sửa lại kích thước theo kích thước mong muốn.
int width;
if (widthMode == MeasureSpec.EXACTLY) {
  width = widthSize;
} else if (widthMode == MeasureSpec.AT_MOST) {
  width = Math.min(desiredWidth, widthSize);
} else {
  width = desiredWidth;
}

Chú thích:

Ta có các giá trị của MeasureSpec như sau

  • MeasureSpec.EXACTLY : user đặt giá trị kích thước fixed cứng.
  • MeasureSpec.AT_MOST : giá trị kích thước của view được đặt lớn nhất có thể (match_parent) và có thể giới hạn bởi kích thước cụ thể (max_width, max_height).
  • MeasureSpec.UNSPECIFIED : bao kích thước của view bằng với content bên trong. Với giá trị này ta có thể đặt kích thước mong muốn đã tính toán phía trên.

Trước khi đặt các giá trị cuối cùng vào setMesuredDimension, cẩn thận thì ta nên kiểm tra các giá trị không phải là số âm để tránh xảy ra các lỗi khi preview layout.

OnLayout

Ở đây thực hiện việc chỉ định kích thước và vị trí các children view bên trong custom view.

OnDraw

Đây là nơi quan trọng nhất bài viết muốn nói đến, ta sử dụng CanvasPaint object để vẽ những gì bạn cần được thực hiện trong method này.

Canvas instance được nằm trong parameter của onDraw, nó đơn giản là để vẽ các hình khác nhau, còn Paint object sẽ chỉ định màu sắc (chung hơn là style) của hình đó. Nó được sử dụng hầu như mọi nơi đễ vẽ bất kì một đường thằng, hình vuông, tròn hay bất cứ hình gì…

Khi vẽ custom view, ta cần ghi nhớ một điều rằng onDraw sẽ được gọi rất nhiều lần. Khi có bất kì sự thay đổi nào, khi ta vuốt hay kéo ngang màn hình … view sẽ được vẽ lại. Chính vì vậy mà Android Studio khuyên rằng nên tránh khai báo khởi tạo Object trong method này mà thay vào đó nên tạo mới ở chỗ khác và gọi sử dụng nó.

Ghi nhớ

  • Luôn sử dụng lại object thay vì tạo mới trong onDraw. Đừng hy vọng IDE sẽ cảnh báo hết các trường hợp cho bạn. Ví dụ lint sẽ cảnh báo khi bạn tạo mới ojbject trong onDraw nhưng sẽ không cảnh báo khi bạn tạo mới object trong method được onDraw gọi đến.
  • Không hard code kích thước view khi vẽ. Để phòng việc developer khác sử dụng lại view đó nhưng cần kích thước khác, hãy vẽ view phụ thuộc vào kích thước nó có.

ViewUpdate Nhìn vào View lifecycle bạn sẽ thấy 2 method được sử dụng để tự nó thực hiện việc vẽ lại : invalidate()requestLayout() giúp bạn tương tác qua lại với custom view, bạn hoàn toàn có thể thay đổi view khi đang runtime. Nhưng tại sạo lại có tận 2 method ?

  • invalidate() sử dụng được vẽ lại các view đơn giản. Ví dụ khi bạn update lại text, color hay tương tác chạm điểm. Có nghĩa là view chỉ cần đơn giản gọi onDraw() để update lại trạng thái của view.
  • requestLayout() như bạn thấy trong sơ đồ lifecycle thì method này sẽ gọi lại view update từ onMeasure(). Điều đó có nghĩa là việc thực hiện vẽ lại view sẽ được tính toán lại kích thước. Kích thước mới có thể được tính lại ở onMeasure vẽ sẽ thực hiện vẽ theo kích thước mới đó.

Animation

Animation trong custom view là quá trình xử lí các frame liên tiếp. Có nghĩa là khi bạn muốn tạo một vòng tròn có bán kính di chuyển từ nhỏ đến lớn để tạo ra hình tròn thay đổi to nhỏ. Ở từng bước bạn cần tăng giá trị bán kính lên và gọi invalidate() để vẽ view mới.

Để làm animation custom view, ta có class ValueAnimator. Class này giúp ta thiết lập animation của view từ lúc bắt đầu và kết thúc, ngoài ra có còn hỗ trợ Interpolator (style animation có sẵn).

ValueAnimator animator = ValueAnimator.ofInt(0, 100);
animator.setDuration(1000);
animator.setInterpolator(new DecelerateInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
  public void onAnimationUpdate(ValueAnimator animation) {
    int newRadius = (int) animation.getAnimatedValue();
  }
});
animator.start();

Ghi nhớ

  • Đứng quên gọi invalidate() mỗi khi set giá trị animation mới.

Hy vọng bài viết này sẽ giúp bạn có kiến thức cơ bản để vẽ custom view. Bạn có thể tham khảo video này để biết thêm chi tiết.

Bài dịch từ nguồn : https://medium.com/@romandanylyk96/android-draw-a-custom-view-ef79fe2ff54b


All Rights Reserved