Tạo ra trải nghiệm người dùng chỉn chu hơn

1. Trước khi bắt đầu

Như bạn đã tìm hiểu trong các lớp học lập trình trước đây, Material là một hệ thống thiết kế do Google tạo ra với các nguyên tắc, thành phần và công cụ nhằm hỗ trợ các phương pháp hay nhất để thiết kế giao diện người dùng. Trong lớp học lập trình này, bạn sẽ cập nhật ứng dụng tính tiền boa (trong các lớp học lập trình trước) để cải thiện sự tinh tế trong trải nghiệm người dùng, thể hiện trong ảnh chụp màn hình cuối cùng bên dưới. Bạn cũng sẽ kiểm thử ứng dụng trong một số trường hợp bổ sung để đảm bảo người dùng có được trải nghiệm suôn sẻ nhất có thể.

5743ac5ee2493d7.png

Điều kiện tiên quyết

  • Quen thuộc với các tiện ích giao diện người dùng phổ biến như TextView, ImageView, Button, EditText, RadioButton, RadioGroupSwitch
  • Quen thuộc với ConstraintLayout cũng như cách dùng điều kiện ràng buộc để định vị khung nhìn con
  • Tự tin chỉnh sửa bố cục XML
  • Hiểu rõ sự khác biệt giữa hình ảnh bitmap và vectơ vẽ được
  • Có thể thiết lập thuộc tính giao diện trong một giao diện (theme)
  • Có thể bật chế độ Giao diện tối trên thiết bị
  • Từng chỉnh sửa tệp build.gradle của ứng dụng cho các phần phụ thuộc của dự án

Kiến thức bạn sẽ học được

  • Cách sử dụng Thành phần Material Design trong ứng dụng
  • Cách sử dụng biểu tượng Material trên ứng dụng bằng cách nhập qua Image Asset Studio
  • Cách tạo và áp dụng kiểu định dạng mới
  • Cách thiết lập các thuộc tính giao diện khác ngoài màu sắc

Sản phẩm bạn sẽ tạo ra

  • Một ứng dụng tính toán tiền boa chỉn chu, làm theo các phương pháp hay nhất được đề xuất về giao diện người dùng

Bạn cần có

  • Máy tính đã cài đặt phiên bản Android Studio ổn định mới nhất
  • Mã cho ứng dụng Tip Time hoàn thành trong các lớp học lập trình trước tại đường dẫn nàyđường dẫn này

2. Tổng quan về ứng dụng khởi động

Qua các lớp học lập trình trước, bạn đã xây dựng ứng dụng Tip Time. Đây là một ứng dụng tính tiền boa cho phép tuỳ chỉnh số tiền boa. Giao diện hiện tại của ứng dụng có dạng như ảnh chụp màn hình dưới đây. Chức năng ứng dụng vẫn hoạt động, nhưng giao diện thì như một bản thiết kế mẫu. Các trường thông tin chưa được sắp xếp một cách trực quan. Còn rất nhiều chỗ cần cải thiện, chẳng hạn như cách tạo kiểu và độ giãn cách cần nhất quán hơn cũng như cần sử dụng thành phần Material Design.

6685eaafba30960a.png

3. Thành phần Material

Thành phần Material (Material Component) là các tiện ích giao diện người dùng phổ biến, giúp bạn định kiểu Material cho ứng dụng dễ dàng hơn. Tài liệu này hướng dẫn cách sử dụng và tuỳ chỉnh các Thành phần Material Design. Có một số nguyên tắc chung về thiết kế Material Design cho mỗi thành phần cũng như hướng dẫn riêng cho các thành phần hiện có trên Android. Các sơ đồ có gắn nhãn sẽ cung cấp đầy đủ thông tin, giúp bạn tái tạo một thành phần nào đó nếu nền tảng bạn đã chọn chưa có thành phần đó.

c4a4db857bb36c3f.png

Khi bạn sử dụng Thành phần Material, ứng dụng của bạn sẽ hoạt động nhất quán hơn so với các ứng dụng khác trên thiết bị của người dùng. Nhờ đó, các mẫu giao diện người dùng đã học từ ứng dụng này có thể chuyển sang cho ứng dụng khác. Do đó, người dùng có thể tìm hiểu cách sử dụng ứng dụng của bạn nhanh hơn. Bạn nên sử dụng Thành phần Material bất cứ khi nào có thể (thay vì các tiện ích không phải Material). Các thành phần dễ tuỳ chỉnh và linh hoạt hơn. Bạn sẽ tìm hiểu về điều này trong nhiệm vụ tiếp theo.

Bạn cần đưa thư viện Thành phần Material Design (MDC) vào dự án dưới dạng một thành phần phụ thuộc. Dòng này phải hiển thị trong dự án của bạn theo mặc định. Trong tệp build.gradle của ứng dụng, hãy đảm bảo phần phụ thuộc này có trong phiên bản thư viện mới nhất. Để biết thêm thông tin, hãy xem trang Get started (Bắt đầu) trên trang web của Material.

app/build.gradle

dependencies {
    ...
    implementation 'com.google.android.material:material:<version>'
}

Trường văn bản

Trong ứng dụng tính tiền boa, ở phần trên cùng của bố cục, bạn sẽ thấy trường EditText dành cho chi phí dịch vụ. Trường EditText này vẫn hoạt động nhưng không tuân theo các nguyên tắc thiết kế mới đây của Material Design về hình thức cũng như hành vi của trường văn bản.

Nếu bạn muốn sử dụng thành phần mới nào đó, trước hết hãy tìm hiểu về thành phần đó trên trang web của Material. Trong hướng dẫn về Text Fields (Trường văn bản), có hai loại trường văn bản:

Trường văn bản được tô màu nền (filled text field)

29fab63417a5e9ed.png

Trường văn bản có đường viền (outlined text field)

3f085f837e146150.png

Để tạo trường văn bản như trình bày ở trên, hãy sử dụng TextInputLayout kèm theo TextInputEditText trong thư viện MDC. Trường văn bản Material có thể tuỳ chỉnh dễ dàng để:

  • Hiện văn bản nhập hoặc một nhãn luôn xuất hiện
  • Hiện một biểu tượng trong trường văn bản
  • Hiện trình trợ giúp hoặc thông báo lỗi

Trong nhiệm vụ đầu tiên của lớp học lập trình này, bạn sẽ thay thế phần chi phí dịch vụ EditText bằng một trường văn bản Material (bao gồm một TextInputLayoutTextInputEditText).

  1. Mở ứng dụng Tip Time (Tiền boa) trong Android Studio rồi chuyển đến tệp bố cục activity_main.xml. Tệp này chứa ConstraintLayout cho bố cục của phần tính tiền boa.
  2. Để xem ví dụ về định dạng XML cho trường văn bản Material, hãy xem lại hướng dẫn dành cho Android về Trường văn bản. Bạn sẽ thấy các đoạn mã như sau:
<com.google.android.material.textfield.TextInputLayout
    android:id="@+id/textField"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="@string/label">

    <com.google.android.material.textfield.TextInputEditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
    />

</com.google.android.material.textfield.TextInputLayout>
  1. Sau khi xem ví dụ này, hãy chèn trường văn bản Material vào trường con đầu tiên của ConstraintLayout (trước trường EditText). Bạn sẽ bỏ trường EditText trong một bước sau này.

Bạn có thể nhập nội dung này vào Android Studio và sử dụng tính năng tự động hoàn thành để nhập dễ dàng hơn. Hoặc bạn có thể sao chép nội dung XML mẫu qua trang tài liệu rồi dán vào bố cục như dưới đây. Hãy lưu ý TextInputLayout có một khung nhìn con là TextInputEditText. Hãy nhớ rằng dấu ba chấm (...) được dùng để viết tắt đoạn trích, giúp bạn tập trung vào các dòng XML thực sự cần thay đổi.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    ...>

    <com.google.android.material.textfield.TextInputLayout
        android:id="@+id/textField"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="@string/label">

        <com.google.android.material.textfield.TextInputEditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
        />

    </com.google.android.material.textfield.TextInputLayout>

    <EditText
        android:id="@+id/cost_of_service" ... />

    ...

