Tạo chế độ xem tuỳ chỉnh

Lớp học lập trình này nằm trong khoá học Kotlin nâng cao cho Android. Bạn sẽ nhận được nhiều giá trị nhất qua khoá học này nếu thực hiện các lớp học lập trình theo trình tự, nhưng đó không phải là yêu cầu bắt buộc. Tất cả các lớp học lập trình của khoá học đều được liệt kê trên trang đích của lớp học lập trình Kiến thức nâng cao về cách tạo ứng dụng Android bằng Kotlin.

Giới thiệu

Android cung cấp một tập hợp lớn các lớp con View, chẳng hạn như Button, TextView, EditText, ImageView, CheckBox hoặc RadioButton. Bạn có thể dùng các lớp con này để tạo giao diện người dùng cho phép người dùng tương tác và hiển thị thông tin trong ứng dụng. Nếu không có lớp con View nào đáp ứng được nhu cầu của bạn, thì bạn có thể tạo một lớp con View được gọi là chế độ xem tuỳ chỉnh .

Để tạo một khung hiển thị tuỳ chỉnh, bạn có thể mở rộng một lớp con View hiện có (chẳng hạn như Button hoặc EditText) hoặc tạo lớp con View của riêng bạn. Bằng cách mở rộng trực tiếp View, bạn có thể tạo một phần tử tương tác trên giao diện người dùng có kích thước và hình dạng bất kỳ bằng cách ghi đè phương thức onDraw() cho View để vẽ phần tử đó.

Sau khi tạo một khung hiển thị tuỳ chỉnh, bạn có thể thêm khung hiển thị đó vào bố cục hoạt động theo cách tương tự như khi thêm một TextView hoặc Button.

Bài học này hướng dẫn bạn cách tạo một khung hiển thị tuỳ chỉnh từ đầu bằng cách mở rộng View.

Kiến thức bạn cần có

  • Cách tạo một ứng dụng có Hoạt động và chạy ứng dụng đó bằng Android Studio.

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

  • Cách mở rộng View để tạo một khung hiển thị tuỳ chỉnh.
  • Cách vẽ một khung hiển thị tuỳ chỉnh có hình tròn.
  • Cách sử dụng trình nghe để xử lý hoạt động tương tác của người dùng với khung hiển thị tuỳ chỉnh.
  • Cách sử dụng khung hiển thị tuỳ chỉnh trong bố cục.

Bạn sẽ thực hiện

  • Mở rộng View để tạo chế độ xem tuỳ chỉnh.
  • Khởi tạo khung hiển thị tuỳ chỉnh bằng các giá trị vẽ và tô.
  • Ghi đè onDraw() để vẽ khung hiển thị.
  • Sử dụng trình nghe để cung cấp hành vi cho khung hiển thị tuỳ chỉnh.
  • Thêm khung hiển thị tuỳ chỉnh vào một bố cục.

Ứng dụng CustomFanController minh hoạ cách tạo một lớp con khung hiển thị tuỳ chỉnh bằng cách mở rộng lớp View. Lớp con mới có tên là DialView.

Ứng dụng này hiển thị một phần tử giao diện người dùng hình tròn giống như một nút điều khiển quạt vật lý, với các chế độ cài đặt cho tắt (0), thấp (1), trung bình (2) và cao (3). Khi người dùng nhấn vào khung hiển thị, chỉ báo lựa chọn sẽ di chuyển đến vị trí tiếp theo: 0-1-2-3 rồi quay lại 0. Ngoài ra, nếu lựa chọn là 1 trở lên, màu nền của phần hình tròn trong khung hiển thị sẽ thay đổi từ màu xám sang màu xanh lục (cho biết quạt đang bật).

Khung hiển thị là thành phần cơ bản của giao diện người dùng của ứng dụng. Lớp View cung cấp nhiều lớp con, được gọi là tiện ích giao diện người dùng, đáp ứng nhiều nhu cầu của giao diện người dùng trong một ứng dụng Android thông thường.

Các khối tạo giao diện người dùng như ButtonTextView là các lớp con mở rộng lớp View. Để tiết kiệm thời gian và công sức phát triển, bạn có thể mở rộng một trong các lớp con View này. Khung hiển thị tuỳ chỉnh kế thừa giao diện và hành vi của khung hiển thị gốc, đồng thời, bạn có thể ghi đè hành vi hoặc khía cạnh của giao diện mà bạn muốn thay đổi. Ví dụ: nếu bạn mở rộng EditText để tạo một khung hiển thị tuỳ chỉnh, thì khung hiển thị này sẽ hoạt động giống như khung hiển thị EditText, nhưng cũng có thể được tuỳ chỉnh để hiện, chẳng hạn như nút X xoá văn bản khỏi trường nhập văn bản.

Bạn có thể mở rộng bất kỳ lớp con View nào, chẳng hạn như EditText, để có một khung hiển thị tuỳ chỉnh – hãy chọn khung hiển thị gần nhất với những gì bạn muốn đạt được. Sau đó, bạn có thể dùng khung hiển thị tuỳ chỉnh này như bất kỳ lớp con View nào khác trong một hoặc nhiều bố cục dưới dạng phần tử XML có các thuộc tính.

Để tạo chế độ xem tuỳ chỉnh của riêng bạn từ đầu, hãy mở rộng chính lớp View. Mã của bạn sẽ ghi đè các phương thức View để xác định giao diện và chức năng của khung hiển thị. Yếu tố quan trọng để tạo thành phần hiển thị tuỳ chỉnh của riêng bạn là bạn phải chịu trách nhiệm vẽ toàn bộ phần tử giao diện người dùng có kích thước và hình dạng bất kỳ lên màn hình. Nếu bạn tạo lớp con cho một khung hiển thị hiện có, chẳng hạn như Button, thì lớp đó sẽ xử lý việc vẽ cho bạn. (Bạn sẽ tìm hiểu thêm về cách vẽ ở phần sau của lớp học lập trình này.)

