Tạo thành phần khung hiển thị tuỳ chỉnh

Thử cách sử dụng Compose
Jetpack Compose là bộ công cụ giao diện người dùng được đề xuất cho Android. Tìm hiểu cách sử dụng bố cục trong ứng dụng Compose.

Android cung cấp một mô hình được tạo thành phần tinh vi và mạnh mẽ để xây dựng giao diện người dùng, dựa trên các lớp bố cục cơ bản ViewViewGroup. Nền tảng này bao gồm nhiều lớp con ViewViewGroup được tạo sẵn (tương ứng được gọi là tiện ích và bố cục) mà bạn có thể dùng để tạo giao diện người dùng.

Một phần danh sách các tiện ích widget có sẵn bao gồmButton, TextView, EditText, ListView, CheckBox, RadioButton, Gallery Spinner, cũng như AutoCompleteTextView với mục đích đặc biệt hơn, ImageSwitcher, và TextSwitcher.

Trong số các bố cục có sẵn gồm có LinearLayout, FrameLayout, RelativeLayout, và các bố cục khác. Để biết thêm ví dụ, hãy xem phần Các bố cục phổ biến.

Nếu không có tiện ích hoặc bố cục tạo sẵn nào đáp ứng được nhu cầu của bạn, thì bạn có thể tạo lớp con View của riêng mình. Nếu chỉ cần thực hiện những điều chỉnh nhỏ đối với một tiện ích hoặc bố cục hiện có, thì bạn có thể tạo lớp con cho tiện ích hoặc bố cục đó và ghi đè các phương thức của tiện ích hoặc bố cục đó.

Việc tạo các lớp con View của riêng bạn sẽ giúp bạn kiểm soát chính xác giao diện và chức năng của một phần tử trên màn hình. Để đưa ra ý tưởng về quyền kiểm soát bạn có được với khung hiển thị tuỳ chỉnh, sau đây là một số ví dụ về những gì bạn có thể làm với các chế độ đó:

  • Bạn có thể tạo một loại View kết xuất tuỳ chỉnh hoàn toàn – ví dụ: nút "điều chỉnh âm lượng", kết xuất bằng đồ hoạ 2D, giống như một bảng điều khiển điện tử analog.
  • Bạn có thể kết hợp một nhóm các thành phần View thành một thành phần mới, chẳng hạn như để tạo thành một hộp kết hợp (kết hợp danh sách bật lên và trường văn bản nhập tự do), một thành phần điều khiển bộ chọn ngăn kép (một ngăn bên trái và bên phải có danh sách trong mỗi nơi bạn có thể chỉ định lại mục nằm trong danh sách nào), v.v.
  • Bạn có thể ghi đè cách kết xuất một thành phần EditText trên màn hình. Ứng dụng mẫu NotePad sử dụng hiệu ứng này để tạo một trang sổ tay có dòng chữ.
  • Bạn có thể ghi lại các sự kiện khác (như thao tác nhấn phím) và xử lý các sự kiện đó theo cách tuỳ chỉnh, chẳng hạn như khi chơi trò chơi.

Các phần sau đây giải thích cách tạo thành phần hiển thị tuỳ chỉnh và sử dụng chúng trong ứng dụng của bạn. Để biết thông tin tham khảo chi tiết, hãy xem lớp View.

Phương pháp cơ bản

Dưới đây là thông tin tổng quan cấp cao về những điều bạn cần biết để tạo các thành phần View của riêng mình:

  1. Mở rộng một lớp View hoặc lớp con hiện có bằng lớp của riêng bạn.
  2. Ghi đè một số phương thức từ lớp cha. Các phương thức của lớp cấp cao cần ghi đè bắt đầu bằng on, ví dụ: onDraw(), onMeasure()onKeyDown(). Điều này tương tự như các sự kiện on trong Activity hoặc ListActivity mà bạn ghi đè cho vòng đời và các chức năng hook khác.
  3. Sử dụng lớp mở rộng mới của bạn. Sau khi hoàn tất, bạn có thể sử dụng lớp tiện ích mới thay cho khung hiển thị mà lớp đó dựa trên.

Các thành phần được tuỳ chỉnh toàn bộ