Theo dự kiến, bạn sẽ thấy lỗi trên thành phần TextInputLayout. Bạn chưa ràng buộc đúng khung hiển thị này trong thành phần mẹ ConstraintLayout. Ngoài ra cũng chưa nhận dạng được tài nguyên chuỗi. Bạn sẽ khắc phục những lỗi này trong các bước sắp tới.

344a98d866c7f68c.png

  1. Thêm điều kiện ràng buộc theo chiều dọc và chiều ngang vào trường văn bản để định vị chính xác trong ConstraintLayout gốc. Vì bạn chưa xoá trường EditText, hãy cắt và dán các thuộc tính sau từ EditText rồi đặt vào TextInputLayout: điều kiện ràng buộc, mã nhận dạng tài nguyên cost_of_service, chiều rộng bố cục 160dp, chiều cao bố cục wrap_content và văn bản gợi ý @string/cost_of_service.
...

<com.google.android.material.textfield.TextInputLayout
   android:id="@+id/cost_of_service"
   android:layout_width="160dp"
   android:layout_height="wrap_content"
   android:hint="@string/cost_of_service"
   app:layout_constraintStart_toStartOf="parent"
   app:layout_constraintTop_toTopOf="parent">

   <com.google.android.material.textfield.TextInputEditText
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>

</com.google.android.material.textfield.TextInputLayout>

...

Có thể bạn sẽ thấy lỗi mã nhận dạng cost_of_service giống với mã nhận dạng tài nguyên của EditText. Tuy nhiên, hiện tại bạn có thể bỏ qua lỗi này. (EditText sẽ được xoá trong vài bước tới).

  1. Tiếp theo, hãy đảm bảo thành phần TextInputEditText có tất cả thuộc tính phù hợp. Cắt và dán kiểu nhập từ EditText vào TextInputEditText. Thay đổi mã tài nguyên của phần tử TextInputEditText thành cost_of_service_edit_text.
<com.google.android.material.textfield.TextInputLayout ... >

   <com.google.android.material.textfield.TextInputEditText
       android:id="@+id/cost_of_service_edit_text"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:inputType="numberDecimal" />

</com.google.android.material.textfield.TextInputLayout>

Chiều rộng của match_parent và chiều cao của wrap_content như hiện tại không có vấn đề gì. Khi thiết lập chiều rộng của match_parent, TextInputEditText sẽ có cùng chiều rộng với bố cục TextInputLayout gốc là 160dp.

  1. Giờ đây khi bạn đã sao chép toàn bộ thông tin liên quan từ EditText, hãy tiếp tục và xoá EditText khỏi bố cục.
  2. Trong chế độ xem Design (Thiết kế) của bố cục, bạn sẽ thấy bản xem trước dưới đây. Trường chi phí dịch vụ hiện tại trông giống một trường văn bản Material.

28582b8e4103beeb.png

  1. Bạn chưa thể chạy ứng dụng vì có lỗi xảy ra trong tệp MainActivity.kt, ở phương thức calculateTip(). Hãy nhớ lại một lớp học lập trình trước đây khi dự án của bạn đã bật tính năng liên kết khung nhìn, Android sẽ tạo các thuộc tính trong đối tượng liên kết dựa trên tên mã nhận dạng tài nguyên. Trường để truy xuất chi phí dịch vụ đã thay đổi trong bố cục XML, do đó bạn cần cập nhật lại mã Kotlin cho phù hợp.

Bây giờ, bạn sẽ truy xuất thông tin đầu vào của người dùng qua thành phần TextInputEditText có mã nhận dạng tài nguyên cost_of_service_edit_text. Trong MainActivity, hãy sử dụng binding.costOfServiceEditText để truy cập chuỗi văn bản được lưu trữ trong đó. Phần còn lại của phương thức calculateTip() có thể giữ nguyên.

private fun calculateTip() {
    // Get the decimal value from the cost of service text field
    val stringInTextField = binding.costOfServiceEditText.text.toString()
    val cost = stringInTextField.toDoubleOrNull()

    ...
}
  1. Tuyệt vời! Bây giờ, hãy chạy ứng dụng và kiểm tra để đảm bảo ứng dụng vẫn hoạt động tốt. Hãy lưu ý cách xuất hiện của "Cost of Service" ("Chi phí dịch vụ") phía trên thông tin đầu vào khi bạn nhập. Tiền boa vẫn sẽ được tính như dự kiến.

b4a27e58f63417b7.png

Nút chuyển

Trong nguyên tắc thiết kế Material Design cũng có hướng dẫn về nút chuyển (switch). Nút chuyển là một tiện ích dùng để bật hoặc tắt một chế độ cài đặt nào đó.

  1. Hãy tham khảo hướng dẫn cho Android về nút chuyển của Material. Bạn sẽ tìm hiểu về tiện ích SwitchMaterial (trong thư viện MDC). Tiện ích này sẽ cung cấp tính năng tạo kiểu Material cho các nút chuyển. Nếu tiếp tục xem qua hướng dẫn này, bạn sẽ thấy một số XML mẫu.
  2. Để sử dụng SwitchMaterial, bạn phải chỉ định rõ SwitchMaterial trong bố cục và sử dụng tên đường dẫn đủ điều kiện.

Trong bố cục activity_main.xml, hãy thay đổi thẻ XML từ Switch sang com.google.android.material.switchmaterial.SwitchMaterial.

...

<com.google.android.material.switchmaterial.SwitchMaterial
    android:id="@+id/round_up_switch"
    android:layout_width="0dp"
    android:layout_height="wrap_content" ... />

...
  1. Chạy ứng dụng để kiểm tra xem ứng dụng còn biên dịch được không. Bạn sẽ thấy ứng dụng chưa có gì thay đổi. Tuy nhiên, việc sử dụng SwitchMaterial trong thư viện MDC (thay vì Switch trong nền tảng Android) sẽ mang lại lợi ích cho bạn. Đó là khi thư viện SwitchMaterial được cập nhật (ví dụ: thay đổi nguyên tắc thiết kế Material Design), bạn sẽ được sử dụng miễn phí tiện ích đã cập nhật mà không cần thay đổi gì. Điều này giúp ích cho ứng dụng của bạn trong tương lai.

Đến đây bạn đã nhìn thấy hai ví dụ về những lợi ích khi sử dụng các Thành phần Material Design độc đáo để thiết kế giao diện người dùng cũng như cách thức để ứng dụng của bạn phù hợp hơn với các nguyên tắc của Material Design. Đừng quên rằng bạn có thể khám phá các Thành phần Material Design khác được cung cấp cho Android tại trang web này.

4. Biểu tượng

Biểu tượng (icon) là các ký hiệu giúp người dùng hiểu giao diện người dùng qua việc truyền đạt chức năng dự định dưới dạng hình ảnh trực quan. Biểu tượng thường lấy cảm hứng từ đối tượng trong thế giới thực người dùng thường bắt gặp. Thiết kế biểu tượng thường giảm tối đa mức độ chi tiết để tạo cảm giác quen thuộc cho người dùng. Ví dụ: trong thế giới thực, bút chì dùng để viết, vì thế biểu tượng bút chì thường biểu thị việc tạo, thêm hoặc chỉnh sửa một mục.

Chiếc bút chì được chuốt nhọn bằng gọt bút chì trên một cuốn vở đang mở. Ảnh chụp của Angelina Litvin trên Unsplash

Biểu tượng bút chì đen trắng

Đôi khi biểu tượng còn được liên kết với các đối tượng đã lỗi thời trong thế giới thực, chẳng hạn như biểu tượng chiếc đĩa mềm. Đây là biểu tượng phổ biến, biểu thị cho việc lưu tệp hoặc bản ghi cơ sở dữ liệu; tuy nhiên, đĩa mềm chỉ phổ biến vào những năm 1970 và dần biến mất sau năm 2000. Tuy nhiên, biểu tượng này vẫn được sử dụng cho đến ngày nay, là một minh chứng cho thấy một hình ảnh mạnh mẽ có thể vượt trên dạng thức vật chất của nó.