Để tạo một khung hiển thị tuỳ chỉnh, hãy làm theo các bước chung sau:

  • Tạo một lớp khung hiển thị tuỳ chỉnh mở rộng View hoặc mở rộng một lớp con View (chẳng hạn như Button hoặc EditText).
  • Nếu bạn mở rộng một lớp con View hiện có, hãy chỉ ghi đè hành vi hoặc các khía cạnh của giao diện mà bạn muốn thay đổi.
  • Nếu bạn mở rộng lớp View, hãy vẽ hình dạng của khung hiển thị tuỳ chỉnh và kiểm soát giao diện của khung hiển thị đó bằng cách ghi đè các phương thức View như onDraw()onMeasure() trong lớp mới.
  • Thêm mã để phản hồi hoạt động tương tác của người dùng và vẽ lại khung hiển thị tuỳ chỉnh nếu cần.
  • Sử dụng lớp khung hiển thị tuỳ chỉnh làm tiện ích giao diện người dùng trong bố cục XML của hoạt động. Bạn cũng có thể xác định các thuộc tính tuỳ chỉnh cho khung hiển thị để tuỳ chỉnh khung hiển thị trong nhiều bố cục.

Trong nhiệm vụ này, bạn sẽ:

  • Tạo một ứng dụng có ImageView làm phần giữ chỗ tạm thời cho khung hiển thị tuỳ chỉnh.
  • Mở rộng View để tạo chế độ xem tuỳ chỉnh.
  • Khởi tạo khung hiển thị tuỳ chỉnh bằng các giá trị vẽ và tô.

Bước 1: Tạo ứng dụng có phần giữ chỗ ImageView

  1. Tạo một ứng dụng Kotlin có tiêu đề CustomFanController bằng mẫu Empty Activity (Hoạt động trống). Đảm bảo tên gói là com.example.android.customfancontroller.
  2. Mở activity_main.xml trong thẻ Text (Văn bản) để chỉnh sửa mã XML.
  3. Thay thế TextView hiện có bằng đoạn mã này. Văn bản này đóng vai trò là nhãn trong hoạt động cho khung hiển thị tuỳ chỉnh.
<TextView
       android:id="@+id/customViewLabel"
       android:textAppearance="@style/Base.TextAppearance.AppCompat.Display3"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:padding="16dp"
       android:textColor="@android:color/black"
       android:layout_marginStart="8dp"
       android:layout_marginEnd="8dp"
       android:layout_marginTop="24dp"
       android:text="Fan Control"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toTopOf="parent"/>
  1. Thêm phần tử ImageView này vào bố cục. Đây là phần giữ chỗ cho khung hiển thị tuỳ chỉnh mà bạn sẽ tạo trong lớp học lập trình này.
<ImageView
       android:id="@+id/dialView"
       android:layout_width="200dp"
       android:layout_height="200dp"
       android:background="@android:color/darker_gray"
       app:layout_constraintTop_toBottomOf="@+id/customViewLabel"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       android:layout_marginLeft="8dp"
       android:layout_marginRight="8dp"
       android:layout_marginTop="8dp"/>
  1. Trích xuất tài nguyên chuỗi và tài nguyên kích thước trong cả hai phần tử trên giao diện người dùng.
  2. Nhấp vào thẻ Thiết kế. Bố cục sẽ có dạng như sau:

Bước 2. Tạo lớp khung hiển thị tuỳ chỉnh

  1. Tạo một lớp Kotlin mới có tên là DialView.
  2. Sửa đổi định nghĩa lớp để mở rộng View. Nhập android.view.View khi được nhắc.
  3. Nhấp vào View rồi nhấp vào bóng đèn màu đỏ. Chọn Thêm hàm khởi tạo Android View bằng "@JvmOverloads". Android Studio sẽ thêm hàm khởi tạo từ lớp View. Chú giải @JvmOverloads hướng dẫn trình biên dịch Kotlin tạo các hàm nạp chồng cho hàm này để thay thế các giá trị tham số mặc định.
class DialView @JvmOverloads constructor(
   context: Context,
   attrs: AttributeSet? = null,
   defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
  1. Phía trên phần khai báo lớp DialView, ngay bên dưới phần nội dung nhập, hãy thêm một enum cấp cao nhất để biểu thị tốc độ quạt hiện có. Xin lưu ý rằng enum này thuộc loại Int vì các giá trị là tài nguyên chuỗi chứ không phải chuỗi thực tế. Android Studio sẽ hiển thị lỗi cho các tài nguyên chuỗi bị thiếu trong mỗi giá trị này; bạn sẽ khắc phục lỗi đó ở bước sau.
private enum class FanSpeed(val label: Int) {
   OFF(R.string.fan_off),
   LOW(R.string.fan_low),
   MEDIUM(R.string.fan_medium),
   HIGH(R.string.fan_high);
}
  1. Bên dưới enum, hãy thêm các hằng số này. Bạn sẽ dùng các giá trị này trong quá trình vẽ chỉ báo và nhãn trên mặt số.
private const val RADIUS_OFFSET_LABEL = 30      
private const val RADIUS_OFFSET_INDICATOR = -35
  1. Trong lớp DialView, hãy xác định một số biến bạn cần để vẽ khung hiển thị tuỳ chỉnh. Nhập android.graphics.PointF nếu có yêu cầu.
private var radius = 0.0f                   // Radius of the circle.
private var fanSpeed = FanSpeed.OFF         // The active selection.
// position variable which will be used to draw label and indicator circle position
private val pointPosition: PointF = PointF(0.0f, 0.0f)
  • radius là bán kính hiện tại của hình tròn. Giá trị này được đặt khi khung hiển thị được vẽ trên màn hình.
  • fanSpeed là tốc độ hiện tại của quạt, đây là một trong các giá trị trong quá trình liệt kê FanSpeed. Theo mặc định, giá trị đó là OFF.
  • Cuối cùng, postPosition là một điểm X,Y sẽ được dùng để vẽ một số phần tử của khung hiển thị trên màn hình.

Các giá trị này được tạo và khởi chạy ở đây thay vì khi khung hiển thị thực sự được vẽ, để đảm bảo rằng bước vẽ thực tế chạy nhanh nhất có thể.

  1. Cũng trong định nghĩa lớp DialView, hãy khởi tạo một đối tượng Paint bằng một số kiểu cơ bản. Nhập android.graphics.Paintandroid.graphics.Typeface khi được yêu cầu. Như trước đây với các biến, những kiểu này được khởi tạo ở đây để giúp tăng tốc bước vẽ.
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
   style = Paint.Style.FILL
   textAlign = Paint.Align.CENTER
   textSize = 55.0f
   typeface = Typeface.create( "", Typeface.BOLD)
}
  1. Mở res/values/strings.xml rồi thêm tài nguyên chuỗi cho tốc độ quạt:
<string name="fan_off">off</string>
<string name="fan_low">1</string>
<string name="fan_medium">2</string>
<string name="fan_high">3</string>

Sau khi tạo một khung hiển thị tuỳ chỉnh, bạn cần có thể vẽ khung hiển thị đó. Khi bạn mở rộng một lớp con View, chẳng hạn như EditText, lớp con đó sẽ xác định giao diện và các thuộc tính của khung hiển thị, đồng thời tự vẽ trên màn hình. Do đó, bạn không cần viết mã để vẽ khung hiển thị. Thay vào đó, bạn có thể ghi đè các phương thức của thành phần mẹ để tuỳ chỉnh khung hiển thị.

Nếu đang tạo thành phần hiển thị của riêng mình từ đầu (bằng cách mở rộng View), bạn phải chịu trách nhiệm vẽ toàn bộ thành phần hiển thị mỗi khi màn hình làm mới và ghi đè các phương thức View xử lý việc vẽ. Để vẽ đúng một khung hiển thị tuỳ chỉnh mở rộng View, bạn cần:

  • Tính kích thước của khung hiển thị khi khung hiển thị đó xuất hiện lần đầu tiên và mỗi khi kích thước của khung hiển thị đó thay đổi, bằng cách ghi đè phương thức onSizeChanged().
  • Ghi đè phương thức onDraw() để vẽ khung hiển thị tuỳ chỉnh, bằng cách sử dụng đối tượng Canvas được tạo kiểu bằng đối tượng Paint.
  • Gọi phương thức invalidate() khi phản hồi một lượt nhấp của người dùng làm thay đổi cách vẽ khung hiển thị để làm mất hiệu lực toàn bộ khung hiển thị, từ đó buộc lệnh gọi đến onDraw() để vẽ lại khung hiển thị.

Phương thức onDraw() được gọi mỗi khi màn hình làm mới, có thể là nhiều lần mỗi giây. Vì lý do hiệu suất và để tránh các lỗi về hình ảnh, bạn nên thực hiện ít thao tác nhất có thể trong onDraw(). Cụ thể, đừng đặt các hoạt động phân bổ trong onDraw(), vì các hoạt động phân bổ có thể dẫn đến việc thu gom rác, gây ra hiện tượng giật hình.

Các lớp CanvasPaint cung cấp một số phím tắt vẽ hữu ích:

Bạn sẽ tìm hiểu thêm về CanvasPaint trong một lớp học lập trình sau này. Để tìm hiểu thêm về cách Android vẽ khung hiển thị, hãy xem bài viết Cách Android vẽ khung hiển thị.

Trong nhiệm vụ này, bạn sẽ vẽ khung hiển thị tuỳ chỉnh của bộ điều khiển quạt lên màn hình (chính mặt số, chỉ báo vị trí hiện tại và nhãn chỉ báo) bằng các phương thức onSizeChanged()onDraw(). Bạn cũng sẽ tạo một phương thức trợ giúp, computeXYForSpeed(), để tính toán vị trí X,Y hiện tại của nhãn chỉ báo trên mặt số.

Bước 1. Tính toán vị trí và vẽ khung hiển thị

  1. Trong lớp DialView, bên dưới các hoạt động khởi tạo, hãy ghi đè phương thức onSizeChanged() từ lớp View để tính toán kích thước cho mặt số của khung hiển thị tuỳ chỉnh. Nhập kotlin.math.min khi có yêu cầu.

    Phương thức onSizeChanged() được gọi bất cứ khi nào kích thước của khung hiển thị thay đổi, kể cả lần đầu tiên khung hiển thị được vẽ khi bố cục được mở rộng. Ghi đè onSizeChanged() để tính toán vị trí, kích thước và mọi giá trị khác liên quan đến kích thước của khung hiển thị tuỳ chỉnh, thay vì tính toán lại mỗi khi bạn vẽ. Trong trường hợp này, bạn dùng onSizeChanged() để tính bán kính hiện tại của phần tử vòng tròn trên mặt số.
override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
   radius = (min(width, height) / 2.0 * 0.8).toFloat()
}
  1. Bên dưới onSizeChanged(), hãy thêm mã này để xác định một hàm mở rộng computeXYForSpeed() cho lớp PointF . Nhập kotlin.math.coskotlin.math.sin khi được yêu cầu. Hàm mở rộng này trên lớp PointF sẽ tính toán toạ độ X, Y trên màn hình cho nhãn văn bản và chỉ báo hiện tại (0, 1, 2 hoặc 3), dựa trên vị trí FanSpeed hiện tại và bán kính của mặt số. Bạn sẽ dùng mã này trong onDraw().
private fun PointF.computeXYForSpeed(pos: FanSpeed, radius: Float) {
   // Angles are in radians.
   val startAngle = Math.PI * (9 / 8.0)   
   val angle = startAngle + pos.ordinal * (Math.PI / 4)
   x = (radius * cos(angle)).toFloat() + width / 2
   y = (radius * sin(angle)).toFloat() + height / 2
}
  1. Ghi đè phương thức onDraw() để kết xuất khung hiển thị trên màn hình bằng các lớp CanvasPaint. Nhập android.graphics.Canvas khi có yêu cầu. Đây là chế độ ghi đè khung:
override fun onDraw(canvas: Canvas) {
   super.onDraw(canvas)
   
}
  1. Bên trong onDraw(), hãy thêm dòng này để đặt màu sơn thành màu xám (Color.GRAY) hoặc màu xanh lục (Color.GREEN) tuỳ thuộc vào tốc độ quạt là OFF hay bất kỳ giá trị nào khác. Nhập android.graphics.Color khi có yêu cầu.
// Set dial background color to green if selection not off.
paint.color = if (fanSpeed == FanSpeed.OFF) Color.GRAY else Color.GREEN
  1. Thêm mã này để vẽ một hình tròn cho mặt số bằng phương thức drawCircle(). Phương thức này sử dụng chiều rộng và chiều cao hiện tại của khung hiển thị để tìm tâm của hình tròn, bán kính của hình tròn và màu vẽ hiện tại. Các thuộc tính widthheight là thành phần của siêu lớp View và cho biết kích thước hiện tại của khung hiển thị.
// Draw the dial.
canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, paint)
  1. Thêm đoạn mã sau để vẽ một vòng tròn nhỏ hơn cho dấu chỉ báo tốc độ quạt, cũng bằng phương thức drawCircle(). Phần này sử dụng PointF.Phương thức mở rộng computeXYforSpeed() để tính toán toạ độ X,Y cho tâm của chỉ báo dựa trên tốc độ quạt hiện tại.
// Draw the indicator circle.
val markerRadius = radius + RADIUS_OFFSET_INDICATOR
pointPosition.computeXYForSpeed(fanSpeed, markerRadius)
paint.color = Color.BLACK
canvas.drawCircle(pointPosition.x, pointPosition.y, radius/12, paint)
  1. Cuối cùng, hãy vẽ nhãn tốc độ quạt (0, 1, 2, 3) ở các vị trí thích hợp xung quanh mặt số. Phần này của phương thức gọi lại PointF.computeXYForSpeed() để lấy vị trí cho từng nhãn và sử dụng lại đối tượng pointPosition mỗi lần để tránh phân bổ. Sử dụng drawText() để vẽ nhãn.
// Draw the text labels.
val labelRadius = radius + RADIUS_OFFSET_LABEL
for (i in FanSpeed.values()) {
   pointPosition.computeXYForSpeed(i, labelRadius)
   val label = resources.getString(i.label)
   canvas.drawText(label, pointPosition.x, pointPosition.y, paint)
}

Phương thức onDraw() hoàn chỉnh sẽ có dạng như sau:

override fun onDraw(canvas: Canvas) {
   super.onDraw(canvas)
   // Set dial background color to green if selection not off.
   paint.color = if (fanSpeed == FanSpeed.OFF) Color.GRAY else Color.GREEN
   // Draw the dial.
   canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, paint)
   // Draw the indicator circle.
   val markerRadius = radius + RADIUS_OFFSET_INDICATOR
   pointPosition.computeXYForSpeed(fanSpeed, markerRadius)
   paint.color = Color.BLACK
   canvas.drawCircle(pointPosition.x, pointPosition.y, radius/12, paint)
   // Draw the text labels.
   val labelRadius = radius + RADIUS_OFFSET_LABEL
   for (i in FanSpeed.values()) {
       pointPosition.computeXYForSpeed(i, labelRadius)
       val label = resources.getString(i.label)
       canvas.drawText(label, pointPosition.x, pointPosition.y, paint)
   }
}

Bước 2. Thêm khung hiển thị vào bố cục

Để thêm một khung hiển thị tuỳ chỉnh vào giao diện người dùng của ứng dụng, bạn chỉ định khung hiển thị đó dưới dạng một phần tử trong bố cục XML của hoạt động. Kiểm soát giao diện và hành vi của thành phần này bằng các thuộc tính của phần tử XML, giống như đối với mọi thành phần khác trên giao diện người dùng.

  1. Trong activity_main.xml, hãy thay đổi thẻ ImageView cho dialView thành com.example.android.customfancontroller.DialView và xoá thuộc tính android:background. Cả DialViewImageView ban đầu đều kế thừa các thuộc tính tiêu chuẩn từ lớp View, nên bạn không cần thay đổi bất kỳ thuộc tính nào khác. Phần tử DialView mới sẽ có dạng như sau:
<com.example.android.customfancontroller.DialView
       android:id="@+id/dialView"
       android:layout_width="@dimen/fan_dimen"
       android:layout_height="@dimen/fan_dimen"
       app:layout_constraintTop_toBottomOf="@+id/customViewLabel"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       android:layout_marginLeft="@dimen/default_margin"
       android:layout_marginRight="@dimen/default_margin"
       android:layout_marginTop="@dimen/default_margin" />
  1. Chạy ứng dụng. Chế độ xem điều khiển quạt sẽ xuất hiện trong hoạt động.

Nhiệm vụ cuối cùng là cho phép khung hiển thị tuỳ chỉnh thực hiện một thao tác khi người dùng nhấn vào khung hiển thị đó. Mỗi lần nhấn sẽ di chuyển chỉ báo lựa chọn đến vị trí tiếp theo: tắt – 1 – 2 – 3 và quay lại tắt. Ngoài ra, nếu lựa chọn là 1 trở lên, hãy thay đổi nền từ màu xám sang màu xanh lục để cho biết quạt đang bật.

Để cho phép người dùng nhấp vào khung hiển thị tuỳ chỉnh, bạn cần:

  • Đặt thuộc tính isClickable của khung hiển thị thành true. Điều này cho phép khung hiển thị tuỳ chỉnh của bạn phản hồi các lượt nhấp.
  • Triển khai performClick() của lớp View để thực hiện các thao tác khi người dùng nhấp vào khung hiển thị.
  • Gọi phương thức invalidate(). Điều này cho hệ thống Android biết rằng cần gọi phương thức onDraw() để vẽ lại khung hiển thị.