Bạn có thể tạo các thành phần đồ hoạ được tuỳ chỉnh hoàn toàn, xuất hiện theo bất kỳ cách nào bạn muốn. Bạn có thể cần một đồng hồ VU dạng đồ hoạ trông giống như một chiếc đồng hồ đo analog cũ, hoặc một chế độ xem văn bản hát theo nhạc với quả bóng bật theo các từ khi bạn hát cùng máy hát karaoke. Có thể bạn muốn một thứ mà các thành phần tích hợp sẵn không làm được, bất kể bạn kết hợp chúng bằng cách nào.

May mắn là bạn có thể tạo các thành phần có giao diện và hoạt động theo bất kỳ cách nào bạn muốn, chỉ giới hạn theo trí tưởng tượng, kích thước màn hình và khả năng xử lý hiện có, nhưng hãy lưu ý rằng ứng dụng của bạn có thể phải chạy trên thiết bị có công suất thấp hơn đáng kể so với máy tính để bàn.

Để tạo một thành phần được tuỳ chỉnh hoàn toàn, hãy cân nhắc những điều sau:

  • Khung hiển thị tổng quát nhất mà bạn có thể mở rộng là View. Vì vậy, bạn thường bắt đầu bằng cách mở rộng khung hiển thị này để tạo siêu thành phần mới.
  • Bạn có thể cung cấp một hàm khởi tạo có thể lấy các thuộc tính và tham số từ XML. Bạn cũng có thể sử dụng các thuộc tính và tham số của riêng mình, chẳng hạn như màu và phạm vi của đồng hồ VU hoặc chiều rộng và độ giảm chấn của kim.
  • Bạn nên tạo trình nghe sự kiện, trình truy cập thuộc tính và đối tượng sửa đổi của riêng mình, cũng như tạo các hành vi tinh vi hơn trong lớp thành phần.
  • Bạn gần như chắc chắn muốn ghi đè onMeasure() và cũng có thể cần phải ghi đè onDraw() nếu muốn thành phần này hiển thị nội dung nào đó. Mặc dù cả hai đều có hành vi mặc định, nhưng onDraw() mặc định không có tác dụng gì và onMeasure() mặc định luôn đặt kích thước 100x100 mà bạn có thể không muốn.
  • Bạn cũng có thể ghi đè các phương thức on khác theo yêu cầu.

Mở rộng onDraw() và onMeasure()

Phương thức onDraw() cung cấp Canvas trên đó bạn có thể triển khai mọi thứ mình muốn: đồ hoạ 2D, các thành phần tiêu chuẩn hoặc tuỳ chỉnh khác, văn bản được tạo kiểu hoặc bất cứ thứ gì khác mà bạn có thể nghĩ ra.

onMeasure() có liên quan nhiều hơn một chút. onMeasure() là một phần quan trọng trong hợp đồng kết xuất giữa thành phần và vùng chứa của thành phần đó. onMeasure() phải được ghi đè để báo cáo hiệu quả và chính xác kết quả đo lường các phần chứa trong đó. Điều này phức tạp hơn một chút do các yêu cầu giới hạn từ thành phần mẹ (được truyền vào phương thức onMeasure()) và do yêu cầu gọi phương thức setMeasuredDimension() có chiều rộng và chiều cao đo được sau khi tính toán. Nếu bạn không gọi phương thức này từ một phương thức onMeasure() bị ghi đè, thì sẽ dẫn đến một trường hợp ngoại lệ tại thời điểm đo lường.

Nhìn chung, việc triển khai onMeasure() sẽ có dạng như sau:

  • Phương thức onMeasure() bị ghi đè được gọi với thông số kỹ thuật về chiều rộng và chiều cao. Những thông số này được coi là yêu cầu đối với quy định hạn chế về số đo chiều rộng và chiều cao mà bạn thực hiện. Tham số widthMeasureSpecheightMeasureSpec đều là mã số nguyên đại diện cho phương diện. Bạn có thể xem tài liệu tham khảo đầy đủ về các loại quy định hạn chế mà những quy cách này có thể yêu cầu trong tài liệu tham khảo thuộc phần View.onMeasure(int, int) Tài liệu tham khảo này cũng giải thích về toàn bộ hoạt động đo lường.
  • Phương thức onMeasure() của thành phần sẽ tính toán chiều rộng và chiều cao đo lường cần thiết để hiển thị thành phần. Thuộc tính này phải cố gắng không vượt quá các thông số kỹ thuật được truyền vào, mặc dù có thể vượt quá các thông số đó. Trong trường hợp này, thành phần mẹ có thể chọn việc cần làm, bao gồm cắt xén, cuộn, gửi một ngoại lệ hoặc yêu cầu onMeasure() thử lại, có thể là với các thông số kỹ thuật đo lường khác.
  • Khi tính chiều rộng và chiều cao, hãy gọi phương thức setMeasuredDimension(int width, int height) có các số đo đã tính toán. Nếu không thực hiện việc này, thì hệ thống sẽ ngoại lệ.