Chiếc đĩa mềm nằm phẳngẢnh chụp của Vincent Botta trên UnSplash

Biểu tượng đĩa mềm

Biểu tượng đại diện trong ứng dụng

Đối với biểu tượng trong ứng dụng, thay vì cung cấp phiên bản hình ảnh bitmap riêng cho từng mật độ màn hình, bạn nên sử dụng các vectơ vẽ được. Vectơ vẽ được thường được biểu thị dưới dạng tệp XML. Tệp này lưu trữ hướng dẫn về cách tạo hình ảnh thay vì lưu các pixel thực tế tạo nên hình ảnh đó. Bạn có thể tăng hoặc giảm tỷ lệ các vectơ vẽ được mà không ảnh hưởng đến chất lượng hình ảnh hay làm tăng kích thước tệp.

Biểu tượng được cung cấp

Material Design cung cấp một số biểu tượng được sắp xếp trong các danh mục phổ biến, đáp ứng được hầu hết nhu cầu của bạn. Xem danh sách biểu tượng.

bfdb896506790c69.png

Các biểu tượng này có thể được tô màu và vẽ bằng một trong 5 giao diện (Lấp đầy, Đường viền, Bo tròn góc, Hai tông màu và Sắc cạnh).

Lấp đầy (filled)

Đường viền (outlined)

Bo tròn góc (rounded)

Hai tông màu (two-tone)

Sắc cạnh (sharp)

Thêm biểu tượng

Trong nhiệm vụ này, bạn sẽ thêm vào ứng dụng 3 biểu tượng vectơ vẽ được sau đây:

  1. Biểu tượng bên cạnh trường văn bản chi phí dịch vụ
  2. Biểu tượng bên cạnh câu hỏi về chất lượng dịch vụ
  3. Biểu tượng bên cạnh lời nhắc làm tròn tiền boa

Dưới đây là ảnh chụp màn hình của phiên bản hoàn thiện của ứng dụng. Sau khi thêm các biểu tượng, bạn sẽ điều chỉnh bố cục cho phù hợp với vị trí của các biểu tượng này. Hãy để ý việc các trường và nút tính toán được dịch chuyển sang phải khi bổ sung các biểu tượng này.

8c4225390dd1fb20.png

Thêm thành phần vectơ vẽ được

Bạn có thể trực tiếp sử dụng Asset Studio trong Android Studio để tạo những biểu tượng này dưới dạng vectơ vẽ được.

  1. Mở thẻ Resource Manager (Trình quản lý tài nguyên) ở bên trái của cửa sổ ứng dụng.
  2. Nhấp vào biểu tượng + rồi chọn Vector Asset (Thành phần vectơ).

6dabda0f4bc1f6ed.png

  1. Đối với Asset Type (Loại thành phần), hãy nhớ chọn nút chọn gắn nhãn Clip Art (Hình mẫu).

914786d2d8b4025.png

  1. Nhấp vào nút bên cạnh Clip Art: (Hình mẫu:) để chọn một hình mẫu khác. Khi lời nhắc xuất hiện, hãy nhập "call made" ("thực hiện cuộc gọi") vào cửa sổ đang hiện. Bạn sẽ sử dụng biểu tượng mũi tên này cho chế độ làm tròn tiền boa. Chọn biểu tượng này rồi nhấp OK.

e7f607e4f576d75c.png

  1. Đổi tên biểu tượng thành ic_round_up. (Bạn nên sử dụng tiền tố ic_ khi đặt tên tệp biểu tượng.) Bạn có thể để Size (kích thước) là 24 dp x 24 dp và Color (màu) là màu đen 000000.
  2. Nhấp vào Tiếp theo.
  3. Chấp nhận vị trí thư mục mặc định rồi nhấp vào Finish (Hoàn tất).

200aed40ee987672.png

  1. Lặp lại các bước từ 2 đến 7 cho hai biểu tượng còn lại:
  • Biểu tượng câu hỏi về chất lượng dịch vụ: Tìm biểu tượng "room service" ("dịch vụ phòng") rồi lưu lại thành ic_service.
  • Biểu tượng chi phí dịch vụ: Tìm kiếm biểu tượng "store" ("cửa hàng") rồi lưu lại thành ic_store.
  1. Sau khi hoàn tất, giao diện Resource Manager (Trình quản lý tài nguyên) sẽ trông như ảnh chụp màn hình dưới đây. Bạn cũng có 3 tệp vectơ vẽ được này (ic_round_up, ic_serviceic_store) trong thư mục res/drawable.

c2d8b22f0fb55ce0.png

Hỗ trợ phiên bản Android cũ

Bạn vừa thêm các vectơ vẽ được vào ứng dụng, nhưng bạn phải lưu ý rằng vectơ vẽ được chỉ được hỗ trợ trên nền tảng Android 5.0 (cấp độ API 21) trở lên.

Dựa trên cách thiết lập dự án, phiên bản SDK tối thiểu cho ứng dụng Tip Time là API 19. Tức là ứng dụng có thể hoạt động trên các thiết bị Android chạy nền tảng Android phiên bản 19 trở lên.

Để ứng dụng hoạt động trên các phiên bản Android cũ hơn (còn gọi là khả năng tương thích ngược), hãy thêm thành phần vectorDrawables vào tệp build.gradle của ứng dụng. Nhờ vậy, bạn có thể sử dụng vectơ vẽ được trên các phiên bản nền tảng thấp hơn API 21 mà không cần chuyển đổi sang PNG khi xây dựng dự án. Tìm hiểu thêm chi tiết tại đây.

app/build.gradle

android {
  defaultConfig {
    ...
    vectorDrawables.useSupportLibrary = true
   }
   ...
}

Khi dự án đã được định cấu hình chính xác, bây giờ bạn có thể chuyển sang phần thêm biểu tượng vào bố cục.

Chèn biểu tượng và thành phần vị trí

Bạn sẽ sử dụng ImageViews để hiện biểu tượng trong ứng dụng. Đây là giao diện người dùng hoàn thiện của bạn.

5ed07dfeb648bd62.png

  1. Mở bố cục activity_main.xml.
  2. Đầu tiên, hãy đặt biểu tượng cửa hàng bên cạnh trường văn bản chi phí dịch vụ. Chèn một ImageView mới làm thành phần con đầu tiên của ConstraintLayout, trước TextInputLayout.
<androidx.constraintlayout.widget.ConstraintLayout
   ...>

   <ImageView
       android:layout_width=""
       android:layout_height=""

   <com.google.android.material.textfield.TextInputLayout
       android:id="@+id/cost_of_service"
       ...
  1. Thiết lập các thuộc tính thích hợp trên ImageView để giữ biểu tượng ic_store. Đặt mã nhận dạng thành icon_cost_of_service. Đặt thuộc tính app:srcCompat thành tài nguyên có thể vẽ @drawable/ic_store và bạn sẽ thấy bản xem trước của biểu tượng này bên cạnh dòng XML đó.

Ngoài ra, hãy đặt android:importantForAccessibility="no" vì hình ảnh này chỉ dùng để trang trí.

<ImageView
    android:id="@+id/icon_cost_of_service"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:importantForAccessibility="no"
    app:srcCompat="@drawable/ic_store" />

Theo dự kiến, sẽ có lỗi trên ImageView vì khung nhìn chưa được ràng buộc phù hợp. Bạn sẽ khắc phục lỗi này trong bước tiếp theo.

  1. Xác định vị trí icon_cost_of_service trong 2 bước. Trước tiên, hãy thêm điều kiện ràng buộc vào ImageView (bước này), sau đó cập nhật điều kiện ràng buộc trên TextInputLayout ở bên cạnh (bước 5). Sơ đồ này thể hiện cách thiết lập điều kiện ràng buộc.

d982b1b1f0131630.png