Thông thường, với một khung hiển thị Android tiêu chuẩn, bạn sẽ triển khai OnClickListener() để thực hiện một hành động khi người dùng nhấp vào khung hiển thị đó. Đối với một khung hiển thị tuỳ chỉnh, bạn sẽ triển khai phương thức performClick() của lớp View thay vào đó và gọi super.performClick(). Phương thức performClick() mặc định cũng gọi onClickListener(), vì vậy, bạn có thể thêm các thao tác vào performClick() và để onClickListener() có sẵn để bạn hoặc những nhà phát triển khác có thể tuỳ chỉnh thêm nếu họ sử dụng khung hiển thị tuỳ chỉnh của bạn.

  1. Trong DialView.kt, bên trong quá trình liệt kê FanSpeed, hãy thêm một hàm mở rộng next() để thay đổi tốc độ quạt hiện tại thành tốc độ tiếp theo trong danh sách (từ OFF sang LOW, MEDIUMHIGH, rồi quay lại OFF). Giờ đây, quá trình liệt kê hoàn chỉnh sẽ như sau:
private enum class FanSpeed(val label: Int) {
   OFF(R.string.fan_off),
   LOW(R.string.fan_low),
   MEDIUM(R.string.fan_medium),
   HIGH(R.string.fan_high);

   fun next() = when (this) {
       OFF -> LOW
       LOW -> MEDIUM
       MEDIUM -> HIGH
       HIGH -> OFF
   }
}
  1. Bên trong lớp DialView, ngay trước phương thức onSizeChanged(), hãy thêm một khối init(). Việc đặt thuộc tính isClickable của khung hiển thị thành true cho phép khung hiển thị đó chấp nhận dữ liệu đầu vào của người dùng.
init {
   isClickable = true
}
  1. Bên dưới init(),, hãy ghi đè phương thức performClick() bằng đoạn mã dưới đây.
override fun performClick(): Boolean {
   if (super.performClick()) return true

   fanSpeed = fanSpeed.next()
   contentDescription = resources.getString(fanSpeed.label)
  
   invalidate()
   return true
}

Cuộc gọi đến super.performClick() phải diễn ra trước tiên, cho phép các sự kiện hỗ trợ tiếp cận cũng như các lệnh gọi onClickListener().

Hai dòng tiếp theo tăng tốc độ của quạt bằng phương thức next() và đặt nội dung mô tả của khung hiển thị thành tài nguyên chuỗi đại diện cho tốc độ hiện tại (tắt, 1, 2 hoặc 3).

Cuối cùng, phương thức invalidate() sẽ vô hiệu hoá toàn bộ khung hiển thị, buộc phải gọi onDraw() để vẽ lại khung hiển thị. Nếu có thay đổi trong khung hiển thị tuỳ chỉnh của bạn vì bất kỳ lý do nào, kể cả hoạt động tương tác của người dùng và bạn cần hiển thị thay đổi đó, hãy gọi invalidate().

  1. Chạy ứng dụng. Nhấn vào phần tử DialView để di chuyển chỉ báo từ trạng thái tắt sang 1. Mặt số sẽ chuyển sang màu xanh lục. Với mỗi lần nhấn, chỉ báo sẽ di chuyển đến vị trí tiếp theo. Khi chỉ báo chuyển về trạng thái tắt, mặt số sẽ chuyển lại sang màu xám.

Ví dụ này minh hoạ cơ chế cơ bản của việc sử dụng các thuộc tính tuỳ chỉnh với khung hiển thị tuỳ chỉnh. Bạn xác định các thuộc tính tuỳ chỉnh cho lớp DialView bằng một màu riêng cho từng vị trí của nút xoay.

  1. Tạo và mở res/values/attrs.xml.
  2. Bên trong <resources>, hãy thêm một phần tử tài nguyên <declare-styleable>.
  3. Bên trong phần tử tài nguyên <declare-styleable>, hãy thêm 3 phần tử attr, mỗi phần tử cho một thuộc tính, có nameformat. format giống như một kiểu và trong trường hợp này, đó là color.
<?xml version="1.0" encoding="utf-8"?>
<resources>
       <declare-styleable name="DialView">
           <attr name="fanColor1" format="color" />
           <attr name="fanColor2" format="color" />
           <attr name="fanColor3" format="color" />
       </declare-styleable>
</resources>
  1. Mở tệp bố cục activity_main.xml.
  2. Trong DialView, hãy thêm các thuộc tính cho fanColor1, fanColor2fanColor3, rồi đặt giá trị của các thuộc tính đó thành màu sắc như minh hoạ bên dưới. Hãy dùng app: làm tiền tố cho thuộc tính tuỳ chỉnh (như trong app:fanColor1) thay vì android: vì các thuộc tính tuỳ chỉnh của bạn thuộc không gian tên schemas.android.com/apk/res/your_app_package_name chứ không phải không gian tên android.
app:fanColor1="#FFEB3B"
app:fanColor2="#CDDC39"
app:fanColor3="#009688"

Để sử dụng các thuộc tính trong lớp DialView, bạn cần truy xuất các thuộc tính đó. Chúng được lưu trữ trong một AttributeSet, được chuyển cho lớp của bạn khi tạo, nếu có. Bạn truy xuất các thuộc tính trong init và chỉ định các giá trị thuộc tính cho các biến cục bộ để lưu vào bộ nhớ đệm.

  1. Mở tệp lớp DialView.kt.
  2. Bên trong DialView, hãy khai báo các biến để lưu vào bộ nhớ đệm các giá trị thuộc tính.
private var fanSpeedLowColor = 0
private var fanSpeedMediumColor = 0
private var fanSeedMaxColor = 0
  1. Trong khối init, hãy thêm mã sau bằng hàm mở rộng withStyledAttributes. Bạn cung cấp các thuộc tính và khung hiển thị, đồng thời thiết lập các biến cục bộ. Việc nhập withStyledAttributes cũng sẽ nhập hàm getColor() bên phải.
context.withStyledAttributes(attrs, R.styleable.DialView) {
   fanSpeedLowColor = getColor(R.styleable.DialView_fanColor1, 0)
   fanSpeedMediumColor = getColor(R.styleable.DialView_fanColor2, 0)
   fanSeedMaxColor = getColor(R.styleable.DialView_fanColor3, 0)
}
  1. Sử dụng các biến cục bộ trong onDraw() để đặt màu mặt số dựa trên tốc độ quạt hiện tại. Thay thế dòng đặt màu vẽ (paint.color = if (fanSpeed == FanSpeed.OFF) Color.GRAY else Color.GREEN) bằng đoạn mã dưới đây.
