Tìm hiểu về các constructor của View trong Android

Như các bạn đã biết, 1 View cơ bản trong Android sẽ có 4 constructor, tuy nhiên chúng ta lại chưa hiểu rõ về việc từng param trong các constructor đó để làm gì? Hay chúng ta cần phải implement constructor nào khi subclass 1 View?

Tóm tắt

1 vài điểm quan trọng nếu bạn lười đọc:

  • Sử dụng View(Context) để khởi tạo View từ trong Java code.
  • Override View(Context, AttributeSet) khi inflate View từ file XML.
  • Không cần quan tâm đến 2 constructor còn lại vì chắc chắn là bạn không cần đến nó.

Các param trong constructor

Constructor có thể có nhiều nhất 4 param. Sơ lược qua thì:

  • Context: Được sử dụng ở rất nhiều chỗ trong View, dùng để access các resource.
  • AttributeSet: Các thuộc tính mà bạn thêm vào trong XML.
  • int defStyleAttr: Style mặc định để áp dụng vào View (được định nghĩa trong theme).
  • int defStyleResource: Style mặc định để áp dụng vào View, nếu defStyleAttr không được sử dụng.

Bên cạnh Context thì các param còn lại chỉ được sử dụng để setup các trạng thái khởi điểm của View dựa vào các thuộc tính trong XML (từ layout, style và theme).

Thuộc tính

Hãy bắt đầu bằng việc nhớ lại cách để định nghĩa các thuộc tính trong XML. Cùng xem 1 ví dụ cơ bản về việc tạo 1 ImageView:

<ImageView
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:src="@drawable/icon"/>

Bạn đã từng tự hỏi là layout_width, layout_height hay src ở đâu ra không? Nếu bạn từng làm 1 custom view thì chắc đã biết câu trả lời. Thật ra nó không phải tự có đâu mà chúng ta phải khai báo những thuộc tính này để hệ thống có thể xử lý thông qua <declare-styleable> (chúng ta có thể dùng luôn vì những lập trình viên của Google đã thêm những thuộc tính này vào framework rồi). Ví dụ src sẽ được khai báo như thế này:

<declare-styleable name="ImageView">
  <!-- Sets a drawable as the content of this ImageView. -->
  <attr name="src" format="reference|color" />
</declare-styleable>

Mỗi declare-styleable sẽ tạo ra 1 R.styleable.[tên] cộng với 1 R.styleable.[tên]_[thuộc tính] cho từng thuộc tính. Như ví dụ trên thì sẽ tạo ra R.styleable.ImageViewR.styleable.ImageView_src.

Những resource đó là cái gì? Về cơ bản R.styleable.[tên] là 1 array bao gồm tất cả các thuộc tính mà hệ thống sẽ dùng để tìm kiếm giá trị khi cần. Mỗi R.styleable.[tên]_[thuộc tính] chỉ là 1 index trong cái array đấy, để bạn có thể lấy tất cả thuộc tính 1 lúc hoặc tìm từng thuộc tính riêng lẻ.

AttributeSet

Đoạn code XML mà chúng ta viết ở trên sẽ được đưa cho View dưới dạng 1 AttributeSet.

Thường thì chúng ta sẽ không truy cập cái biến này trực tiếp, thay vào đó sẽ sử dụng Theme.obtainStyledAttributes(). Đó là bởi vì các thuộc tính thường sẽ phải giải quyết các reference và apply các style. Ví dụ nếu bạn định nghĩa [email protected]/MyStyle trong XML, cái hàm này sẽ đọc MyStyle và lấy các thuộc tính trong đó ra để ghép với các thuộc tính có sẵn. obtainStyledAttributes() sẽ trả về 1 TypedArray để chúng ta có thể sử dụng các thuộc tính.

Quá trình này có thể diễn ra như sau:

public ImageView(Context context, AttributeSet attrs) {
  TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ImageView, 0, 0);
  Drawable src = ta.getDrawable(R.styleable.ImageView_src);
  setImageDrawable(src);
  ta.recycle();
}

Trong trường hợp này, chúng ta đã đưa vào hàm obtainStyledAttributes 2 tham số. Tham số thứ nhất là AttributeSet - các thuộc tính từ XML. Tham số thứ 2 là 1 array R.styleable.ImageView, sẽ nói với hàm này là chúng ta cần extract những thuộc tính nào.

Với TypedArray lấy được từ hàm trên, giờ đây chúng ta có thể truy cập vào từng thuộc tính riêng biệt. Chúng ta cần phải dùng R.styleable.ImageView_src để lấy đúng index của thuộc tính này trong array.

Thường thì chúng ta sẽ extract nhiều thuộc tính 1 lúc. Implementation của 1 ImageView sẽ phức tạp hơn nhiều so với đoạn code sample ở trên (bởi vì ImageView sẽ cần phải biết rất nhiều các thuộc tính khác như scaleType, tint,..).

Default Style Attribute

Ban có thể đã nhận ra là tôi đã truyền 2 số 0 vào 2 tham số cuối của hàm obtainStyledAttributes(). 2 tham số này thực chất là 2 tham chiếu resource - defStyleAttrdefStyleRes. Tôi sẽ tập trung vào giải thích tham số đầu tiên:

defStyleAttr là tham số khó hiểu nhất trong hàm obtainStyledAttributes(). Dựa theo tài liệu trên trang chủ Android:

1 thuộc tính trong theme hiện tại có chứa 1 tham chiếu đến 1 style resource cung cấp các giá trị mặc định cho TypedArray.