Trên ImageView, bạn muốn cạnh bắt đầu của ứng dụng ràng buộc với cạnh bắt đầu của khung nhìn gốc (app:layout_constraintStart_toStartOf="parent").

Biểu tượng xuất hiện ở giữa theo chiều dọc so với trường văn bản bên cạnh, vì vậy, hãy ràng buộc phần trên cùng của ImageView (layout_constraintTop_toTopOf) này với phần trên cùng của trường văn bản. Cố định phần đáy của ImageView (layout_constraintBottom_toBottomOf) này phần đáy của trường văn bản. Để tham chiếu đến trường văn bản, hãy sử dụng mã nhận dạng tài nguyên @id/cost_of_service. Khi 2 điều kiện ràng buộc được áp dụng trên một tiện ích trong cùng một kích thước (chẳng hạn như ràng buộc trên cùng và dưới cùng), các điều kiện ràng buộc sẽ được mặc định áp dụng như nhau. Kết quả là biểu tượng này sẽ được căn giữa theo chiều dọc, tương quan với trường chi phí dịch vụ.

<ImageView
    android:id="@+id/icon_cost_of_service"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:importantForAccessibility="no"
    app:srcCompat="@drawable/ic_store"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="@id/cost_of_service"
    app:layout_constraintBottom_toBottomOf="@id/cost_of_service" />

Biểu tượng và trường văn bản vẫn còn chồng chéo trong chế độ xem Design (Thiết kế). Tình trạng này sẽ được khắc phục trong bước tiếp theo.

  1. Trước khi thêm biểu tượng, trường văn bản nằm ở vị trí bắt đầu của thành phần mẹ. Bây giờ, cần dịch chuyển biểu tượng sang phải. Hãy cập nhật các điều kiện ràng buộc trên trường văn bản cost_of_service theo tương quan với icon_cost_of_service.

bb55ea0cddaa2a12.png

Cạnh bắt đầu của TextInputLayout phải được cố định với cạnh cuối của ImageView (@id/icon_cost_of_service). Để thêm khoảng cách giữa hai khung nhìn này, hãy thêm khoảng cách lề bắt đầu 16dp cho TextInputLayout.

<com.google.android.material.textfield.TextInputLayout
    android:id="@+id/cost_of_service"
    ...
    android:layout_marginStart="16dp"
    app:layout_constraintStart_toEndOf="@id/icon_cost_of_service">

    <com.google.android.material.textfield.TextInputEditText ... />

</com.google.android.material.textfield.TextInputLayout>

Sau khi hoàn thành tất cả thay đổi này, biểu tượng phải được đặt đúng vị trí ở bên cạnh trường văn bản.

23dcae5c3931903f.png

  1. Tiếp theo, hãy chèn biểu tượng chuông dịch vụ bên cạnh "How was the service?" ("Bạn thấy thế nào về dịch vụ?") TextView. Tuy bạn có thể khai báo ImageView ở bất kỳ đâu trong ConstraintLayout, nhưng bố cục XML sẽ dễ đọc hơn nếu bạn chèn ImageView mới vào bố cục XML sau TextInputLayout nhưng trước TextView service_question.

Với ImageView mới, hãy gán mã nhận dạng tài nguyên @+id/icon_service_question. Đặt các điều kiện ràng buộc phù hợp trên ImageView và phần câu hỏi dịch vụ TextView.

38c2dcb4cb18b5a.png

Ngoài ra, hãy thêm khoảng cách lề trên (top margin) 16dp cho service_question TextView để có thêm khoảng trống theo chiều dọc giữa phần câu hỏi dịch vụ và trường văn bản về chi phí dịch vụ ở phía trên.

...

   <ImageView
        android:id="@+id/icon_service_question"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:importantForAccessibility="no"
        app:srcCompat="@drawable/ic_service"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@id/service_question"
        app:layout_constraintBottom_toBottomOf="@id/service_question" />

    <TextView
        android:id="@+id/service_question"
        ...
        android:layout_marginTop="16dp"
        app:layout_constraintStart_toStartOf="@id/cost_of_service"
        app:layout_constraintTop_toBottomOf="@id/cost_of_service"/>

...
  1. Lúc này, chế độ xem Design (Thiết kế) sẽ có dạng như sau. Trường chi phí dịch vụ và câu hỏi về chất lượng dịch vụ (cũng như các biểu tượng tương ứng của dịch vụ) trông rất tuyệt, nhưng hiện không còn chỗ để hiện các nút chọn. Những nút chọn này chưa được căn chỉnh theo chiều dọc với phần nội dung ở phía trên.

578834f5bd3a2d2a.png

  1. Điều chỉnh vị trí của các nút chọn bằng cách dịch chuyển các nút này sang phải, bên dưới câu hỏi về chất lượng dịch vụ. Tức là bạn cần cập nhật điều kiện ràng buộc cho RadioGroup. Ràng buộc cạnh bắt đầu của RadioGroup với cạnh bắt đầu của service_question TextView. Tất cả thuộc tính khác trên RadioGroup có thể giữ nguyên.

bf454f3f1617024d.png

...

<RadioGroup
    android:id="@+id/tip_options"
    ...
    app:layout_constraintStart_toStartOf="@id/service_question">

...
  1. Sau đó, hãy tiếp tục thêm biểu tượng ic_round_up vào bố cục bên cạnh nút chuyển "Round up tip?" ("Làm tròn tiền boa?"). Hãy thử tự thực hiện việc này và nếu gặp khó khăn, bạn có thể tham khảo đoạn mã XML dưới đây. Bạn có thể gán mã nhận dạng tài nguyên icon_round_up cho ImageView mới.
  2. Trong bố cục XML, hãy chèn một ImageView mới sau RadioGroup nhưng trước tiện ích SwitchMaterial.
  3. Gán mã nhận dạng tài nguyên icon_round_up cho ImageView rồi đặt srcCompat thành biểu tượng @drawable/ic_round_up có thể vẽ. Ràng buộc phần bắt đầu của ImageView với phần bắt đầu của thành phần mẹ, đồng thời căn giữa biểu tượng này theo chiều dọc so với SwitchMaterial.
  4. Cập nhật SwitchMaterial bên cạnh biểu tượng này và thiết lập khoảng cách lề bắt đầu (start margin) 16dp. Kết quả XML cho icon_round_upround_up_switch sẽ có dạng như sau.
...

   <ImageView
        android:id="@+id/icon_round_up"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:importantForAccessibility="no"
        app:srcCompat="@drawable/ic_round_up"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@id/round_up_switch"
        app:layout_constraintBottom_toBottomOf="@id/round_up_switch" />

    <com.google.android.material.switchmaterial.SwitchMaterial
        android:id="@+id/round_up_switch"
        ...
        android:layout_marginStart="16dp"
        app:layout_constraintStart_toEndOf="@id/icon_round_up" />

...
  1. Chế độ xem Design (Thiết kế) sẽ có dạng như sau. Cả ba biểu tượng đều được đặt đúng vị trí.

8781ecbd11859cc4.png

  1. Nếu so sánh với ảnh chụp màn hình ứng dụng hoàn thiện, bạn sẽ thấy nút tính tiền boa cũng được dịch chuyển theo chiều dọc tương ứng với trường chi phí dịch vụ, câu hỏi về chất lượng dịch vụ, tuỳ chọn trên nút chọn và câu hỏi làm tròn tiền boa. Bạn có thể làm được điều này bằng cách ràng buộc phần bắt đầu của nút tính tiền với phần bắt đầu của round_up_switch. Đồng thời, thêm khoảng cách lề dọc 8dp giữa nút tính tiền và nút chuyển ở phía trên.

84348568e13d9e32.png

...

<Button
   android:id="@+id/calculate_button"
   ...
   android:layout_marginTop="8dp"
   app:layout_constraintStart_toStartOf="@id/round_up_switch" />

...
  1. Cuối cùng nhưng không kém phần quan trọng, hãy định vị tip_result bằng cách thêm khoảng cách lề trên (top margin) 8dp cho TextView.

8e21f52be710340d.png

...