paint.color = when (fanSpeed) {
   FanSpeed.OFF -> Color.GRAY
   FanSpeed.LOW -> fanSpeedLowColor
   FanSpeed.MEDIUM -> fanSpeedMediumColor
   FanSpeed.HIGH -> fanSeedMaxColor
} as Int
  1. Chạy ứng dụng, nhấp vào mặt số và chế độ cài đặt màu sẽ khác nhau cho từng vị trí, như minh hoạ bên dưới.

Để tìm hiểu thêm về các thuộc tính khung hiển thị tuỳ chỉnh, hãy xem bài viết Tạo một lớp khung hiển thị.

Hỗ trợ tiếp cận là một bộ kỹ thuật thiết kế, triển khai và kiểm thử giúp mọi người, kể cả người khuyết tật, có thể sử dụng ứng dụng của bạn.

Sau đây là một số chứng khuyết tật thường gặp có thể ảnh hưởng đến khả năng của một người trong việc sử dụng thiết bị Android: mù, thị lực thấp, mù màu, điếc hoặc suy giảm thính giác, và kỹ năng vận động hạn chế. Bằng việc phát triển ứng dụng có khả năng hỗ trợ tiếp cận, bạn có thể cải thiện trải nghiệm người dùng không chỉ cho người dùng khuyết tật mà còn cho tất cả người dùng khác.

Theo mặc định, Android cung cấp một số tính năng hỗ trợ tiếp cận trong các thành phần hiển thị giao diện người dùng tiêu chuẩn, chẳng hạn như TextViewButton. Tuy nhiên, khi tạo một khung hiển thị tuỳ chỉnh, bạn cần cân nhắc cách khung hiển thị tuỳ chỉnh đó sẽ cung cấp các tính năng hỗ trợ tiếp cận, chẳng hạn như nội dung mô tả bằng lời nói về nội dung trên màn hình.

Trong nhiệm vụ này, bạn sẽ tìm hiểu về TalkBack (trình đọc màn hình của Android) và sửa đổi ứng dụng để thêm các gợi ý và nội dung mô tả có thể đọc được cho khung hiển thị tuỳ chỉnh DialView.

Bước 1. Khám phá TalkBack

TalkBack là trình đọc màn hình tích hợp sẵn của Android. Khi TalkBack được bật, người dùng có thể tương tác với thiết bị Android mà không cần nhìn vào màn hình, vì Android sẽ mô tả to các phần tử trên màn hình. Những người dùng bị khiếm thị có thể dùng ứng dụng của bạn thông qua TalkBack.

Trong nhiệm vụ này, bạn sẽ bật TalkBack để hiểu cách trình đọc màn hình hoạt động và cách điều hướng ứng dụng.

  1. Trên thiết bị Android hoặc trình mô phỏng, hãy chuyển đến phần Cài đặt > Hỗ trợ tiếp cận > TalkBack.
  2. Nhấn vào nút bật/tắt Bật/Tắt để bật TalkBack.
  3. Nhấn vào OK để xác nhận các quyền.
  4. Xác nhận mật khẩu thiết bị (nếu được yêu cầu). Nếu đây là lần đầu tiên bạn chạy TalkBack, một hướng dẫn sẽ mở ra. (Hướng dẫn này có thể không dùng được trên các thiết bị cũ.)
  5. Bạn nên di chuyển trong hướng dẫn này khi nhắm mắt. Sau này, nếu muốn mở lại hướng dẫn đó, hãy chuyển đến phần Cài đặt > Hỗ trợ tiếp cận > TalkBack > Cài đặt > Mở hướng dẫn TalkBack.
  6. Biên dịch và chạy ứng dụng CustomFanController hoặc mở ứng dụng này bằng nút Tổng quan hoặc Gần đây trên thiết bị của bạn. Khi TalkBack bật, hãy lưu ý rằng tên của ứng dụng sẽ được thông báo, cũng như văn bản của nhãn TextView ("Fan Control" (Điều khiển quạt)). Tuy nhiên, nếu bạn nhấn vào chính khung hiển thị DialView, thì sẽ không có thông tin nào được đọc về trạng thái của khung hiển thị (chế độ cài đặt hiện tại cho nút xoay) hoặc hành động sẽ diễn ra khi bạn nhấn vào khung hiển thị để kích hoạt.

Bước 2. Thêm nội dung mô tả cho nhãn trên mặt số

Nội dung mô tả nội dung cho biết ý nghĩa và mục đích của các khung hiển thị trong ứng dụng. Các nhãn này cho phép trình đọc màn hình (chẳng hạn như tính năng TalkBack của Android) giải thích chính xác chức năng của từng phần tử. Đối với các khung hiển thị tĩnh như ImageView, bạn có thể thêm nội dung mô tả vào khung hiển thị trong tệp bố cục bằng thuộc tính contentDescription. Khung hiển thị văn bản (TextViewEditText) sẽ tự động dùng văn bản trong khung hiển thị làm nội dung mô tả.

Đối với chế độ xem điều khiển quạt tuỳ chỉnh, bạn cần cập nhật nội dung mô tả một cách linh động mỗi khi người dùng nhấp vào chế độ xem này để cho biết chế độ cài đặt quạt hiện tại.

  1. Ở cuối lớp DialView, hãy khai báo một hàm updateContentDescription() không có đối số hoặc loại dữ liệu trả về.
fun updateContentDescription() {
}
  1. Bên trong updateContentDescription(), hãy thay đổi thuộc tính contentDescription cho khung hiển thị tuỳ chỉnh thành tài nguyên chuỗi được liên kết với tốc độ quạt hiện tại (tắt, 1, 2 hoặc 3). Đây là các nhãn giống như nhãn được dùng trong onDraw() khi mặt số được vẽ trên màn hình.