Rất khó hiểu đúng không ạ. Nói cách khác thì, nó là 1 cách để có thể định nghĩa ra 1 style gốc cho tất cả các View cùng 1 loại. Ví dụ, bạn có thể set textViewStyle trong theme của mình nếu bạn muốn sửa tất cả TextView trong ứng dụng của bạn cùng 1 lúc. Nếu cái thuộc tính này không tồn tại, bạn sẽ phải sửa từng TextView 1 cách thủ công.

Hãy cùng xem nó hoạt động như thế nào, sử dụng TextView làm ví dụ:

Đầu tiên, nó là 1 thuộc tính (trong trường hợp này thì là R.attr.textViewStyle). Đây là đoạn code định nghĩa textViewStyle:

<resources>
  <declare-styleable name="Theme">

    <!-- ...snip... -->

    <!-- Default TextView style. -->
    <attr name="textViewStyle" format="reference" />

    <!-- ...etc... -->

  </declare-styleable>
</resource>

Chúng ta lại sử dụng declare-styleable, nhưng lần này là để định nghĩa thuộc tính có thể tồn tại trong theme. Vì thế chúng ta cho textViewStyle trở thành 1 reference - nghĩa là, giá trị của nó chỉ là 1 tham chiếu đến 1 resource. Trong trường hợp này, nó nên là 1 tham chiếu đến 1 style.

Tiếp theo chúng ta phải set textViewStyle vào theme hiện tại. Theme mặc định của Android nhìn như sau:

<resources>
  <style name="Theme">

    <!-- ...snip... -->

    <item name="textViewStyle">@style/Widget.TextView</item>

    <!-- ...etc... -->

  </style>
</resource>

Sau đó thì bạn phải set cái theme này vào Application hoặc 1 Activity, thường thì thông qua file manifest:

<activity
  android:name=".MyActivity"
  android:theme="@style/Theme"
  />

Giờ thì chúng ta có thể sử dụng nó trong hàm obtainStyledAttributes():

TypedArray ta = theme.obtainStyledAttributes(attrs, R.styleable.TextView, R.attr.textViewStyle, 0);

Kết qủa cuối cùng là bất cứ thuộc tính nào không được định nghĩa bởi AttributeSet sẽ được cung cấp bởi style mà textViewStyle tham chiếu đến.

Thật ra thì nếu không làm những việc quá advance thì bạn không cần biết những thứ này. Tôi chỉ liệt kê ra đây để bạn biết rằng Android framework cho phép bạn định nghĩa style gốc dành cho các View trong theme của bạn.

Default Style Resource

defStyleRes is much simpler than its sibling. It is just a style resource (i.e. @style/Widget.TextView). No complex indirection through the theme.

The attributes from the style in defStyleRes are applied only if defStyleAttr is undefined (either as 0 or it isn't set in the theme).

defStyleRes thì đơn giản hơn nhiều so với defStyleAttr. Nó chỉ là 1 style resource (ví dụ như @style/Widget.TextView).

Các thuộc tính lấy ra từ style trong defStyleRes sẽ được áp dụng trong trường hợp defStyleAttr không được định nghĩa (hoặc là set về 0, hoặc không set trong theme).

Độ ưu tiên

Chúng ta đã có rất nhiều cách để lấy được giá trị của 1 thuộc tính thông qua obtainStyledAttributes(). Sau đây là thứ tự ưu tiên của chúng, từ cao nhất tới thấp nhất:

  • Bất cứ 1 giá trị nào định nghĩa trong AttributeSet.
  • Style resource được định nghĩa trong AttributeSet.
  • Style mặc định cung cấp bởi defStyleAttr.
  • Style resource mặc định cung cấp bởi defStyleResource (nếu không có defStyleAttr).
  • Giá trị trong theme.

Nói cách khác, bất cứ 1 thuộc tính nào bạn set trực tiếp trong XML sẽ được sử dụng đầu tiên. Nhưng ngoài ra thì có rất nhiều chỗ mà những thuộc tính này có thể được lấy ra nếu bạn không tự mình set theme.

View constructors

Có tất cả 4 constructor, mỗi cái lại cho vào thêm 1 tham số:

View(Context)

View(Context, AttributeSet)

View(Context, AttributeSet, defStyleAttr)

View(Context, AttributeSet, defStyleAttr, defStyleRes)

Chú ý: constructor cuối cùng được thêm vào ở API 21, nên trừ khi bạn set minSdkVersion là 21 thì bạn không nên sử dụng nó (nếu bạn muốn dùng defStyleRes thì cứ gọi hàm obtainStyledAttributes() là được bởi vì nó luôn luôn được support). Thường thì bạn chỉ cần override 2 constructor đầu tiên, cái đầu là để khởi tạo View bằng code java, cái thứ 2 là khởi tạo View từ XML.

Tôi thường setup custom View như sau:

SomeView(Context context) {
  this(context, null);
}

SomeView(Context context, AttributeSet attrs) {
  // Gọi `super` để View tự setup 1 cách thích hợp
  super(context, attrs);

  // ...Setup View và handle các thuộc tính ở đây...
}

Trong constructor với 2 tham số, bạn có thể sử dụng obtainStyledAttributes() tùy ý. Cách nhanh nhất để implement 1 style mặc định đó là truyền vào defStyleRes cho nó; bằng cách đó bạn không cần phải trải nghiệm nỗi đau phải làm việc với defStyleAttr (bởi vì nó giống 1 công cụ hỗ trợ của framework hơn là chỉ dành cho 1 app đơn lẻ).

Qua bài viết này tôi hi vọng các bạn sẽ không chỉ hiểu hơn về các constructor trong View mà còn biết được các thuộc tính được lấy ra như thế nào trong quá trình khởi tạo View!

Nguồn bài viết: http://blog.danlew.net/2016/07/19/a-deep-dive-into-android-view-constructors/


All Rights Reserved