<TextView
   android:id="@+id/tip_result"
   ...
   android:layout_marginTop="8dp" />

...
  1. Thật sự phải qua rất nhiều bước! Bạn đã làm rất tốt theo từng bước. Bạn cần rất chú ý đến từng chi tiết để có thể chỉnh sửa các thành phần bố cục một cách chính xác. Nhờ vậy kết quả cuối cùng sẽ đẹp hơn rất nhiều! Hãy chạy ứng dụng và ứng dụng sẽ có dạng như ảnh chụp màn hình dưới đây. Việc căn chỉnh theo chiều dọc và tăng khoảng cách giữa các thành phần giúp cho những thành phần này không bị chồng chéo lên nhau.

1f2ef2c0c9a9bdc7.png

Vẫn chưa xong đâu! Bạn có thể thấy kích thước và màu phông chữ của phần câu hỏi về chất lượng dịch vụ và giá trị tiền boa đang khác với phần văn bản trong các nút chọn và nút chuyển. Nhiệm vụ tiếp theo sẽ giúp tạo ra tính nhất quán bằng cách sử dụng tính năng định kiểu và thiết lập giao diện (theme).

5. Kiểu và giao diện

Kiểu (style) là một tập hợp giá trị thuộc tính của khung nhìn cho một loại tiện ích nào đó. Ví dụ: kiểu cho TextView có thể chỉ định màu phông chữ, kích thước phông chữ và màu nền, v.v. Bằng cách trích xuất các thuộc tính này vào một kiểu, bạn có thể dễ dàng áp dụng kiểu này cho nhiều khung nhìn trong bố cục và lưu giữ kiểu này tại một nơi duy nhất.

Trong nhiệm vụ này, trước tiên bạn sẽ tạo kiểu cho khung nhìn văn bản, nút chọn và tiện ích dạng nút chuyển.

Tạo kiểu

  1. Tạo một tệp mới có tên styles.xml trong thư mục res > values nếu chưa có. Tạo tệp bằng cách nhấp chuột phải vào thư mục values rồi chọn New (Mới) > Values Resource File (Tệp tài nguyên giá trị). Đặt tên tệp là styles.xml. Tệp mới sẽ có những nội dung sau đây.
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>
  1. Tạo một kiểu TextView mới giúp văn bản xuất hiện nhất quán trong toàn bộ ứng dụng. Xác định kiểu này một lần trong styles.xml, sau đó bạn có thể áp dụng kiểu đó cho tất cả TextViews trong bố cục. Tuy có thể định nghĩa một kiểu từ đầu, nhưng bạn cũng có thể mở rộng một kiểu TextView hiện có trong thư viện MDC.

Khi định kiểu cho một thành phần nào đó, thường thì bạn nên mở rộng từ kiểu gốc của loại tiện ích bạn đang sử dụng. Việc này thực sự quan trọng vì hai lý do. Thứ nhất, việc này giúp đảm bảo tất cả giá trị mặc định quan trọng được đặt trên thành phần của bạn. Thứ hai, kiểu của bạn sẽ tiếp tục kế thừa mọi thay đổi sau này đối với kiểu gốc.

Bạn có thể tuỳ ý đặt tên cho kiểu của mình, nhưng nên đặt theo quy ước dưới đây. Nếu bạn kế thừa từ một kiểu Material gốc, hãy đặt tên kiểu dưới dạng song song bằng cách thay thế MaterialComponents bằng tên ứng dụng (TipTime). Lúc này, các thay đổi của bạn sẽ được chuyển vào không gian tên riêng, giúp loại bỏ khả năng xảy ra xung đột sau này khi các Thành phần Material đưa ra kiểu mới. Ví dụ:

Tên kiểu của bạn: Widget.TipTime.TextView, kế thừa từ kiểu gốc: Widget.MaterialComponents.TextView.

Thêm thông tin này vào tệp styles.xml ở giữa thẻ mở và thẻ đóng resources.

<style name="Widget.TipTime.TextView" parent="Widget.MaterialComponents.TextView">
</style>
  1. Thiết lập kiểu TextView để ghi đè các thuộc tính sau: android:minHeight,android:gravity,android:textAppearance.

android:minHeight đặt ra chiều cao tối thiểu là 48 dp trên TextView. Theo nguyên tắc của Material Design, chiều cao tối thiểu cho một hàng bất kỳ phải là 48 dp.

Bạn có thể căn chỉnh văn bản trong TextView vào giữa theo chiều dọc bằng cách đặt thuộc tính android:gravity. (Xem ảnh chụp màn hình dưới đây.) Thuộc tính Gravity (Trọng tâm) giúp kiểm soát cách nội dung tự định vị trong khung nhìn. Vì nội dung văn bản thực tế không chiếm toàn bộ chiều cao 48 dp nên giá trị center_vertical sẽ căn giữa văn bản bên trong TextView theo chiều dọc (nhưng không thay đổi vị trí chiều ngang). Có thể kể đến một số giá trị gravity (lực hấp dẫn) khác như center, center_horizontal, topbottom. Bạn có thể thử các giá trị gravity (lực hấp dẫn) này để xem hiệu ứng trên văn bản.

6a7ecc6a49a858e9.png

Đặt giá trị cho thuộc tính giao diện văn bản (text appearance) thành ?attr/textAppearanceBody1. TextAppearance là tập hợp các kiểu được tạo sẵn về kích thước văn bản, phông chữ và các thuộc tính văn bản khác. Để biết thêm về các giao diện văn bản khác do Material cung cấp, hãy xem danh sách kiểu chữ này.

<style name="Widget.TipTime.TextView" parent="Widget.MaterialComponents.TextView">
    <item name="android:minHeight">48dp</item>
    <item name="android:gravity">center_vertical</item>
    <item name="android:textAppearance">?attr/textAppearanceBody1</item>
</style>
  1. Áp dụng kiểu Widget.TipTime.TextView cho service_question TextView bằng cách thêm thuộc tính định kiểu trên mỗi TextView trong activity_main.xml.
<TextView
    android:id="@+id/service_question"
    style="@style/Widget.TipTime.TextView"
    ... />

Trước khi tạo kiểu, TextView trông như dưới đây với phông chữ cỡ nhỏ và màu xám:

4d54a3179f0c6f8d.png

Sau khi thêm kiểu, TextView sẽ có dạng như sau. Bây giờ TextView này đã trông nhất quán hơn với phần bố cục còn lại.

416d3928f9c3d3de.png

  1. Áp dụng kiểu Widget.TipTime.TextView tương tự cho tip_result TextView.
<TextView
    android:id="@+id/tip_result"
    style="@style/Widget.TipTime.TextView"
    ... />

3ebe16aa8c5bc010.png

  1. Bạn nên áp dụng cùng một kiểu văn bản cho nhãn văn bản trên nút chuyển. Tuy nhiên, bạn không thể đặt kiểu TextView cho tiện ích SwitchMaterial. Chỉ có thể áp dụng kiểu TextView trên TextViews. Do đó, hãy tạo một kiểu mới cho nút chuyển đó. Các thuộc tính này giống nhau về minHeight, gravitytextAppearance. Điểm khác biệt ở đây thể hiện ở tên kiểu vừa tạo và tên của kiểu gốc vì bạn đang kế thừa kiểu này từ kiểu Switch trong thư viện MDC. Tên kiểu của bạn cũng phải phản ánh tên của kiểu gốc.

Tên kiểu của bạn: Widget.TipTime.CompoundButton.Switch, kế thừa từ kiểu gốc: Widget.MaterialComponents.CompoundButton.Switch.

<style name="Widget.TipTime.CompoundButton.Switch" parent="Widget.MaterialComponents.CompoundButton.Switch">
   <item name="android:minHeight">48dp</item>
   <item name="android:gravity">center_vertical</item>
   <item name="android:textAppearance">?attr/textAppearanceBody1</item>
</style>