fun updateContentDescription() {
   contentDescription = resources.getString(fanSpeed.label)
}
  1. Di chuyển lên khối init(), rồi thêm một lệnh gọi đến updateContentDescription() ở cuối khối đó. Thao tác này sẽ khởi tạo nội dung mô tả khi khung hiển thị được khởi tạo.
init {
   isClickable = true
   // ...

   updateContentDescription()
}
  1. Thêm một lệnh gọi khác vào updateContentDescription() trong phương thức performClick(), ngay trước invalidate().
override fun performClick(): Boolean {
   if (super.performClick()) return true
   fanSpeed = fanSpeed.next()
   updateContentDescription()
   invalidate()
   return true
}
  1. Biên dịch và chạy ứng dụng, đồng thời đảm bảo bạn đã bật TalkBack. Nhấn để thay đổi chế độ cài đặt cho chế độ xem mặt số và lưu ý rằng giờ đây TalkBack sẽ thông báo nhãn hiện tại (tắt, 1, 2, 3) cũng như cụm từ "Nhấn đúp để kích hoạt".

Bước 3. Thêm thông tin khác cho thao tác nhấp

Bạn có thể dừng ở đó và khung hiển thị của bạn sẽ dùng được trong TalkBack. Tuy nhiên, sẽ hữu ích nếu khung hiển thị của bạn không chỉ cho biết rằng khung hiển thị đó có thể được kích hoạt ("Nhấn đúp để kích hoạt") mà còn giải thích điều sẽ xảy ra khi khung hiển thị được kích hoạt ("Nhấn đúp để thay đổi" hoặc "Nhấn đúp để đặt lại")

Để làm việc này, bạn sẽ thêm thông tin về thao tác của khung hiển thị (ở đây là thao tác nhấp hoặc nhấn) vào một đối tượng thông tin về nút hỗ trợ tiếp cận, thông qua một uỷ quyền hỗ trợ tiếp cận. Uỷ quyền hỗ trợ tiếp cận cho phép bạn tuỳ chỉnh các tính năng liên quan đến hỗ trợ tiếp cận của ứng dụng thông qua thành phần (thay vì kế thừa).

Đối với tác vụ này, bạn sẽ sử dụng các lớp hỗ trợ tiếp cận trong thư viện Android Jetpack (androidx.*) để đảm bảo khả năng tương thích ngược.

  1. Trong DialView.kt, trong khối init, hãy đặt một uỷ quyền hỗ trợ tiếp cận trên khung hiển thị dưới dạng một đối tượng AccessibilityDelegateCompat mới. Nhập androidx.core.view.ViewCompatandroidx.core.view.AccessibilityDelegateCompat khi được yêu cầu. Chiến lược này cho phép mức độ tương thích ngược cao nhất trong ứng dụng của bạn.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   
})
  1. Bên trong đối tượng AccessibilityDelegateCompat, hãy ghi đè hàm onInitializeAccessibilityNodeInfo() bằng đối tượng AccessibilityNodeInfoCompat và gọi phương thức của siêu dữ liệu. Nhập androidx.core.view.accessibility.AccessibilityNodeInfoCompat khi được nhắc.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   override fun onInitializeAccessibilityNodeInfo(host: View, 
                            info: AccessibilityNodeInfoCompat) {
      super.onInitializeAccessibilityNodeInfo(host, info)

   }  
})

Mỗi khung hiển thị đều có một cây gồm các nút hỗ trợ tiếp cận, có thể tương ứng hoặc không tương ứng với các thành phần bố cục thực tế của khung hiển thị. Các dịch vụ hỗ trợ tiếp cận của Android điều hướng các nút đó để tìm hiểu thông tin về khung hiển thị (chẳng hạn như nội dung mô tả có thể đọc được hoặc các thao tác có thể thực hiện trên khung hiển thị đó). Khi tạo một khung hiển thị tuỳ chỉnh, bạn cũng có thể cần ghi đè thông tin về nút để cung cấp thông tin tuỳ chỉnh cho khả năng tiếp cận. Trong trường hợp này, bạn sẽ ghi đè thông tin về nút để cho biết rằng có thông tin tuỳ chỉnh cho thao tác của khung hiển thị.

  1. Bên trong onInitializeAccessibilityNodeInfo(), hãy tạo một đối tượng AccessibilityNodeInfoCompat.AccessibilityActionCompat mới rồi chỉ định đối tượng đó cho biến customClick. Truyền hằng số AccessibilityNodeInfo.ACTION_CLICK và một chuỗi phần giữ chỗ vào hàm khởi tạo. Nhập AccessibilityNodeInfo khi có yêu cầu.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   override fun onInitializeAccessibilityNodeInfo(host: View, 
                            info: AccessibilityNodeInfoCompat) {
      super.onInitializeAccessibilityNodeInfo(host, info)
      val customClick = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
         AccessibilityNodeInfo.ACTION_CLICK,
        "placeholder"
      )
   }  
})