Dưới đây là phần tóm tắt về các phương thức chuẩn khác mà khung này gọi trên các khung hiển thị:

Danh mục Phương pháp Nội dung mô tả
Tạo Hàm khởi tạo Có một dạng hàm khởi tạo được gọi khi thành phần hiển thị được tạo từ mã và một dạng được gọi khi thành phần hiển thị đó được tăng cường từ tệp bố cục. Biểu mẫu thứ hai phân tích cú pháp và áp dụng các thuộc tính được xác định trong tệp bố cục.
onFinishInflate() Được gọi sau khi một khung hiển thị và tất cả các khung hiển thị con đều được tăng cường từ XML.
Bố cục onMeasure(int, int) Được gọi để xác định các yêu cầu về kích thước cho thành phần hiển thị này và tất cả các thành phần con.
onLayout(boolean, int, int, int, int) Được gọi khi thành phần hiển thị này phải gán một kích thước và vị trí cho tất cả thành phần con.
onSizeChanged(int, int, int, int) Được gọi khi kích thước của thành phần hiển thị này thay đổi.
Vẽ onDraw(Canvas) Được gọi khi khung hiển thị phải kết xuất nội dung.
Xử lý sự kiện onKeyDown(int, KeyEvent) Được gọi khi một sự kiện nhấn phím xuống xảy ra.
onKeyUp(int, KeyEvent) Được gọi khi một sự kiện nhả phím xảy ra.
onTrackballEvent(MotionEvent) Được gọi khi một sự kiện chuyển động bi xoay xảy ra.
onTouchEvent(MotionEvent) Được gọi khi một sự kiện chuyển động trên màn hình cảm ứng xảy ra.
Trọng tâm onFocusChanged(boolean, int, Rect) Được gọi khi thành phần hiển thị nhận hoặc mất tâm điểm.
onWindowFocusChanged(boolean) Được gọi khi cửa sổ chứa thành phần hiển thị nhận hoặc mất tâm điểm.
Đính kèm onAttachedToWindow() Được gọi khi thành phần hiển thị được đính kèm vào cửa sổ.
onDetachedFromWindow() Được gọi khi thành phần hiển thị được tách khỏi cửa sổ.
onWindowVisibilityChanged(int) Được gọi khi chế độ hiển thị của cửa sổ chứa khung hiển thị thay đổi.

Chế độ điều khiển kết hợp

Nếu bạn không muốn tạo một thành phần được tuỳ chỉnh hoàn toàn mà muốn kết hợp một thành phần có thể sử dụng lại bao gồm một nhóm các thành phần điều khiển hiện có, thì tốt nhất là bạn nên tạo một thành phần phức hợp (hay thành phần điều khiển phức hợp). Tóm lại, việc này sẽ tập hợp một số thành phần điều khiển hoặc khung hiển thị có khả năng phân tích cao hơn vào một nhóm các mục logic có thể được coi là một sự vật. Ví dụ: một hộp kết hợp có thể là sự kết hợp giữa trường EditText một dòng và một nút liền kề có danh sách cửa sổ bật lên đính kèm. Nếu người dùng nhấn vào nút và chọn một mục nào đó từ danh sách, thì thao tác đó sẽ điền vào trường EditText. Tuy nhiên, họ cũng có thể nhập trực tiếp nội dung nào đó vào EditText nếu muốn.

Trong Android, bạn có thể dùng 2 khung hiển thị khác để thực hiện việc này: SpinnerAutoCompleteTextView. Dù vậy, khái niệm này dành cho hộp kết hợp là một ví dụ điển hình.