Bạn cũng có thể chỉ định các thuộc tính bổ sung dành riêng cho các nút chuyển trong kiểu này, nhưng không nhất thiết phải làm điều đó.

  1. Văn bản của nút chọn là phần cuối cùng bạn cần kiểm tra để đảm bảo tính nhất quán của văn bản. Bạn không thể áp dụng kiểu TextView hoặc Switch cho tiện ích RadioButton. Thay vào đó, bạn phải tạo kiểu mới cho các nút chọn. Bạn có thể mở rộng từ kiểu RadioButton của thư viện MDC.

Khi tạo kiểu này, hãy thêm một số khoảng đệm giữa văn bản nút chọn và hình ảnh vòng tròn. paddingStart là một thuộc tính mới mà bạn chưa từng sử dụng. Khoảng đệm là khoảng không gian giữa nội dung của một khung nhìn và đường biên của khung nhìn đó. Thuộc tính paddingStart thiết lập khoảng đệm ở phần bắt đầu của mỗi thành phần. Hãy xem sự khác biệt giữa paddingStart 0dp và 8dp của trên nút chọn.

4c1aa37bbdadab1d.png

35a96c994b82539e.png

<style name="Widget.TipTime.CompoundButton.RadioButton"
parent="Widget.MaterialComponents.CompoundButton.RadioButton">
   <item name="android:paddingStart">8dp</item>
   <item name="android:textAppearance">?attr/textAppearanceBody1</item>
</style>
  1. (Không bắt buộc) Tạo tệp dimens.xml để cải thiện khả năng quản lý các giá trị thường dùng. Bạn có thể tạo tệp này theo cách tương tự như cho tệp styles.xml ở trên. Chọn thư mụcvalues, nhấp chuột phải rồi chọn New (Mới) > Values Resource File (Tệp tài nguyên giá trị).

Trong ứng dụng nhỏ này, bạn đã lặp lại thao tác cài đặt chiều cao tối thiểu hai lần. Giờ thì việc này có thể quản lý dễ dàng, nhưng nếu có 4, 6, 10 thành phần trở lên cần thực hiện thao tác này thì việc kiểm soát sẽ trở nên phức tạp. Việc ghi nhớ để thay đổi lần lượt tất cả thành phần này là một công việc rất tẻ nhạt và thường gặp lỗi. Bạn có thể tạo một tệp tài nguyên trợ giúp khác trong res > values có tên dimens.xml chứa các kích thước phổ biến phân biệt theo tên. Khi chuẩn hoá các giá trị dùng chung dưới dạng các kích thước có tên, bạn có thể quản lý ứng dụng của mình dễ dàng hơn. Tip Time là một ứng dụng nhỏ nên không cần thực hiện bước tuỳ chọn này. Tuy nhiên, với các ứng dụng trong môi trường sản xuất phức tạp hơn và cần phải hợp tác với đội ngũ thiết kế, dimens.xml cho phép bạn thay đổi các giá trị này dễ dàng hơn.

dimens.xml

<resources>
   <dimen name="min_text_height">48dp</dimen>
</resources>

Bạn sẽ cập nhật tệp styles.xml để sử dụng @dimen/min_text_height thay vì trực tiếp khai báo giá trị 48dp.

...
<style name="Widget.TipTime.TextView" parent="Widget.MaterialComponents.TextView">
    <item name="android:minHeight">@dimen/min_text_height</item>
    <item name="android:gravity">center_vertical</item>
    <item name="android:textAppearance">?attr/textAppearanceBody1</item>
</style>
...

Thêm kiểu vào giao diện

Có thể thấy rằng bạn chưa áp dụng kiểu RadioButtonSwitch mới cho các tiện ích tương ứng. Lý do là bạn sẽ sử dụng thuộc tính giao diện (theme) để thiết lập kiểu cho radioButtonStyleswitchStyle trong giao diện ứng dụng. Hãy cùng xem lại giao diện là gì.

Giao diện (theme) là một tập hợp các tài nguyên có tên (gọi là thuộc tính giao diện), cho phép tham chiếu sau này về kiểu, bố cục, v.v. Bạn có thể chỉ định giao diện cho toàn bộ ứng dụng, hoạt động hoặc hệ phân cấp khung hiển thị – chứ không chỉ cho từng View riêng lẻ. Ở bước trước, bạn đã sửa đổi giao diện ứng dụng trong themes.xml bằng cách đặt các thuộc tính giao diện như colorPrimarycolorSecondary, áp dụng trên toàn bộ ứng dụng cũng như các thành phần của ứng dụng.

Bạn có thể sử dụng các thuộc tính giao diện khác như radioButtonStyleswitchStyle. Tài nguyên định kiểu mà bạn cung cấp cho các thuộc tính giao diện này sẽ được áp dụng cho mọi nút chọn và mọi nút chuyển trong hệ phân cấp khung nhìn đang áp dụng giao diện đó.

Ngoài ra, có một thuộc tính giao diện cho textInputStyle, trong đó tài nguyên định kiểu được chỉ định sẽ áp dụng cho tất cả trường nhập văn bản trong ứng dụng. Để TextInputLayout xuất hiện dưới dạng trường văn bản có đường viền (như trong nguyên tắc Material Design), bạn có thể sử dụng kiểu OutlinedBox theo định nghĩa trong thư viện MDC dưới dạng Widget.MaterialComponents.TextInputLayout.OutlinedBox. Đây là kiểu bạn sẽ sử dụng.

2b2a5836a5d9bedf.png

  1. Chỉnh sửa tệp themes.xml để giao diện tham chiếu đến các kiểu mong muốn. Việc thiết lập thuộc tính giao diện được thực hiện tương tự như cách khai báo các thuộc tính giao diện colorPrimarycolorSecondary trong một lớp học lập trình trước. Tuy nhiên, lần này các thuộc tính giao diện liên quan là textInputStyle, radioButtonStyleswitchStyle. Bạn sẽ sử dụng các kiểu mà bạn đã tạo trước đây cho RadioButtonSwitch cùng với kiểu cho trường văn bản OutlinedBox của Material.

Sao chép nội dung sau vào res/values/themes.xml trong thẻ định kiểu cho giao diện ứng dụng.

<item name="textInputStyle">@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox</item>
<item name="radioButtonStyle">@style/Widget.TipTime.CompoundButton.RadioButton</item>
<item name="switchStyle">@style/Widget.TipTime.CompoundButton.Switch</item>
  1. Tệp res/values/themes.xml của bạn sẽ có dạng như dưới đây. Bạn có thể thêm ghi chú (comment) trong tệp XML nếu muốn (biểu thị bằng cặp dấu <!--->).
<resources xmlns:tools="http://schemas.android.com/tools">

    <!-- Base application theme. -->
    <style name="Theme.TipTime" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
        ...
        <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
        <!-- Text input fields -->
        <item name="textInputStyle">@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox</item>
        <!-- Radio buttons -->
        <item name="radioButtonStyle">@style/Widget.TipTime.CompoundButton.RadioButton</item>
        <!-- Switches -->
        <item name="switchStyle">@style/Widget.TipTime.CompoundButton.Switch</item>
    </style>

</resources>
  1. Đừng quên thực hiện thay đổi tương tự cho giao diện tối trong themes.xml (night) (themes.xml (ban đêm)). Tệp res/values-night/themes.xml của bạn sẽ có dạng như dưới đây.
<resources xmlns:tools="http://schemas.android.com/tools">

    <!-- Application theme for dark theme. -->
    <style name="Theme.TipTime" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
        ...
        <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
        <!-- Text input fields -->
        <item name="textInputStyle">@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox</item>
        <!-- For radio buttons -->
        <item name="radioButtonStyle">@style/Widget.TipTime.CompoundButton.RadioButton</item>
        <!-- For switches -->
        <item name="switchStyle">@style/Widget.TipTime.CompoundButton.Switch</item>
    </style>

</resources>
  1. Chạy ứng dụng và xem sự thay đổi. Kiểu OutlinedBox cho trường văn bản trông đẹp hơn, đồng thời, tất cả văn bản giờ đây đều nhất quán với nhau!

31ac15991713b031.png 3e861407146c9ed4.png

6. Nâng cao trải nghiệm người dùng

