Có một số thuật ngữ và khái niệm quan trọng cần hiểu rõ khi xử lý cử chỉ trong một ứng dụng. Trang này giải thích các điều khoản con trỏ, sự kiện con trỏ và cử chỉ, đồng thời giới thiệu các mô hình trừu tượng cấp độ cho cử chỉ. Báo cáo này cũng đi sâu vào việc tiêu thụ sự kiện và truyền tải.
Định nghĩa
Để hiểu các khái niệm khác nhau trên trang này, bạn cần hiểu một số khái niệm thuật ngữ được sử dụng:
- Con trỏ: Một đối tượng thực mà bạn có thể dùng để tương tác với ứng dụng.
Đối với thiết bị di động, con trỏ phổ biến nhất là ngón tay bạn tương tác với
màn hình cảm ứng. Hoặc, bạn có thể dùng bút cảm ứng để thay thế ngón tay.
Đối với màn hình lớn, bạn có thể sử dụng chuột hoặc bàn di chuột để tương tác gián tiếp với
màn hình. Thiết bị đầu vào phải có khả năng "trỏ" tại một toạ độ để
được coi là con trỏ, vì vậy, chẳng hạn như bàn phím, bàn phím không thể được coi là một con trỏ
con trỏ. Trong Compose, loại con trỏ được đưa vào các thay đổi về con trỏ bằng cách sử dụng
PointerType
. - Sự kiện con trỏ: Mô tả tương tác cấp thấp của một hoặc nhiều con trỏ
với ứng dụng tại một thời điểm nhất định. Bất kỳ tương tác nào của con trỏ, chẳng hạn như đặt
một ngón tay trên màn hình hoặc kéo chuột, sẽ kích hoạt sự kiện. Trong
Compose, tất cả thông tin liên quan đến sự kiện như vậy đều có trong
Lớp
PointerEvent
. - Cử chỉ: Một chuỗi các sự kiện con trỏ có thể được hiểu là một thao tác đơn hành động. Ví dụ: cử chỉ nhấn có thể được coi là một chuỗi thao tác nhấn xuống sự kiện diễn ra theo sau là sự kiện lên. Có những cử chỉ phổ biến được nhiều người sử dụng các ứng dụng, chẳng hạn như nhấn, kéo hoặc biến đổi, nhưng bạn cũng có thể tạo các ứng dụng tuỳ chỉnh của riêng mình cử chỉ khi cần.
Các mức độ trừu tượng khác nhau
Jetpack Compose cung cấp nhiều cấp độ trừu tượng để xử lý cử chỉ.
Ở cấp cao nhất là hỗ trợ thành phần. Các thành phần kết hợp như Button
tự động bao gồm tính năng hỗ trợ cử chỉ. Để thêm tính năng hỗ trợ cử chỉ vào danh sách tuỳ chỉnh
thành phần, bạn có thể thêm đối tượng sửa đổi cử chỉ như clickable
vào
thành phần kết hợp. Cuối cùng, nếu cần một cử chỉ tuỳ chỉnh, bạn có thể sử dụng
Đối tượng sửa đổi pointerInput
.
Theo quy tắc, hãy xây dựng ứng dụng ở mức độ trừu tượng cao nhất,
chức năng bạn cần. Bằng cách này, bạn được hưởng lợi từ các phương pháp hay nhất có trong
trong lớp. Ví dụ: Button
chứa nhiều thông tin ngữ nghĩa hơn dùng cho
khả năng truy cập, hơn clickable
, chứa nhiều thông tin hơn so với dữ liệu thô
Triển khai pointerInput
.
Hỗ trợ thành phần
Nhiều thành phần có sẵn trong Compose có một số loại cử chỉ nội bộ
xử lý. Ví dụ: LazyColumn
phản hồi các cử chỉ kéo bằng cách
đang cuộn nội dung, Button
sẽ cho thấy một gợn sóng khi bạn nhấn xuống
và thành phần SwipeToDismiss
chứa logic vuốt để đóng
. Kiểu xử lý cử chỉ này hoạt động tự động.
Bên cạnh tính năng xử lý cử chỉ nội bộ, nhiều thành phần cũng yêu cầu phương thức gọi
xử lý cử chỉ. Ví dụ: Button
tự động phát hiện lượt nhấn
và kích hoạt một sự kiện nhấp chuột. Bạn truyền một hàm lambda onClick
vào Button
để
thể hiện cảm xúc với cử chỉ. Tương tự, bạn thêm hàm lambda onValueChange
vào
Slider
để phản ứng với việc người dùng kéo ô điều khiển thanh trượt.
Khi trường hợp này phù hợp với trường hợp sử dụng của bạn, hãy ưu tiên sử dụng các cử chỉ có trong thành phần vì chúng
tích hợp sẵn tính năng hỗ trợ lấy nét và hỗ trợ tiếp cận, đồng thời chúng là
được thử nghiệm kỹ càng. Ví dụ: Button
được đánh dấu theo cách đặc biệt để
dịch vụ hỗ trợ tiếp cận sẽ mô tả chính xác nút đó dưới dạng một nút chứ không chỉ bất kỳ nút
phần tử có thể nhấp:
// Talkback: "Click me!, Button, double tap to activate" Button(onClick = { /* TODO */ }) { Text("Click me!") } // Talkback: "Click me!, double tap to activate" Box(Modifier.clickable { /* TODO */ }) { Text("Click me!") }
Để tìm hiểu thêm về tính năng hỗ trợ tiếp cận trong Compose, hãy xem bài viết Hỗ trợ tiếp cận trong Soạn thư.
Thêm cử chỉ cụ thể vào thành phần kết hợp tuỳ ý bằng đối tượng sửa đổi
Bạn có thể áp dụng các đối tượng sửa đổi cử chỉ cho bất kỳ thành phần kết hợp tuỳ ý nào để thực hiện
thành phần kết hợp nghe cử chỉ. Ví dụ: bạn có thể để Box
chung
xử lý các cử chỉ nhấn bằng cách làm cho ứng dụng đó clickable
hoặc cho phép Column
xử lý thao tác cuộn dọc bằng cách áp dụng verticalScroll
.
Có nhiều đối tượng sửa đổi để xử lý nhiều loại cử chỉ:
- Xử lý thao tác nhấn và nhấn bằng
clickable
.combinedClickable
,selectable
,toggleable
vàtriStateToggleable
đối tượng sửa đổi. - Xử lý thao tác cuộn bằng
horizontalScroll
,verticalScroll
và các đối tượng sửa đổiscrollable
chung khác. - Xử lý thao tác kéo bằng
draggable
vàswipeable
đối tượng sửa đổi. - Xử lý cử chỉ nhiều điểm chạm như kéo, xoay và thu phóng bằng
đối tượng sửa đổi
transformable
.
Theo quy tắc, hãy ưu tiên sử dụng các đối tượng sửa đổi cử chỉ ngay lập tức thay vì cách xử lý cử chỉ tuỳ chỉnh.
Đối tượng sửa đổi bổ sung thêm chức năng ngoài việc xử lý sự kiện con trỏ thuần tuý.
Ví dụ: đối tượng sửa đổi clickable
không chỉ thêm tính năng phát hiện thao tác nhấn và
mà còn thêm thông tin ngữ nghĩa, chỉ báo trực quan về hoạt động tương tác,
di chuột, lấy nét và hỗ trợ bàn phím. Bạn có thể xem mã nguồn
của clickable
để xem chức năng
đang được thêm.
Thêm cử chỉ tuỳ chỉnh vào các thành phần kết hợp tuỳ ý bằng đối tượng sửa đổi pointerInput
Không phải cử chỉ nào cũng được triển khai bằng đối tượng sửa đổi cử chỉ có sẵn. Để
ví dụ: bạn không thể sử dụng đối tượng sửa đổi để phản ứng với thao tác kéo sau khi nhấn và giữ,
nhấn giữ phím Ctrl hoặc nhấn bằng 3 ngón tay. Thay vào đó, bạn có thể viết cử chỉ của riêng mình
trình xử lý để xác định các cử chỉ tùy chỉnh này. Bạn có thể tạo một trình xử lý cử chỉ bằng
đối tượng sửa đổi pointerInput
, cho phép bạn truy cập vào con trỏ thô
các sự kiện.
Mã sau đây theo dõi các sự kiện con trỏ thô:
@Composable private fun LogPointerEvents(filter: PointerEventType? = null) { var log by remember { mutableStateOf("") } Column { Text(log) Box( Modifier .size(100.dp) .background(Color.Red) .pointerInput(filter) { awaitPointerEventScope { while (true) { val event = awaitPointerEvent() // handle pointer event if (filter == null || event.type == filter) { log = "${event.type}, ${event.changes.first().position}" } } } } ) } }
Nếu bạn chia nhỏ đoạn mã này, các thành phần cốt lõi là:
- Đối tượng sửa đổi
pointerInput
. Bạn truyền một hoặc nhiều khoá. Khi giá trị của một trong các khoá đó thay đổi, thì hàm lambda nội dung của đối tượng sửa đổi là thực thi lại. Mẫu này sẽ truyền một bộ lọc không bắt buộc đến thành phần kết hợp. Nếu giá trị của bộ lọc đó thay đổi, thì trình xử lý sự kiện con trỏ sẽ là thực thi lại để đảm bảo ghi lại các sự kiện phù hợp. awaitPointerEventScope
tạo một phạm vi coroutine có thể dùng để chờ các sự kiện con trỏ.awaitPointerEvent
tạm ngưng coroutine cho đến khi xảy ra một sự kiện con trỏ tiếp theo xảy ra.
Mặc dù việc theo dõi các sự kiện đầu vào thô rất hữu ích, nhưng việc viết cũng rất phức tạp một cử chỉ tuỳ chỉnh dựa trên dữ liệu thô này. Để đơn giản hoá việc tạo báo cáo tuỳ chỉnh cử chỉ, có sẵn nhiều phương thức tiện ích.
Phát hiện toàn bộ cử chỉ
Thay vì xử lý các sự kiện con trỏ thô, bạn có thể theo dõi các cử chỉ cụ thể
xảy ra và phản hồi thích hợp. AwaitPointerEventScope
cung cấp
để nghe:
- Nhấn, nhấn, nhấn đúp và nhấn và giữ:
detectTapGestures
- Kéo:
detectHorizontalDragGestures
,detectVerticalDragGestures
,detectDragGestures
vàdetectDragGesturesAfterLongPress
- Biến đổi:
detectTransformGestures
Đây là các trình phát hiện cấp cao nhất, vì vậy, bạn không thể thêm nhiều trình phát hiện trong cùng một trình phát hiện
Đối tượng sửa đổi pointerInput
. Đoạn mã sau đây chỉ phát hiện các lần nhấn, chứ không phát hiện
thao tác kéo:
var log by remember { mutableStateOf("") } Column { Text(log) Box( Modifier .size(100.dp) .background(Color.Red) .pointerInput(Unit) { detectTapGestures { log = "Tap!" } // Never reached detectDragGestures { _, _ -> log = "Dragging" } } ) }
Trong nội bộ, phương thức detectTapGestures
sẽ chặn coroutine này, còn phương thức thứ hai
trình phát hiện không bao giờ tiếp cận được. Nếu bạn cần thêm nhiều trình nghe cử chỉ để
một thành phần kết hợp, hãy sử dụng các thực thể sửa đổi pointerInput
riêng biệt:
var log by remember { mutableStateOf("") } Column { Text(log) Box( Modifier .size(100.dp) .background(Color.Red) .pointerInput(Unit) { detectTapGestures { log = "Tap!" } } .pointerInput(Unit) { // These drag events will correctly be triggered detectDragGestures { _, _ -> log = "Dragging" } } ) }
Xử lý sự kiện cho mỗi cử chỉ
Theo định nghĩa, các cử chỉ bắt đầu bằng một sự kiện trỏ xuống. Bạn có thể sử dụng
awaitEachGesture
thay vì vòng lặp while(true)
chuyển qua từng sự kiện thô. Phương thức awaitEachGesture
sẽ khởi động lại
khối chứa khi tất cả các con trỏ đã được nâng lên, cho biết cử chỉ
đã hoàn tất:
@Composable private fun SimpleClickable(onClick: () -> Unit) { Box( Modifier .size(100.dp) .pointerInput(onClick) { awaitEachGesture { awaitFirstDown().also { it.consume() } val up = waitForUpOrCancellation() if (up != null) { up.consume() onClick() } } } ) }
Trên thực tế, hầu như lúc nào bạn cũng muốn sử dụng awaitEachGesture
, trừ phi bạn muốn
phản hồi các sự kiện con trỏ mà không xác định cử chỉ. Ví dụ:
hoverable
không phản hồi các sự kiện trỏ xuống hoặc lên — mà chỉ
cần biết khi nào một con trỏ tiến vào hoặc thoát khỏi giới hạn của nó.
Chờ một sự kiện hoặc cử chỉ phụ cụ thể
Có một tập hợp phương pháp giúp xác định các phần phổ biến của cử chỉ:
- Tạm ngưng cho đến khi con trỏ di chuyển xuống bằng
awaitFirstDown
hoặc đợi tất cả con trỏ để di chuyển lên vớiwaitForUpOrCancellation
. - Tạo trình nghe kéo cấp thấp bằng
awaitTouchSlopOrCancellation
vàawaitDragOrCancellation
. Trước tiên, trình xử lý cử chỉ sẽ tạm ngưng cho đến con trỏ tiếp cận điểm chạm rồi tạm ngưng cho đến khi xảy ra sự kiện kéo đầu tiên là thành công. Nếu bạn chỉ quan tâm đến lực kéo dọc theo một trục, hãy sử dụngawaitHorizontalTouchSlopOrCancellation
+awaitHorizontalDragOrCancellation
hoặcawaitVerticalTouchSlopOrCancellation
trở lên Thay vào đó, hãyawaitVerticalDragOrCancellation
. - Tạm ngưng cho đến khi nhấn và giữ xảy ra với
awaitLongPressOrCancellation
. - Sử dụng phương thức
drag
để liên tục nghe các sự kiện kéo, hoặchorizontalDrag
hoặcverticalDrag
để nghe các sự kiện kéo trên một thiết bị Trục.
Áp dụng phép tính cho sự kiện nhiều điểm chạm
Khi người dùng thực hiện cử chỉ nhiều điểm chạm bằng nhiều con trỏ,
nên việc hiểu phép biến đổi cần thiết dựa trên các giá trị thô rất phức tạp.
Nếu đối tượng sửa đổi transformable
hoặc detectTransformGestures
có thể không cung cấp đủ quyền kiểm soát chi tiết cho trường hợp sử dụng của bạn, thì bạn có thể
theo dõi các sự kiện thô và áp dụng các phép tính dựa trên các sự kiện đó. Sau đây là các phương thức trợ giúp
là calculateCentroid
, calculateCentroidSize
,
calculatePan
, calculateRotation
và calculateZoom
.
Điều phối sự kiện và thử nghiệm nhấn
Không phải mọi sự kiện con trỏ đều được gửi đến mọi đối tượng sửa đổi pointerInput
. Sự kiện
công việc điều phối như sau:
- Các sự kiện con trỏ được gửi đến hệ phân cấp thành phần kết hợp. Khoảnh khắc mà con trỏ mới kích hoạt sự kiện con trỏ đầu tiên, hệ thống bắt đầu kiểm thử nhấn trạng thái "đủ điều kiện" thành phần kết hợp. Một thành phần kết hợp được coi là đủ điều kiện khi khả năng xử lý phương thức nhập con trỏ. Quy trình kiểm thử lượt truy cập từ đầu giao diện người dùng xuống dưới cùng. Thành phần kết hợp là "lượt truy cập" thời điểm xảy ra sự kiện con trỏ trong giới hạn của thành phần kết hợp đó. Quá trình này dẫn đến một chuỗi thành phần kết hợp giúp kiểm thử tích cực.
- Theo mặc định, khi có nhiều thành phần kết hợp đủ điều kiện ở cùng cấp độ
cây này, chỉ thành phần kết hợp có chỉ mục z cao nhất là "lượt truy cập". Để
ví dụ: khi bạn thêm hai thành phần kết hợp
Button
trùng lặp vào mộtBox
, chỉ mục được vẽ ở trên cùng sẽ nhận được mọi sự kiện con trỏ. Về mặt lý thuyết, bạn có thể ghi đè hành vi này bằng cách tạoPointerInputModifierNode
của riêng bạn triển khai và đặtsharePointerInputWithSiblings
thành true. - Các sự kiện khác cho cùng một con trỏ sẽ được gửi đến cùng một chuỗi thành phần kết hợp và luồng theo logic truyền sự kiện. Hệ thống sẽ không thực hiện thêm bất kỳ thử nghiệm nhấn nào đối với con trỏ này. Điều này có nghĩa là mỗi thành phần kết hợp trong chuỗi nhận tất cả sự kiện cho con trỏ đó, ngay cả khi những giá trị nằm ngoài giới hạn của thành phần kết hợp đó. Các thành phần kết hợp không được trong chuỗi không bao giờ nhận sự kiện con trỏ, ngay cả khi con trỏ nằm trong giới hạn của chúng.
Các sự kiện di chuột được kích hoạt bằng thao tác di chuột bằng chuột hoặc bút cảm ứng là một trường hợp ngoại lệ đối với quy tắc xác định tại đây. Sự kiện di chuột được gửi đến bất kỳ thành phần kết hợp nào mà sự kiện đó nhấn vào. Loại đối thủ sau lượt đánh bóng khi người dùng di con trỏ từ ranh giới của một thành phần kết hợp đến thành phần kết hợp tiếp theo, thay vì gửi sự kiện đến thành phần kết hợp đầu tiên, sự kiện sẽ được gửi đến thành phần kết hợp mới.
Tiêu thụ sự kiện
Khi nhiều thành phần kết hợp được chỉ định một trình xử lý cử chỉ, các trình xử lý không nên xung đột. Ví dụ: hãy xem giao diện người dùng này:
Khi người dùng nhấn vào nút dấu trang, hàm lambda onClick
của nút này sẽ xử lý
cử chỉ. Khi người dùng nhấn vào bất kỳ phần nào khác của mục danh sách, ListItem
xử lý cử chỉ đó và chuyển đến bài viết. Về phương thức nhập con trỏ,
Nút phải sử dụng sự kiện này để phần tử mẹ của nút biết không
phản ứng với nó nữa. Cử chỉ có trong các thành phần có sẵn và
các đối tượng sửa đổi cử chỉ phổ biến bao gồm hành vi sử dụng này, nhưng nếu bạn
viết cử chỉ tuỳ chỉnh của riêng mình, bạn phải sử dụng sự kiện theo cách thủ công. Bạn thực hiện việc này
bằng phương thức PointerInputChange.consume
:
Modifier.pointerInput(Unit) { awaitEachGesture { while (true) { val event = awaitPointerEvent() // consume all changes event.changes.forEach { it.consume() } } } }
Việc tiêu thụ một sự kiện sẽ không dừng việc truyền sự kiện sang các thành phần kết hợp khác. Đáp thay vào đó, thành phần kết hợp cần bỏ qua các sự kiện đã sử dụng một cách rõ ràng. Khi viết cử chỉ tuỳ chỉnh, bạn nên kiểm tra xem một sự kiện đã được người dùng khác sử dụng hay chưa phần tử:
Modifier.pointerInput(Unit) { awaitEachGesture { while (true) { val event = awaitPointerEvent() if (event.changes.any { it.isConsumed }) { // A pointer is consumed by another gesture handler } else { // Handle unconsumed event } } } }
Quảng bá sự kiện
Như đã đề cập trước đó, các thay đổi về con trỏ được truyền đến từng thành phần kết hợp mà nó truy cập.
Nhưng nếu có nhiều thành phần kết hợp như vậy tồn tại, các sự kiện này diễn ra theo thứ tự nào
phổ biến? Nếu bạn lấy ví dụ từ phần trước, giao diện người dùng này sẽ được dịch thành
cây giao diện người dùng sau đây, trong đó chỉ ListItem
và Button
phản hồi
sự kiện con trỏ:
Các sự kiện con trỏ sẽ truyền qua mỗi thành phần kết hợp này 3 lần, trong 3 lần "thẻ":
- Trong Lượt truyền ban đầu, sự kiện di chuyển từ đầu cây giao diện người dùng đến
dưới cùng. Quy trình này cho phép phần tử mẹ chặn một sự kiện trước khi nhà xuất bản con có thể
sử dụng nó. Ví dụ: chú giải công cụ cần chặn
nhấn và giữ thay vì truyền cho con. Trong
ví dụ:
ListItem
nhận được sự kiện trướcButton
. - Trong Thẻ chính, sự kiện di chuyển từ các nút lá của cây giao diện người dùng cho đến
gốc của cây giao diện người dùng. Giai đoạn này là lúc bạn thường sử dụng cử chỉ và
thẻ mặc định khi theo dõi sự kiện. Xử lý các cử chỉ trong thẻ này
có nghĩa là các nút lá được ưu tiên hơn nút mẹ, tức là nút
hành vi hợp lý nhất cho hầu hết các cử chỉ. Trong ví dụ này,
Button
nhận được sự kiện trướcListItem
. - Trong Lượt chuyển cuối cùng, sự kiện sẽ diễn ra thêm một lần nữa từ đầu giao diện người dùng vào các nút lá. Quy trình này cho phép các phần tử ở cấp cao hơn trong ngăn xếp phản hồi việc cha mẹ tiêu thụ sự kiện. Ví dụ: một nút sẽ xoá chỉ báo hiệu ứng gợn sóng khi thao tác nhấn chuyển thành thao tác kéo của thành phần mẹ có thể cuộn.
Trực quan, luồng sự kiện có thể được biểu thị như sau:
Sau khi người dùng sử dụng thay đổi về dữ liệu đầu vào, thông tin này sẽ được chuyển từ điểm trong luồng trở đi:
Trong đoạn mã, bạn có thể chỉ định thẻ/vé mà bạn quan tâm:
Modifier.pointerInput(Unit) { awaitPointerEventScope { val eventOnInitialPass = awaitPointerEvent(PointerEventPass.Initial) val eventOnMainPass = awaitPointerEvent(PointerEventPass.Main) // default val eventOnFinalPass = awaitPointerEvent(PointerEventPass.Final) } }
Trong đoạn mã này, mỗi sự kiện giống nhau trả về cùng một sự kiện các lệnh gọi phương thức này đang chờ các lệnh gọi, mặc dù dữ liệu về việc tiêu thụ có thể đã thay đổi.
Kiểm thử cử chỉ
Trong phương thức kiểm thử của mình, bạn có thể gửi các sự kiện con trỏ theo cách thủ công bằng cách sử dụng
Phương thức performTouchInput
. Điều này cho phép bạn thực hiện
cấp cao hơn
cử chỉ toàn bộ (chẳng hạn như chụm hoặc nhấp và giữ) hoặc cử chỉ cấp thấp (chẳng hạn như
di chuyển con trỏ theo một lượng pixel nhất định):
composeTestRule.onNodeWithTag("MyList").performTouchInput { swipeUp() swipeDown() click() }
Hãy xem tài liệu về performTouchInput
để biết thêm ví dụ.
Tìm hiểu thêm
Bạn có thể tìm hiểu thêm về cử chỉ trong Jetpack Compose qua đường liên kết sau tài nguyên:
Đề xuất cho bạn
- Lưu ý: văn bản có đường liên kết sẽ hiện khi JavaScript tắt
- Hỗ trợ tiếp cận trong Compose
- Cuộn
- Nhấn và nhấn