Lớp AccessibilityActionCompat biểu thị một thao tác trên khung hiển thị cho mục đích hỗ trợ tiếp cận. Một thao tác điển hình là thao tác nhấp hoặc nhấn, như bạn sử dụng ở đây, nhưng các thao tác khác có thể bao gồm việc lấy hoặc mất tiêu điểm, thao tác trên bảng nhớ tạm (cắt/sao chép/dán) hoặc cuộn trong khung hiển thị. Hàm khởi tạo cho lớp này yêu cầu một hằng số hành động (ở đây là AccessibilityNodeInfo.ACTION_CLICK) và một chuỗi mà TalkBack dùng để cho biết hành động là gì.

  1. Thay thế chuỗi "placeholder" bằng một lệnh gọi đến context.getString() để truy xuất một tài nguyên chuỗi. Đối với tài nguyên cụ thể, hãy kiểm tra tốc độ quạt hiện tại. Nếu tốc độ hiện tại là FanSpeed.HIGH, thì chuỗi sẽ là "Reset". Nếu tốc độ quạt là một giá trị khác, chuỗi sẽ là "Change." Bạn sẽ tạo các tài nguyên chuỗi này ở bước sau.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   override fun onInitializeAccessibilityNodeInfo(host: View, 
                            info: AccessibilityNodeInfoCompat) {
      super.onInitializeAccessibilityNodeInfo(host, info)
      val customClick = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
         AccessibilityNodeInfo.ACTION_CLICK,
        context.getString(if (fanSpeed !=  FanSpeed.HIGH) R.string.change else R.string.reset)
      )
   }  
})
  1. Sau dấu ngoặc đơn đóng cho định nghĩa customClick, hãy dùng phương thức addAction() để thêm thao tác hỗ trợ tiếp cận mới vào đối tượng thông tin về nút.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   override fun onInitializeAccessibilityNodeInfo(host: View, 
                            info: AccessibilityNodeInfoCompat) {
       super.onInitializeAccessibilityNodeInfo(host, info)
       val customClick = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
           AccessibilityNodeInfo.ACTION_CLICK,
           context.getString(if (fanSpeed !=  FanSpeed.HIGH) 
                                 R.string.change else R.string.reset)
       )
       info.addAction(customClick)
   }
})
  1. Trong res/values/strings.xml, hãy thêm tài nguyên chuỗi cho "Thay đổi" và "Đặt lại".
<string name="change">Change</string>
<string name="reset">Reset</string>
  1. Biên dịch và chạy ứng dụng, đồng thời đảm bảo bạn đã bật TalkBack. Lưu ý rằng giờ đây, cụm từ "Nhấn đúp để kích hoạt" sẽ là "Nhấn đúp để thay đổi" (nếu tốc độ quạt thấp hơn mức cao hoặc 3) hoặc "Nhấn đúp để đặt lại" (nếu tốc độ quạt đã ở mức cao hoặc 3). Xin lưu ý rằng lời nhắc "Nhấn đúp để..." do chính dịch vụ TalkBack cung cấp.

Tải mã xuống cho lớp học lập trình đã hoàn thành.

$  git clone https://github.com/googlecodelabs/android-kotlin-drawing-custom-views


Ngoài ra, bạn có thể tải kho lưu trữ xuống dưới dạng tệp ZIP, sau đó giải nén và mở tệp đó trong Android Studio.

Tải tệp Zip xuống

  • Để tạo một khung hiển thị tuỳ chỉnh kế thừa giao diện và hành vi của một lớp con View, chẳng hạn như EditText, hãy thêm một lớp mới mở rộng lớp con đó và điều chỉnh bằng cách ghi đè một số phương thức của lớp con.
  • Để tạo một khung hiển thị tuỳ chỉnh có kích thước và hình dạng bất kỳ, hãy thêm một lớp mới mở rộng View.
  • Ghi đè các phương thức View, chẳng hạn như onDraw() để xác định hình dạng và giao diện cơ bản của khung hiển thị.
  • Sử dụng invalidate() để buộc vẽ hoặc vẽ lại khung hiển thị.
  • Để tối ưu hoá hiệu suất, hãy phân bổ các biến và chỉ định mọi giá trị cần thiết để vẽ và tô màu trước khi dùng các biến đó trong onDraw(), chẳng hạn như trong quá trình khởi chạy các biến thành phần.
  • Ghi đè performClick() thay vì OnClickListener() cho khung hiển thị tuỳ chỉnh để cung cấp hành vi tương tác của khung hiển thị. Điều này cho phép bạn hoặc những nhà phát triển Android khác có thể sử dụng lớp khung hiển thị tuỳ chỉnh của bạn để dùng onClickListener() nhằm cung cấp thêm hành vi.
  • Thêm thành phần hiển thị tuỳ chỉnh vào tệp bố cục XML bằng các thuộc tính để xác định giao diện của thành phần hiển thị đó, giống như cách bạn làm với các thành phần khác trên giao diện người dùng.
  • Tạo tệp attrs.xml trong thư mục values để xác định các thuộc tính tuỳ chỉnh. Sau đó, bạn có thể dùng các thuộc tính tuỳ chỉnh cho khung hiển thị tuỳ chỉnh trong tệp bố cục XML.

Khoá học của Udacity:

Tài liệu dành cho nhà phát triển Android:

Video:

Phần này liệt kê các bài tập về nhà cho học viên của lớp học lập trình này trong phạm vi khoá học có người hướng dẫn. Người hướng dẫn phải thực hiện các việc sau đây:

  • Giao bài tập về nhà nếu cần.
  • Trao đổi với học viên về cách nộp bài tập về nhà.
  • Chấm điểm bài tập về nhà.

Người hướng dẫn có thể sử dụng các đề xuất này ít hoặc nhiều tuỳ ý và nên giao cho học viên bất kỳ bài tập về nhà nào khác mà họ cảm thấy phù hợp.

Nếu bạn đang tự học các lớp học lập trình, hãy sử dụng những bài tập về nhà này để kiểm tra kiến thức của mình.

Câu hỏi 1

Để tính toán vị trí, kích thước và mọi giá trị khác khi khung hiển thị tuỳ chỉnh được chỉ định kích thước lần đầu tiên, bạn sẽ ghi đè phương thức nào?

onMeasure()

onSizeChanged()

invalidate()

onDraw()

Câu hỏi 2

Để cho biết bạn muốn thành phần hiển thị được vẽ lại bằng onDraw(), bạn sẽ gọi phương thức nào từ luồng giao diện người dùng sau khi giá trị thuộc tính thay đổi?

▢ onMeasure()

▢ onSizeChanged()

▢ invalidate()

▢ getVisibility()

Câu hỏi 3

Bạn nên ghi đè phương thức View nào để thêm tính năng tương tác vào khung hiển thị tuỳ chỉnh?

▢ setOnClickListener()

▢ onSizeChanged()

▢ isClickable()

▢ performClick()

Để biết đường liên kết đến các lớp học lập trình khác trong khoá học này, hãy xem trang đích của các lớp học lập trình trong khoá học Kiến thức nâng cao về cách tạo ứng dụng Android bằng Kotlin.