Khi gần hoàn thành ứng dụng, bạn nên kiểm thử ứng dụng không chỉ theo quy trình công việc dự kiến mà còn theo nhiều kịch bản người dùng khác. Có thể thấy rằng một số thay đổi nhỏ về mã có thể giúp cải thiện đáng kể trải nghiệm người dùng.

Xoay thiết bị

  1. Xoay thiết bị sang chế độ ngang. Trước hết, bạn cần bật chế độ cài đặt Tự động xoay. (Chế độ này nằm trong phần Cài đặt nhanh của thiết bị hoặc trong tuỳ chọn Cài đặt > Màn hình > Nâng cao > Tự động xoay màn hình.)

f2edb1ae9926d5f1.png

Trong trình mô phỏng, bạn có thể sử dụng các tuỳ chọn mô phỏng (nằm ở phía trên bên phải ngay cạnh thiết bị) để xoay màn hình sang phải hoặc trái.

2bc08f73d28968cb.png

  1. Bạn sẽ nhận thấy một số thành phần giao diện người dùng, bao gồm cả nút lệnh Calculate (Tính toán), sẽ bị cắt đi. Rõ ràng là việc này khiến bạn không dùng được ứng dụng!

d73499f9c9d2b330.png

  1. Để khắc phục lỗi này, hãy thêm ScrollView xung quanh ConstraintLayout. Tệp XML của bạn sẽ có dạng như sau.
<ScrollView
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_height="match_parent"
   android:layout_width="match_parent">

   <androidx.constraintlayout.widget.ConstraintLayout
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:padding="16dp"
       tools:context=".MainActivity">

       ...
   </ConstraintLayout>

</ScrollView>
  1. Chạy và kiểm tra lại ứng dụng. Khi xoay thiết bị sang chế độ ngang, bạn có thể cuộn giao diện người dùng để sử dụng nút tính tiền và xem kết quả tiền boa. Bản sửa lỗi này không chỉ hữu ích khi dùng chế độ ngang mà còn áp dụng tốt trên các thiết bị Android đa dạng kích thước. Bây giờ, người dùng có thể cuộn bố cục bất kể kích thước màn hình thiết bị như thế nào.

Ẩn bàn phím bằng phím Enter

Có thể bạn sẽ để ý thấy sau khi nhập chi phí dịch vụ, bàn phím vẫn còn ở đó. Việc này có thể hơi phiền phức khi bạn phải tự mình ẩn bàn phím mỗi lần cần truy cập nút tính toán. Thay vào đó, hãy khiến bàn phím tự động ẩn khi nhấn phím Enter.

e2c3a3dbc40218a2.png

Với trường văn bản, bạn có thể định nghĩa một trình nghe phím (key listener) để phản hồi sự kiện khi nhấn vào một số phím nhất định. Mọi tuỳ chọn nhập liệu trên bàn phím đều có một mã phím tương ứng, bao gồm cả phím Enter. Hãy lưu ý rằng bàn phím ảo còn được gọi là bàn phím mềm (trái với bàn phím thực).

1c95d7406d3847fe.png

Trong nhiệm vụ này, hãy thiết lập một trình nghe phím trên trường văn bản để theo dõi khi nhấn phím Enter. Khi phát hiện sự kiện đó, hãy bắt đầu ẩn bàn phím.

  1. Sao chép và dán phương thức trợ giúp này vào lớp MainActivity. Bạn có thể chèn phương thức này ngay phía trước dấu ngoặc nhọn đóng của lớp MainActivity. handleKeyEvent() là một hàm trợ giúp riêng tư, giúp ẩn bàn phím ảo khi tham số nhập keyCode bằng KeyEvent.KEYCODE_ENTER. InputMethodManager sẽ kiểm soát xem bàn phím mềm sẽ xuất hiện hay bị ẩn, đồng thời cho phép người dùng lựa chọn bàn phím mềm nào sẽ xuất hiện. Phương thức này sẽ trả về giá trị đúng (true) nếu sự kiện bàn phím đã được xử lý và trả về sai (false) nếu xử lý không thành công.

MainActivity.kt

private fun handleKeyEvent(view: View, keyCode: Int): Boolean {
   if (keyCode == KeyEvent.KEYCODE_ENTER) {
       // Hide the keyboard
       val inputMethodManager =
           getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
       inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0)
       return true
   }
   return false
}
  1. Bây giờ, hãy gắn một trình nghe phím vào tiện ích TextInputEditText. Hãy lưu ý bạn có thể truy cập tiện ích TextInputEditText thông qua đối tượng liên kết dưới dạng binding.costOfServiceEditText.

Gọi phương thức setOnKeyListener() qua costOfServiceEditText rồi truyền vào OnKeyListener. Thao tác này tương tự như cách thiết lập trình nghe lượt nhấp trên nút tính tiền trong ứng dụng có binding.calculateButton.setOnClickListener { calculateTip() }.

Mã để thiết lập trình nghe phím trên một khung nhìn sẽ phức tạp hơn một chút, nhưng nhìn chung ý tưởng là OnKeyListener có một phương thức onKey() được kích hoạt khi người dùng nhấn phím. Phương thức onKey() gồm 3 tham số đầu vào: khung nhìn, mã phím được nhấn và sự kiện phím nhấn (bạn chỉ cần gọi sự kiện này dưới dạng "_"). Khi phương thức onKey() được gọi, bạn sẽ gọi phương thức handleKeyEvent() và truyền vào các tham số khung nhìn và mã phím. Cú pháp để lập trình là: view, keyCode, _ -> handleKeyEvent(view, keyCode). Biểu thức này được gọi là lambda và bạn sẽ tìm hiểu thêm về lambda trong một bài sau.

Thêm mã để thiết lập trình nghe phím trên trường văn bản trong phương thức onCreate() của hoạt động. Lý do là trình nghe phím nên được đính kèm ngay khi bố cục được tạo và trước khi người dùng bắt đầu tương tác với hoạt động.

MainActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
   ...

   setContentView(binding.root)

   binding.calculateButton.setOnClickListener { calculateTip() }

   binding.costOfServiceEditText.setOnKeyListener { view, keyCode, _ -> handleKeyEvent(view, keyCode)
   }
}
  1. Hãy kiểm tra để đảm bảo các thay đổi hoạt động tốt trên ứng dụng. Chạy ứng dụng rồi nhập chi phí dịch vụ. Nhấn phím Enter trên bàn phím và bàn phím mềm sẽ bị ẩn đi.

Kiểm thử ứng dụng dùng TalkBack

Bạn học khoá này vì muốn xây dựng một ứng dụng tiếp cận càng nhiều người dùng càng tốt. Một số người dùng có thể sử dụng TalkBack để truy cập và thao tác trên ứng dụng của bạn. TalkBack là trình đọc màn hình của Google có sẵn trên thiết bị Android. TalkBack cung cấp tính năng phản hồi bằng giọng nói để người dùng có thể sử dụng thiết bị mà không cần nhìn màn hình.

Khi TalkBack đang bật, hãy đảm bảo rằng người dùng có thể hoàn tất việc tính tiền boa trong ứng dụng của bạn.

  1. Bật Talkback trên thiết bị của bạn theo hướng dẫn này.
  2. Quay lại ứng dụng Tip Time (Tiền boa).
  3. Dùng TalkBack để khám phá ứng dụng theo hướng dẫn này. Vuốt sang phải để di chuyển lần lượt qua từng thành phần màn hình rồi vuốt sang trái để di chuyển theo hướng ngược lại. Nhấn đúp vào vị trí bất kỳ để chọn. Kiểm tra để đảm bảo rằng bạn có thể truy cập vào mọi thành phần của ứng dụng bằng cách vuốt.
  4. Đảm bảo người dùng Talkback có thể di chuyển đến từng mục trên màn hình, nhập chi phí dịch vụ, thay đổi cách tính tiền boa, tính tiền boa và nghe được số tiền boa đã tính. Hãy nhớ rằng bạn không cung cấp phản hồi bằng giọng nói cho các biểu tượng vì bạn đã đánh dấu các biểu tượng đó là importantForAccessibility="no".