Để tạo thành phần phức hợp, hãy làm như sau:

  • Tương tự như với Activity, hãy sử dụng phương pháp khai báo (dựa trên XML) để tạo các thành phần được chứa hoặc lồng các thành phần đó theo phương thức lập trình từ mã của bạn. Điểm xuất phát thông thường là một Layout thuộc loại nào đó, vì vậy, hãy tạo một lớp mở rộng Layout. Trong trường hợp một hộp kết hợp, bạn có thể sử dụng LinearLayout với hướng ngang. Bạn có thể lồng các bố cục khác bên trong để thành phần phức hợp có thể phức tạp và có cấu trúc tuỳ ý.
  • Trong hàm khởi tạo của lớp mới, hãy lấy bất kỳ tham số nào mà lớp cấp cao dự kiến rồi truyền các tham số đó đến hàm khởi tạo lớp cấp cao trước tiên. Sau đó, bạn có thể thiết lập các thành phần hiển thị khác để sử dụng trong thành phần mới. Đây là nơi bạn tạo trường EditText và danh sách bật lên. Bạn có thể đưa các thuộc tính và tham số của riêng mình vào XML mà hàm khởi tạo có thể lấy và sử dụng.
  • Bạn có thể tạo trình nghe cho những sự kiện mà các khung hiển thị có trong đó có thể tạo ra (không bắt buộc). Một ví dụ là phương thức trình nghe cho trình nghe lượt nhấp vào mục trong danh sách để cập nhật nội dung của EditText nếu một lựa chọn trong danh sách được thực hiện.
  • Nếu muốn, bạn có thể tạo các tài sản của riêng mình bằng trình truy cập và đối tượng sửa đổi. Ví dụ: ban đầu, hãy đặt giá trị EditText trong thành phần và truy vấn nội dung của thành phần đó khi cần.
  • Ghi đè onDraw()onMeasure() (không bắt buộc). Việc này thường không cần thiết khi mở rộng Layout, vì bố cục có hành vi mặc định có thể hoạt động tốt.
  • (Không bắt buộc) ghi đè các phương thức on khác, chẳng hạn như onKeyDown(), chẳng hạn như để chọn một số giá trị mặc định nhất định từ danh sách bật lên của hộp kết hợp khi nhấn vào một phím nhất định.

Việc sử dụng Layout làm cơ sở cho chế độ điều khiển tuỳ chỉnh có một số ưu điểm như sau:

  • Bạn có thể chỉ định bố cục bằng cách sử dụng các tệp XML khai báo, tương tự như với màn hình hoạt động, hoặc bạn có thể tạo các chế độ xem theo phương thức lập trình rồi lồng các chế độ xem đó vào bố cục từ mã của mình.
  • Phương thức onDraw()onMeasure(), cùng với hầu hết các phương thức on khác, đều có hành vi phù hợp nên bạn không phải ghi đè các phương thức đó.
  • Bạn có thể nhanh chóng tạo các thành phần hiển thị phức hợp có độ phức tạp tuỳ ý và sử dụng lại như thể một thành phần duy nhất.

Sửa đổi loại chế độ xem hiện có

Nếu có một thành phần tương tự như nội dung bạn muốn, thì bạn có thể mở rộng thành phần đó và ghi đè hành vi mà bạn muốn thay đổi. Bạn có thể làm mọi việc bằng một thành phần được tuỳ chỉnh hoàn toàn, nhưng bằng cách bắt đầu bằng một lớp chuyên biệt hơn trong hệ phân cấp View, bạn có thể nhận được một số hành vi thực hiện miễn phí những gì bạn muốn.

Ví dụ: ứng dụng mẫu NotePad minh hoạ nhiều khía cạnh của việc sử dụng nền tảng Android. Một trong số đó là mở rộng thành phần hiển thị EditText để tạo một sổ tay có dòng chữ. Đây không phải là một ví dụ hoàn hảo và các API để làm điều này có thể thay đổi, nhưng nó thể hiện các nguyên tắc.

Nếu bạn chưa thực hiện việc này, hãy nhập mẫu NotePad vào Android Studio hoặc xem nguồn bằng đường liên kết được cung cấp. Cụ thể, hãy xem định nghĩa của LinedEditText trong tệp NoteEditor.java.

