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
Ứ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ư Button
và TextView
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 conView
(chẳng hạn nhưButton
hoặcEditText
). - 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ứcView
nhưonDraw()
và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
- 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
. - Mở
activity_main.xml
trong thẻ Text (Văn bản) để chỉnh sửa mã XML. - 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"/>
- 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"/>
- 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.
- 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
- Tạo một lớp Kotlin mới có tên là
DialView
. - Sửa đổi định nghĩa lớp để mở rộng
View
. Nhậpandroid.view.View
khi được nhắc. - 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ớpView
. 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) {
- 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ộtenum
cấp cao nhất để biểu thị tốc độ quạt hiện có. Xin lưu ý rằngenum
này thuộc loạiInt
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);
}
- 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
- 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ậpandroid.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ể.
- Cũng trong định nghĩa lớp
DialView
, hãy khởi tạo một đối tượngPaint
bằng một số kiểu cơ bản. Nhậpandroid.graphics.Paint
vàandroid.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)
}
- 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ượngCanvas
được tạo kiểu bằng đối tượngPaint
. - 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 đếnonDraw()
để 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 Canvas
và Paint
cung cấp một số phím tắt vẽ hữu ích:
- Vẽ văn bản bằng
drawText()
. Chỉ định kiểu chữ bằng cách gọisetTypeface()
và màu văn bản bằng cách gọisetColor()
. - Vẽ các hình dạng cơ bản bằng
drawRect()
,drawOval()
vàdrawArc()
. Thay đổi xem các hình dạng được tô màu, có đường viền hay cả hai bằng cách gọisetStyle()
. - Vẽ bitmap bằng
drawBitmap()
.
Bạn sẽ tìm hiểu thêm về Canvas
và Paint
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()
và 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ị
- Trong lớp
DialView
, bên dưới các hoạt động khởi tạo, hãy ghi đè phương thứconSizeChanged()
từ lớpView
để tính toán kích thước cho mặt số của khung hiển thị tuỳ chỉnh. Nhậpkotlin
.math.min
khi có yêu cầu.
Phương thứconSizeChanged()
đượ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ùngonSizeChanged()
để 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()
}
- Bên dưới
onSizeChanged()
, hãy thêm mã này để xác định một hàm mở rộngcomputeXYForSpeed()
cho lớpPointF
. Nhậpkotlin.math.cos
vàkotlin.math.sin
khi được yêu cầu. Hàm mở rộng này trên lớpPointF
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 trongonDraw().
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
}
- 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ớpCanvas
vàPaint
. Nhậpandroid.graphics.Canvas
khi có yêu cầu. Đây là chế độ ghi đè khung:
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
}
- 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ậpandroid.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
- 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ínhwidth
vàheight
là thành phần của siêu lớpView
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)
- 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ụngPointF
.Phương thức mở rộngcomputeXYforSpeed()
để 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)
- 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ượngpointPosition
mỗi lần để tránh phân bổ. Sử dụngdrawText()
để 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.
- Trong
activity_main.xml
, hãy thay đổi thẻImageView
chodialView
thànhcom.example.android.customfancontroller.DialView
và xoá thuộc tínhandroid:background
. CảDialView
vàImageView
ban đầu đều kế thừa các thuộc tính tiêu chuẩn từ lớpView
, 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" />
- 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ànhtrue
. Đ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ớpView
để 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ứconDraw()
để 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.
- Trong
DialView.kt
, bên trong quá trình liệt kêFanSpeed
, hãy thêm một hàm mở rộngnext()
để thay đổi tốc độ quạt hiện tại thành tốc độ tiếp theo trong danh sách (từOFF
sangLOW
,MEDIUM
vàHIGH
, rồi quay lạiOFF
). 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
}
}
- Bên trong lớp
DialView
, ngay trước phương thứconSizeChanged()
, hãy thêm một khốiinit()
. Việc đặt thuộc tínhisClickable
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
}
- Bên dưới
init(),
, hãy ghi đè phương thứcperformClick()
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().
- 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.
- Tạo và mở
res/values/attrs.xml
. - Bên trong
<resources>
, hãy thêm một phần tử tài nguyên<declare-styleable>
. - 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óname
vàformat
.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>
- Mở tệp bố cục
activity_main.xml
. - Trong
DialView
, hãy thêm các thuộc tính chofanColor1
,fanColor2
vàfanColor3
, 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ùngapp:
làm tiền tố cho thuộc tính tuỳ chỉnh (như trongapp:fanColor1
) thay vìandroid:
vì các thuộc tính tuỳ chỉnh của bạn thuộc không gian tênschemas.android.com/apk/res/
your_app_package_name
chứ không phải không gian tênandroid
.
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.
- Mở tệp lớp
DialView.kt
. - 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
- Trong khối
init
, hãy thêm mã sau bằng hàm mở rộngwithStyledAttributes
. 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ậpwithStyledAttributes
cũng sẽ nhập hàmgetColor()
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)
}
- 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
- 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ư TextView
và Button
. 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.
- 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.
- Nhấn vào nút bật/tắt Bật/Tắt để bật TalkBack.
- Nhấn vào OK để xác nhận các quyền.
- 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ũ.)
- 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.
- 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ãnTextView
("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 (TextView
và EditText
) 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.
- Ở cuối lớp
DialView
, hãy khai báo một hàmupdateContentDescription()
không có đối số hoặc loại dữ liệu trả về.
fun updateContentDescription() {
}
- Bên trong
updateContentDescription()
, hãy thay đổi thuộc tínhcontentDescription
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 trongonDraw()
khi mặt số được vẽ trên màn hình.
fun updateContentDescription() {
contentDescription = resources.getString(fanSpeed.label)
}
- Di chuyển lên khối
init()
, rồi thêm một lệnh gọi đếnupdateContentDescription()
ở 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()
}
- Thêm một lệnh gọi khác vào
updateContentDescription()
trong phương thứcperformClick()
, ngay trướcinvalidate()
.
override fun performClick(): Boolean {
if (super.performClick()) return true
fanSpeed = fanSpeed.next()
updateContentDescription()
invalidate()
return true
}
- 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.
- Trong
DialView.kt
, trong khốiinit
, 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ượngAccessibilityDelegateCompat
mới. Nhậpandroidx.core.view.ViewCompat
vàandroidx.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() {
})
- Bên trong đối tượng
AccessibilityDelegateCompat
, hãy ghi đè hàmonInitializeAccessibilityNodeInfo()
bằng đối tượngAccessibilityNodeInfoCompat
và gọi phương thức của siêu dữ liệu. Nhậpandroidx.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ị.
- Bên trong
onInitializeAccessibilityNodeInfo()
, hãy tạo một đối tượngAccessibilityNodeInfoCompat.AccessibilityActionCompat
mới rồi chỉ định đối tượng đó cho biếncustomClick
. 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ậpAccessibilityNodeInfo
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ì.
- Thay thế chuỗi
"placeholder"
bằng một lệnh gọi đếncontext.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)
)
}
})
- Sau dấu ngoặc đơn đóng cho định nghĩa
customClick
, hãy dùng phương thứcaddAction()
để 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)
}
})
- 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>
- 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ạ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ùngonClickListener()
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ụcvalues
để 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:
- Tạo thành phần hiển thị tuỳ chỉnh
@JvmOverloads
- Thành phần tuỳ chỉnh
- Cách Android thu hút lượt xem
onMeasure()
onSizeChanged()
onDraw()
Canvas
Paint
drawText()
setTypeface()
setColor()
drawRect()
drawOval()
drawArc()
drawBitmap()
setStyle()
invalidate()
- Xem
- Sự kiện đầu vào
- Paint
- Thư viện tiện ích Kotlin android-ktx
withStyledAttributes
- Tài liệu về Android KTX
- Blog thông báo ban đầu về Android KTX
- Tăng khả năng tiếp cận cho khung hiển thị tuỳ chỉnh
AccessibilityDelegateCompat
AccessibilityNodeInfoCompat
AccessibilityNodeInfoCompat.AccessibilityActionCompat
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.