Để biết thêm thông tin về cách cải thiện khả năng hỗ trợ tiếp cận của ứng dụng, hãy tham khảo các nguyên tắc này và lộ trình học tập này.

(Không bắt buộc) Điều chỉnh sắc thái màu của vectơ vẽ được

Trong nhiệm vụ không bắt buộc này, bạn sẽ phủ màu cho các biểu tượng dựa trên màu chính của giao diện, nhờ đó các biểu tượng sẽ thay đổi giữa giao diện sáng và giao diện tối (như hình dưới đây). Đây là một tính năng bổ sung tuyệt vời cho giao diện người dùng, giúp các biểu tượng thể hiện nhất quán hơn với giao diện ứng dụng.

77092f702beb1cfb.png 80a390087905eb29.png

Như đã đề cập, một trong những ưu điểm của VectorDrawables so với hình ảnh bitmap là khả năng điều chỉnh tỷ lệ và phủ màu. Bên dưới là mã XML thể hiện biểu tượng chuông. Có 2 thuộc tính màu sắc đặc trưng cần lưu ý là android:tintandroid:fillColor.

ic_service.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"
   android:width="24dp"
   android:height="24dp"
   android:viewportWidth="24"
   android:viewportHeight="24"
   android:tint="?attr/colorControlNormal">
 <path
     android:fillColor="@android:color/white"
     android:pathData="M2,17h20v2L2,19zM13.84,7.79c0.1,-0.24 0.16,-0.51 0.16,-0.79 0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2c0,0.28 0.06,0.55 0.16,0.79C6.25,8.6 3.27,11.93 3,16h18c-0.27,-4.07 -3.25,-7.4 -7.16,-8.21z"/>
</vector>

aab70a5d4eaabdc7.png

Nếu một sắc thái màu nào đó đang hiện diện, sắc thái màu này sẽ ghi đè lên mọi lệnh fillColor cho đối tượng có thể vẽ. Trong trường hợp này, thuộc tính giao diện colorControlNormal sẽ ghi đè màu trắng. colorControlNormal là màu ở trạng thái "bình thường" (chưa chọn/chưa kích hoạt) của một tiện ích. Hiện tại, màu này là màu xám.

Có một cách để cải thiện hình ảnh cho ứng dụng là phủ màu lên đối tượng có thể vẽ dựa trên màu chính của giao diện ứng dụng. Đối với giao diện sáng, biểu tượng sẽ xuất hiện dưới dạng @color/green, còn trong giao diện tối, biểu tượng sẽ xuất hiện dưới dạng @color/green_light, chính là ?attr/colorPrimary. Việc phủ màu lên đối tượng có thể vẽ dựa trên màu chính của giao diện ứng dụng giúp cho các phần tử trong bố cục trở nên nhất quán và gắn kết hơn. Đồng thời, điều này cũng giúp chúng ta không phải tạo bản sao tập hợp biểu tượng riêng cho giao diện sáng và giao diện tối. Chỉ có 1 tập hợp vectơ vẽ được và sắc thái màu sẽ thay đổi dựa trên thuộc tính giao diện colorPrimary.

  1. Thay đổi giá trị của thuộc tính android:tint trong ic_service.xml
android:tint="?attr/colorPrimary"

Trong Android Studio, biểu tượng này nay đã có sắc thái màu phù hợp.

f0b8f59dbf00a20b.png

Giá trị mà thuộc tính giao diện colorPrimary trỏ đến sẽ thay đổi tuỳ thuộc vào giao diện sáng hay tối.

  1. Lặp lại các bước trên để thay đổi sắc thái màu cho các vectơ vẽ được khác.

ic_store.xml

<vector ...
   android:tint="?attr/colorPrimary">
   ...
</vector>

ic_round_up.xml

<vector ...
   android:tint="?attr/colorPrimary">
   ...
</vector>
  1. Chạy ứng dụng. Đảm bảo rằng các biểu tượng thay đổi tuỳ theo giao diện sáng và tối.
  2. Bước cuối cùng là dọn dẹp, hãy nhớ định dạng lại tất cả tệp mã XML và Kotlin trong ứng dụng.

Xin chúc mừng! Cuối cùng, bạn đã hoàn tất ứng dụng tính tiền boa! Bạn nên tự hào về những gì mình đã làm được. Hy vọng rằng đây chính là nền tảng giúp bạn xây dựng những ứng dụng đẹp mắt và dễ sử dụng hơn nữa!

7. Mã giải pháp

Mã giải pháp cho lớp học lập trình này nằm trong kho lưu trữ GitHub nêu dưới đây.

5743ac5ee2493d7.png ab4acfeed8390465.png

Để lấy mã cho lớp học lập trình này và mở trong Android Studio, hãy thực hiện các bước sau.

Lấy mã

  1. Nhấp vào URL được cung cấp. Thao tác này sẽ mở trang GitHub của dự án trong một trình duyệt.
  2. Kiểm tra để đảm bảo tên nhánh khớp với tên nhánh được chỉ định trong lớp học lập trình. Ví dụ: trong ảnh chụp màn hình sau đây, tên nhánh là main (chính).

8cf29fa81a862adb.png

  1. Trên trang GitHub cho dự án này, hãy nhấp vào nút Code (Mã), một cửa sổ bật lên sẽ hiện ra.

1debcf330fd04c7b.png

  1. Trong cửa sổ bật lên, nhấp vào nút Download ZIP (Tải tệp ZIP xuống) để lưu dự án vào máy tính. Chờ quá trình tải xuống hoàn tất.
  2. Xác định vị trí của tệp trên máy tính (thường nằm trong thư mục Downloads (Tệp đã tải xuống)).
  3. Nhấp đúp vào tệp ZIP để giải nén. Thao tác này sẽ tạo một thư mục mới chứa các tệp dự án.

Mở dự án trong Android Studio

  1. Khởi động Android Studio.
  2. Trong cửa sổ Welcome to Android Studio (Chào mừng bạn đến với Android Studio), hãy nhấp vào Open (Mở).

d8e9dbdeafe9038a.png

Lưu ý: Nếu Android Studio đã mở thì chuyển sang chọn tuỳ chọn File (Tệp) > Open (Mở) trong trình đơn.

8d1fda7396afe8e5.png

  1. Trong trình duyệt tệp, hãy chuyển đến vị trí của thư mục dự án chưa giải nén (thường nằm trong thư mục Downloads (Tệp đã tải xuống)).
  2. Nhấp đúp vào thư mục dự án đó.
  3. Chờ Android Studio mở dự án.
  4. Nhấp vào nút Run (Chạy) 8de56cba7583251f.png để tạo và chạy ứng dụng. Đảm bảo ứng dụng được xây dựng như mong đợi.

8. Tóm tắt

  • Sử dụng các Thành phần Material Design nếu có thể nhằm tuân thủ nguyên tắc của Material Design và tăng khả năng tuỳ chỉnh.
  • Thêm biểu tượng để cung cấp cho người dùng dấu hiệu trực quan về cách hoạt động của các phần trong ứng dụng.
  • Sử dụng ConstraintLayout để định vị các thành phần trong bố cục.
  • Kiểm tra ứng dụng theo một số trường hợp cụ thể (ví dụ: xoay ứng dụng ở chế độ ngang) và cải thiện (nếu cần).
  • Chú thích mã để giúp người khác hiểu được phương pháp tiếp cận của bạn khi đọc mã.
  • Định dạng lại và dọn dẹp để mã của bạn càng ngắn gọn càng tốt.

9. Tìm hiểu thêm

10. Tự thực hành

  • Tiếp nối các lớp học lập trình trước đây, hãy cập nhật ứng dụng chuyển đổi đơn vị nấu ăn để tuân thủ chặt chẽ hơn các nguyên tắc của Material Design bằng cách sử dụng các phương pháp hay nhất mà bạn đã học được trong lớp học lập trình này (chẳng hạn như sử dụng Thành phần Material Design).