Dưới đây là một số điểm cần lưu ý trong tệp này:

  1. Định nghĩa

    Lớp này được định nghĩa bằng dòng sau:
    public static class LinedEditText extends EditText

    LinedEditText được định nghĩa là một lớp bên trong trong hoạt động NoteEditor, nhưng lớp này là công khai để có thể được truy cập dưới dạng NoteEditor.LinedEditText từ bên ngoài lớp NoteEditor.

    Ngoài ra, LinedEditTextstatic, nghĩa là không tạo phương thức gọi là "phương thức tổng hợp" cho phép truy cập vào dữ liệu từ lớp mẹ. Điều này đồng nghĩa lớp này hoạt động như một lớp riêng biệt thay vì một lớp có liên quan chặt chẽ đến NoteEditor. Đây là một cách rõ ràng hơn để tạo các lớp bên trong nếu chúng không cần quyền truy cập vào trạng thái từ lớp bên ngoài. Điều này giúp giữ cho lớp được tạo có kích thước nhỏ và cho phép dễ dàng sử dụng từ các lớp khác.

    LinedEditText mở rộng EditText, đây là khung hiển thị cần tuỳ chỉnh trong trường hợp này. Khi bạn hoàn tất, lớp mới có thể thay thế cho thành phần hiển thị EditText thông thường.

  2. Khởi tạo lớp

    Như thường lệ, lớp cha sẽ được gọi trước tiên. Đây không phải là một hàm khởi tạo mặc định, mà là một hàm có tham số. EditText được tạo bằng các tham số này khi được tăng cường từ tệp bố cục XML. Do đó, hàm khởi tạo cần lấy và truyền chúng đến hàm khởi tạo lớp cấp cao.

  3. Phương thức bị ghi đè

    Ví dụ này chỉ ghi đè phương thức onDraw(), nhưng bạn có thể cần phải ghi đè các phương thức khác khi tạo các thành phần tuỳ chỉnh của riêng mình.

    Đối với mẫu này, việc ghi đè phương thức onDraw() cho phép bạn vẽ các đường màu xanh dương trên canvas khung hiển thị EditText. Canvas được truyền vào phương thức onDraw() bị ghi đè. Phương thức super.onDraw() được gọi trước khi phương thức này kết thúc. Phải gọi phương thức lớp cấp cao. Trong trường hợp này, hãy gọi hàm này ở cuối sau khi bạn vẽ các đường bạn muốn đưa vào.

  4. Thành phần tuỳ chỉnh

    Hiện bạn đã có thành phần tuỳ chỉnh, nhưng làm cách nào để sử dụng? Trong ví dụ về NotePad, thành phần tuỳ chỉnh được sử dụng trực tiếp từ bố cục khai báo, vì vậy, hãy xem note_editor.xml trong thư mục res/layout:

    <view xmlns:android="http://schemas.android.com/apk/res/android"
        class="com.example.android.notepad.NoteEditor$LinedEditText"
        android:id="@+id/note"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/transparent"
        android:padding="5dp"
        android:scrollbars="vertical"
        android:fadingEdge="vertical"
        android:gravity="top"
        android:textSize="22sp"
        android:capitalize="sentences"
    />
    

    Thành phần tuỳ chỉnh được tạo dưới dạng thành phần hiển thị chung trong XML và lớp này được chỉ định bằng cách sử dụng gói đầy đủ. Lớp bên trong mà bạn định nghĩa sẽ được tham chiếu bằng cách sử dụng ký hiệu NoteEditor$LinedEditText. Đây là cách thức tiêu chuẩn để tham chiếu đến các lớp bên trong bằng ngôn ngữ lập trình Java.

    Nếu thành phần khung hiển thị tuỳ chỉnh không được xác định là một lớp bên trong, bạn có thể khai báo thành phần khung hiển thị bằng tên phần tử XML và loại trừ thuộc tính class. Ví dụ:

    <com.example.android.notepad.LinedEditText
      id="@+id/note"
      ... />
    

    Xin lưu ý rằng lớp LinedEditText hiện là một tệp lớp riêng biệt. Khi lớp này được lồng trong lớp NoteEditor, kỹ thuật này sẽ không hoạt động.

    Các thuộc tính và tham số khác trong phần định nghĩa là những thuộc tính và tham số được truyền vào hàm khởi tạo thành phần tuỳ chỉnh, sau đó được chuyển tới hàm khởi tạo EditText. Vì vậy, đây cũng là các tham số mà bạn sử dụng cho khung hiển thị EditText. Bạn cũng có thể thêm các thông số của riêng mình.

Việc tạo các thành phần tuỳ chỉnh chỉ phức tạp theo nhu cầu của bạn.

Một thành phần tinh vi hơn có thể ghi đè nhiều phương thức on hơn nữa và ra mắt các phương thức trợ giúp riêng, giúp tuỳ chỉnh đáng kể các thuộc tính và hành vi của thành phần đó. Giới hạn duy nhất là trí tưởng tượng của bạn và những gì bạn cần thành phần đó làm.