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

Thử cách 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 sắp xếp 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 (lần lượt đượ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, LinearLayout, FrameLayout, RelativeLayout, và các bố cục khác. Để biết thêm ví dụ, hãy xem phần 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 mình, 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ó, 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 riêng 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ử màn hình. Để giúp bạn nắm được thành phần điều khiển bạn có thể dùng với thành phần hiển thị tuỳ chỉnh, sau đây là một số ví dụ về những việc bạn có thể làm với các thành phần đó:

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

Các phần sau đây giải thích cách tạo khung hiển thị tuỳ chỉnh và cách sử dụng các khung hiển thị đó 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 thành phần View của riêng mình:

  1. Mở rộng một lớp hoặc lớp con View 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 để 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 các 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ị dựa trên lớp mở rộng đó.

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 cách bạn muốn. Có thể bạn cần một đồng hồ VU dạng đồ hoạ trông giống như một đồng hồ đo kim loại cũ, hoặc một khung hiển thị văn bản hát theo, trong đó một quả bóng nảy lên di chuyển theo lời khi bạn hát theo máy hát karaoke. Bạn có thể 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 theo 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ỉ bị giới hạn bởi trí tưởng tượng, kích thước màn hình và công suất xử lý có sẵn, lưu ý rằng ứng dụng của bạn có thể phải chạy trên một thiết bị nào đó có công suất thấp hơn đáng kể so với máy trạm của máy tính.

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

  • Khung hiển thị chung 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, đồng thời bạn 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 sắc 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ư 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 bạn muốn thành phần 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 làm gì cả và onMeasure() mặc định luôn đặt kích thước là 100x100 mà có thể bạn không muốn.
  • Nếu cần, bạn cũng có thể ghi đè các phương thức on khác.

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

Phương thức onDraw() cung cấp một Canvas mà bạn có thể triển khai mọi nội dung 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ành phần nào khác mà bạn có thể nghĩ đến.

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 hiển thị giữa thành phần và vùng chứa của thành phần đó. Bạn phải ghi đè onMeasure() để báo cáo hiệu quả và chính xác số liệu đo lường các phần chứa trong đó. Việc này phức tạp hơn một chút bởi các yêu cầu giới hạn từ phần tử mẹ (được truyền vào phương thức onMeasure()) và theo 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 ngoại lệ tại thời điểm đo lường.

Ở cấp độ cao, 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ố chiều rộng và chiều cao. Các thông số này được coi là yêu cầu đối với các hạn chế đối với số đo chiều rộng và chiều cao mà bạn tạo ra. Tham số widthMeasureSpecheightMeasureSpec đều là mã số nguyên đại diện cho các phương diện. Bạn có thể xem tài liệu tham khảo đầy đủ về loại hạn chế mà các thông số kỹ thuật này có thể yêu cầu trong tài liệu tham khảo tại mục View.onMeasure(int, int). Tài liệu tham khảo này cũng giải thích 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 tuân thủ 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ố kỹ thuật đó. Trong trường hợp này, phần tử mẹ có thể chọn việc cần làm, bao gồm cắt đoạn, cuộn, loại bỏ một trường hợp ngoại lệ hoặc yêu cầu onMeasure() thử lại, có thể là với các thông số đo lường khác.
  • Khi chiều rộng và chiều cao được tính toán, hãy gọi phương thức setMeasuredDimension(int width, int height) có số đo đã tính. Không thực hiện được điều này sẽ dẫn đến một ngoại lệ.

Dưới đây là bản tóm tắt các phương thức chuẩn khác mà khung này gọi trên 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. Dạng 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 khung hiển thị này phải gán một kích thước và vị trí cho tất cả các khung hiển thị 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 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 phức 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à tìm cách kết hợp một thành phần có thể tái sử dụng gồm một nhóm thành phần điều khiển hiện có, thì tốt nhất bạn nên tạo thành phần kết hợp (hoặc thành phần điều khiển kết hợp). Tóm lại, tính năng này tập hợp nhiều chế độ xem hoặc chế độ điều khiển nguyên tử hơn thành một nhóm mục hợp lý có thể được coi là một đối tượng duy nhất. Ví dụ: hộp kết hợp có thể là sự kết hợp giữa trường EditText dòng đơn và 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 nội dung nào đó trong danh sách, 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ể sử 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 về hộp kết hợp này 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 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 hộp kết hợp, bạn có thể sử dụng LinearLayout có hướng ngang. Bạn có thể lồng các bố cục khác vào bên trong, vì vậy, thành phần phức hợp có thể có cấu trúc và phức tạp 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 chuyể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ể kéo và sử dụng.
  • Nếu muốn, hãy tạo trình nghe cho những sự kiện mà các khung hiển thị được chứa của bạn có thể tạo ra. Ví dụ: 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 lựa chọn danh sách.
  • Bạn có thể tạo các thuộc tính của riêng mình bằng trình truy cập và đối tượng sửa đổi (không bắt buộc). Ví dụ: hãy đặt giá trị EditText ban đầu 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 này 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 người dùng 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ó nhiều lợi ích, bao gồm:

  • Bạn có thể chỉ định bố cục bằng cách sử dụng các tệp XML khai báo, giống như với màn hình hoạt động, hoặc bạn có thể tạo các thành phần hiển thị theo phương thức lập trình và lồng các thành phần này vào bố cục từ mã của bạn.
  • 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, vì vậy, bạn không phải ghi đè các phương thức này.
  • Bạn có thể nhanh chóng tạo các thành phần hiển thị phức hợp tuỳ ý và sử dụng lại như thể chúng là một thành phần duy nhất.

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

Nếu có một thành phần tương tự như thành phần bạn muốn, 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 với một thành phần được tuỳ chỉnh hoàn toàn, nhưng bằng cách bắt đầu với 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. Trong đó có việc mở rộng khung hiển thị EditText để tạo sổ tay có dòng chữ. Đây không phải là một ví dụ hoàn hảo và các API để thực hiện việc này có thể thay đổi, nhưng ví dụ này minh hoạ 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 về 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 xác định 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 ở chế độ 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 được gọi là "phương thức tổng hợp" cho phép truy cập vào dữ liệu của lớp mẹ. Điều này có nghĩa là lớp này hoạt động như một lớp riêng biệt thay vì một lớp liên quan chặt chẽ đến NoteEditor. Đây là cách gọn gàng hơn để tạo các lớp bên trong nếu các lớp đó không cần quyền truy cập vào trạng thái từ lớp bên ngoài. Tệp này giúp lớp được tạo có kích thước nhỏ và cho phép dễ dàng sử dụng trong các lớp khác.

    LinedEditText mở rộng EditText, đây là thành phần hiển thị để 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 một khung 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 nhận và truyền các lớp này đế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 thành phần hiển thị EditText. Canvas được chuyể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ương thức của lớp cấp cao phải được gọi. Trong trường hợp này, hãy gọi phương thức đó ở cuối sau khi bạn vẽ các dòng mà bạn muốn đưa vào.

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

    Bây giờ, 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 khung hiển thị chung trong XML và lớp được chỉ định bằng cách sử dụng gói đầy đủ. Lớp bên trong mà bạn định nghĩa được tham chiếu bằng ký hiệu NoteEditor$LinedEditText. Đây là cách tiêu chuẩn để tham chiếu đến các lớp bên trong trong 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 vào 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 định nghĩa này là các 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 truyền đến 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 tham số của riêng mình.

Việc tạo thành phần tuỳ chỉnh chỉ phức tạp theo mức bạn cầ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, đồng thời giới thiệu 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 đó. Hạn chế duy nhất là trí tưởng tượng của bạn và việc bạn cần thành phần này làm gì.