Các thành phần giao diện người dùng cung cấp phản hồi cho người dùng thiết bị theo cách chúng phản hồi với tương tác của người dùng. Mỗi thành phần đều có cách phản hồi tương tác riêng, giúp người dùng biết hoạt động tương tác của họ là gì. Ví dụ: nếu người dùng nhấn vào một nút trên màn hình cảm ứng của thiết bị, nút đó có thể thay đổi theo cách nào đó, có thể là thêm màu đánh dấu chẳng hạn. Thay đổi này cho người dùng biết rằng họ đã nhấn vào nút. Nếu người dùng không muốn làm để họ biết cách kéo ngón tay ra khỏi nút trước khi nếu không, nút này sẽ kích hoạt.
Tài liệu về Cử chỉ trong Compose đề cập đến cách các thành phần Compose xử lý sự kiện con trỏ cấp thấp, chẳng hạn như thao tác di chuyển con trỏ và lượt nhấp. Ngoài ra, Compose tóm tắt những sự kiện cấp thấp đó thành các lượt tương tác cấp cao hơn – ví dụ: một loạt các sự kiện con trỏ có thể thêm vào một lần nhấn và thả nút. Việc hiểu các khái niệm trừu tượng cấp cao hơn có thể giúp bạn tùy chỉnh cách giao diện người dùng phản hồi cho người dùng. Ví dụ: bạn có thể muốn tùy chỉnh cách giao diện của một thành phần thay đổi khi người dùng tương tác với thành phần đó hoặc có thể bạn chỉ muốn duy trì nhật ký của những hoạt động người dùng đó. Tài liệu này cung cấp cho bạn thông tin cần thiết để sửa đổi các thành phần giao diện người dùng tiêu chuẩn hoặc thiết kế của riêng bạn.
Tương tác
Trong nhiều trường hợp, bạn không cần phải biết thành phần Compose đang diễn giải lượt tương tác của người dùng như thế nào. Ví dụ: Button
dựa vào Modifier.clickable
để tìm hiểu xem người dùng có nhấp vào nút này hay không. Nếu thêm một nút thông thường vào ứng dụng, bạn có thể xác định mã onClick
của nút đó và Modifier.clickable
sẽ chạy mã đó khi thích hợp. Điều đó có nghĩa là bạn không cần phải biết người dùng đã nhấn vào màn hình hay chọn nút bằng bàn phím; Modifier.clickable
nhận thấy người dùng đã thực hiện lượt nhấp và phản hồi bằng cách chạy mã onClick
của bạn.
Tuy nhiên, nếu muốn tùy chỉnh phản hồi của thành phần giao diện người dùng đối với hành vi của người dùng, bạn có thể cần biết thêm về những gì đang diễn ra. Phần này cung cấp cho bạn một vài thông tin đó.
Khi người dùng tương tác với một thành phần giao diện người dùng, hệ thống đại diện cho hành vi của họ bằng cách tạo một số sự kiện Interaction
. Ví dụ: nếu người dùng nhấn vào một nút, thì nút đó sẽ tạo raPressInteraction.Press
.
Nếu người dùng nhấc ngón tay ra khỏi nút, nút này sẽ tạo ra PressInteraction.Release
, cho nút đó nhận biết lượt nhấp đã hoàn tất. Tuy nhiên, nếu người dùng kéo ngón tay ra khỏi nút rồi nhấc ngón tay lên, nút đó sẽ tạo ra PressInteraction.Cancel
để cho biết Thao tác nhấn trên nút đã bị hủy, chưa hoàn tất.
Những lượt tương tác này không hợp lý. Điều này có nghĩa là các sự kiện tương tác cấp thấp không có ý định diễn giải ý nghĩa của hành động của người dùng hoặc trình tự của chúng. Chúng cũng không giải thích hành động nào của người dùng có thể được ưu tiên hơn các hành động khác.
Các hoạt động tương tác này thường đi theo cặp, bắt đầu và kết thúc. Lượt tương tác thứ hai chứa một thông tin tham chiếu đến bản tương tác đầu tiên. Ví dụ: nếu người dùng nhấn vào một nút rồi nhấc ngón tay lên, thì thao tác nhấn đó sẽ tạo ra một lượt tương tác PressInteraction.Press
và bản phát hành tạo ra PressInteraction.Release
; Release
có thuộc tính press
xác định tên viết tắt PressInteraction.Press
ban đầu.
Bạn có thể xem các lượt tương tác của một thành phần cụ thể bằng cách quan sát InteractionSource
. InteractionSource
được xây dựng dựa trên Kotlin flows, vì thế bạn có thể thu thập các lượt tương tác từ luồng này giống như cách bạn làm việc với bất kỳ luồng nào khác. Để biết thêm thông tin về quyết định thiết kế này,
hãy xem bài đăng trên blog về Lượt tương tác làm sáng tỏ.
Trạng thái tương tác
Bạn nên mở rộng chức năng tích hợp sẵn của các thành phần bằng cách tự theo dõi các hoạt động tương tác đó. Ví dụ: có thể bạn muốn một nút thay đổi màu khi bạn nhấn vào. Cách đơn giản nhất để theo dõi các tương tác là quan sát trạng thái tương tác thích hợp. InteractionSource
cung cấp một số phương thức để biết nhiều trạng thái tương tác dưới dạng trạng thái. Ví dụ: nếu muốn xem liệu một nút cụ thể có được nhấn hay không, bạn có thể gọi phương thức InteractionSource.collectIsPressedAsState()
:
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() Button( onClick = { /* do something */ }, interactionSource = interactionSource ) { Text(if (isPressed) "Pressed!" else "Not pressed") }
Ngoài collectIsPressedAsState()
, tính năng Compose còn cung cấp collectIsFocusedAsState()
, collectIsDraggedAsState()
và collectIsHoveredAsState()
. Các phương thức này thực sự tiện dụng được xây dựng dựa trên các API InteractionSource
cấp thấp hơn. Trong một số trường hợp, bạn có thể muốn sử dụng các hàm cấp thấp hơn đó.
Ví dụ: giả sử bạn cần biết liệu một nút có đang được nhấn hay không, cũng như liệu nút đó có đang được kéo hay không. Nếu bạn sử dụng cả collectIsPressedAsState()
và collectIsDraggedAsState()
, thì Compose có nhiều tác vụ trùng lặp và không có gì đảm bảo bạn sẽ nhận được tất cả các lượt tương tác theo đúng thứ tự. Đối với các tình huống như thế này, bạn nên làm việc trực tiếp với InteractionSource
. Để biết thêm thông tin về cách theo dõi các lượt tương tác
chính bạn với InteractionSource
, hãy xem bài viết Làm việc với InteractionSource
.
Phần sau đây mô tả cách sử dụng và phát các tương tác với
InteractionSource
và MutableInteractionSource
tương ứng.
Tiêu thụ và phát thải Interaction
InteractionSource
biểu thị luồng Interactions
chỉ đọc chứ không phải
có thể phát Interaction
đến InteractionSource
. Phát ra
Interaction
, bạn cần sử dụng MutableInteractionSource
, mở rộng từ
InteractionSource
.
Đối tượng sửa đổi và thành phần có thể sử dụng, phát hoặc sử dụng và phát Interactions
.
Các phần sau đây mô tả cách tiếp nhận và phát các lượt tương tác từ cả
đối tượng sửa đổi và thành phần.
Ví dụ về việc sử dụng đối tượng sửa đổi
Đối với đối tượng sửa đổi vẽ đường viền cho trạng thái được lấy làm tâm điểm, bạn chỉ cần quan sát
Interactions
nên bạn có thể chấp nhận một InteractionSource
:
fun Modifier.focusBorder(interactionSource: InteractionSource): Modifier { // ... }
Chữ ký hàm rõ ràng cho thấy đối tượng sửa đổi này là người tiêu dùng (consumer)
có thể tiêu thụ Interaction
nhưng không thể phát chúng.
Ví dụ về quá trình tạo đối tượng sửa đổi
Đối với một đối tượng sửa đổi xử lý các sự kiện di chuột như Modifier.hoverable
, bạn
cần phát ra Interactions
và chấp nhận MutableInteractionSource
làm một
thay vào đó:
fun Modifier.hover(interactionSource: MutableInteractionSource, enabled: Boolean): Modifier { // ... }
Đối tượng sửa đổi này là một thực thể sản xuất — có thể sử dụng
MutableInteractionSource
để phát HoverInteractions
khi di chuột hoặc
không di chuột.
Xây dựng các thành phần tiêu thụ và sản xuất
Các thành phần cấp cao như Button
của Material đóng vai trò vừa là nhà sản xuất và
người tiêu dùng. Chúng xử lý các sự kiện nhập và tiêu điểm, đồng thời thay đổi giao diện của mình
để phản hồi các sự kiện này, chẳng hạn như hiển thị một hiệu ứng gợn sóng hoặc tạo ảnh động cho
độ cao. Do đó, chúng trực tiếp hiển thị MutableInteractionSource
dưới dạng một
để bạn có thể cung cấp phiên bản đã nhớ của riêng mình:
@Composable fun Button( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, // exposes MutableInteractionSource as a parameter interactionSource: MutableInteractionSource? = null, elevation: ButtonElevation? = ButtonDefaults.elevatedButtonElevation(), shape: Shape = MaterialTheme.shapes.small, border: BorderStroke? = null, colors: ButtonColors = ButtonDefaults.buttonColors(), contentPadding: PaddingValues = ButtonDefaults.ContentPadding, content: @Composable RowScope.() -> Unit ) { /* content() */ }
Điều này cho phép chuyển
MutableInteractionSource
ra khỏi thành phần và quan sát tất cả
Interaction
do thành phần tạo ra. Bạn có thể sử dụng tính năng này để kiểm soát
giao diện của thành phần đó hoặc bất kỳ thành phần nào khác trong giao diện người dùng.
Nếu đang xây dựng các thành phần cấp cao có tính tương tác của riêng mình, bạn nên
bạn hiển thị MutableInteractionSource
dưới dạng tham số theo cách này. Bên cạnh
thực hiện theo các phương pháp hay nhất để chuyển trạng thái lên trên, điều này cũng giúp người dùng dễ dàng đọc
điều khiển trạng thái hiển thị của một thành phần theo cách tương tự như bất kỳ loại
Trạng thái (chẳng hạn như trạng thái đã bật) có thể được đọc và kiểm soát.
Compose tuân theo phương pháp tiếp cận cấu trúc phân lớp,
vì vậy, các thành phần Material cấp cao được xây dựng
dựa trên bản dựng nền tảng
khối tạo ra Interaction
chúng cần để kiểm soát gợn sóng và các
hiệu ứng hình ảnh. Thư viện nền tảng cung cấp các đối tượng sửa đổi tương tác cấp cao
chẳng hạn như Modifier.hoverable
, Modifier.focusable
và
Modifier.draggable
.
Để tạo một thành phần phản hồi sự kiện di chuột, bạn chỉ cần sử dụng
Modifier.hoverable
và truyền MutableInteractionSource
dưới dạng tham số.
Bất cứ khi nào thành phần được di chuột, thành phần đó sẽ tạo ra HoverInteraction
và bạn có thể sử dụng
thao tác này để thay đổi cách thành phần xuất hiện.
// This InteractionSource will emit hover interactions val interactionSource = remember { MutableInteractionSource() } Box( Modifier .size(100.dp) .hoverable(interactionSource = interactionSource), contentAlignment = Alignment.Center ) { Text("Hello!") }
Để thành phần này có thể làm tâm điểm, bạn có thể thêm Modifier.focusable
và truyền
MutableInteractionSource
tương tự với tham số. Giờ đây, cả hai
HoverInteraction.Enter/Exit
và FocusInteraction.Focus/Unfocus
được phát ra
thông qua cùng một MutableInteractionSource
, và bạn có thể tuỳ chỉnh
cho cả hai loại tương tác ở cùng một nơi:
// This InteractionSource will emit hover and focus interactions val interactionSource = remember { MutableInteractionSource() } Box( Modifier .size(100.dp) .hoverable(interactionSource = interactionSource) .focusable(interactionSource = interactionSource), contentAlignment = Alignment.Center ) { Text("Hello!") }
Modifier.clickable
thậm chí còn cao hơn
trừu tượng ở cấp độ cao hơn hoverable
và focusable
– để một thành phần trở thành
có thể nhấp, nó hoàn toàn có thể di chuột được và các thành phần có thể nhấp vào được
cũng có thể làm tâm điểm. Bạn có thể dùng Modifier.clickable
để tạo một thành phần
xử lý thao tác di chuột, lấy tiêu điểm và nhấn mà không cần phải kết hợp các tương tác
cấp độ. Nếu bạn cũng muốn làm cho thành phần của mình có thể nhấp vào, bạn có thể
thay thế hoverable
và focusable
bằng clickable
:
// This InteractionSource will emit hover, focus, and press interactions val interactionSource = remember { MutableInteractionSource() } Box( Modifier .size(100.dp) .clickable( onClick = {}, interactionSource = interactionSource, // Also show a ripple effect indication = ripple() ), contentAlignment = Alignment.Center ) { Text("Hello!") }
Làm việc với InteractionSource
Nếu cần thông tin cấp thấp về các lượt tương tác với một thành phần, bạn có thể sử dụng API luồng chuẩn cho InteractionSource
của thành phần đó.
Ví dụ: giả sử bạn muốn duy trì một danh sách các tương tác nhấn và kéo vào InteractionSource
. Mã này thực hiện một nửa công việc, thêm các thao tác nhấn mới vào danh sách khi chúng xuất hiện:
val interactionSource = remember { MutableInteractionSource() } val interactions = remember { mutableStateListOf<Interaction>() } LaunchedEffect(interactionSource) { interactionSource.interactions.collect { interaction -> when (interaction) { is PressInteraction.Press -> { interactions.add(interaction) } is DragInteraction.Start -> { interactions.add(interaction) } } } }
Tuy nhiên, ngoài việc thêm các lượt tương tác mới, bạn cũng phải xóa các lượt tương tác khi nó kết thúc (ví dụ: khi người dùng nhấc ngón tay lên khỏi thành phần). Điều này rất dễ thực hiện, vì lượt tương tác cuối cùng luôn tham chiếu đến lượt tương tác bắt đầu được liên kết. Mã này cho biết cách bạn sẽ xóa những lượt tương tác đã kết thúc:
val interactionSource = remember { MutableInteractionSource() } val interactions = remember { mutableStateListOf<Interaction>() } LaunchedEffect(interactionSource) { interactionSource.interactions.collect { interaction -> when (interaction) { is PressInteraction.Press -> { interactions.add(interaction) } is PressInteraction.Release -> { interactions.remove(interaction.press) } is PressInteraction.Cancel -> { interactions.remove(interaction.press) } is DragInteraction.Start -> { interactions.add(interaction) } is DragInteraction.Stop -> { interactions.remove(interaction.start) } is DragInteraction.Cancel -> { interactions.remove(interaction.start) } } } }
Bây giờ, nếu muốn biết thành phần hiện đang được nhấn hay kéo, bạn chỉ cần kiểm tra xem interactions
có trống hay không:
val isPressedOrDragged = interactions.isNotEmpty()
Nếu muốn biết lượt tương tác gần đây nhất, bạn chỉ cần xem mục trong danh sách. Ví dụ: đây là cách triển khai hiệu ứng gợn sóng trong Compose chỉ ra lớp phủ trạng thái thích hợp để sử dụng cho tương tác gần đây nhất:
val lastInteraction = when (interactions.lastOrNull()) { is DragInteraction.Start -> "Dragged" is PressInteraction.Press -> "Pressed" else -> "No state" }
Vì tất cả các Interaction
đều theo cùng một cấu trúc, nên sẽ không có nhiều
sự khác biệt về mã khi làm việc với các loại tương tác khác nhau của người dùng —
là giống nhau.
Xin lưu ý rằng các ví dụ trước trong phần này thể hiện Flow
của
tương tác bằng State
— điều này giúp bạn dễ dàng quan sát các giá trị được cập nhật,
vì việc đọc giá trị trạng thái sẽ tự động dẫn đến các quá trình kết hợp lại. Tuy nhiên,
cấu trúc được nhóm trước khung hình. Điều này có nghĩa là nếu trạng thái thay đổi và
thì sẽ quay lại trong cùng một khung, các thành phần quan sát trạng thái sẽ không
xem thay đổi.
Điều này rất quan trọng đối với các lượt tương tác, vì các hoạt động tương tác có thể thường xuyên bắt đầu và kết thúc
trong cùng một khung. Ví dụ: sử dụng ví dụ trước với Button
:
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() Button(onClick = { /* do something */ }, interactionSource = interactionSource) { Text(if (isPressed) "Pressed!" else "Not pressed") }
Nếu thao tác nhấn bắt đầu và kết thúc trong cùng một khung, thì văn bản sẽ không bao giờ xuất hiện dưới dạng
"Đã nhấn!". Trong hầu hết các trường hợp, đây không phải là vấn đề — hiển thị hiệu ứng hình ảnh cho
khoảng thời gian ngắn như vậy sẽ dẫn đến
nhấp nháy và sẽ không
đáng chú ý với người dùng. Đối với một số trường hợp, chẳng hạn như hiển thị hiệu ứng gợn sóng hoặc
ảnh động tương tự, bạn nên hiển thị hiệu ứng trong ít nhất một lượng tối thiểu
thay vì dừng ngay lập tức nếu không còn nhấn nút. Người nhận
bạn có thể trực tiếp bắt đầu và dừng ảnh động từ bên trong bộ sưu tập
lambda, thay vì ghi vào một trạng thái. Có một ví dụ về mẫu này trong
phần Tạo Indication
nâng cao bằng đường viền động.
Ví dụ: Tạo thành phần có khả năng xử lý tương tác tuỳ chỉnh
Để xem cách bạn có thể tạo thành phần với một phản hồi tùy chỉnh cho mục nhập, dưới đây là ví dụ về nút đã sửa đổi. Trong trường hợp này, giả sử bạn muốn có một nút phản hồi về các lần nhấn bằng cách thay đổi giao diện của nút:
Để làm việc này, hãy tạo một thành phần kết hợp tùy chỉnh dựa trên Button
và yêu cầu tham số icon
bổ sung để vẽ biểu tượng (trong trường hợp này là giỏ hàng). Bạn gọi collectIsPressedAsState()
để theo dõi xem người dùng có di chuột qua nút hay không; khi nút được nhấn, bạn sẽ thêm biểu tượng vào. Dưới đây là giao diện của mã:
@Composable fun PressIconButton( onClick: () -> Unit, icon: @Composable () -> Unit, text: @Composable () -> Unit, modifier: Modifier = Modifier, interactionSource: MutableInteractionSource? = null ) { val isPressed = interactionSource?.collectIsPressedAsState()?.value ?: false Button( onClick = onClick, modifier = modifier, interactionSource = interactionSource ) { AnimatedVisibility(visible = isPressed) { if (isPressed) { Row { icon() Spacer(Modifier.size(ButtonDefaults.IconSpacing)) } } } text() } }
Dưới đây là giao diện khi sử dụng thành phần kết hợp mới:
PressIconButton( onClick = {}, icon = { Icon(Icons.Filled.ShoppingCart, contentDescription = null) }, text = { Text("Add to cart") } )
Vì PressIconButton
mới này được tạo dựa trên Tài liệu Button
hiện có, nó này phản ứng với các hành động tương tác của người dùng theo mọi cách thông thường. Khi người dùng nhấn nút, nút này sẽ thay đổi độ mờ một chút, giống như một Tài liệu thông thường Button
.
Tạo và áp dụng hiệu ứng tuỳ chỉnh có thể sử dụng lại bằng Indication
Trong các phần trước, bạn đã tìm hiểu cách thay đổi một phần của một thành phần trong phản hồi
cho nhiều Interaction
, chẳng hạn như hiện một biểu tượng khi được nhấn. Cũng như vậy
phương pháp tiếp cận có thể được sử dụng để thay đổi giá trị của các thông số mà bạn cung cấp thành
thành phần hoặc thay đổi nội dung hiển thị bên trong một thành phần, nhưng điều này
chỉ áp dụng theo từng thành phần. Thông thường, một hệ thống ứng dụng hoặc thiết kế
sẽ có một hệ thống chung cho các hiệu ứng hình ảnh có trạng thái — một hiệu ứng nên
được áp dụng nhất quán cho tất cả các thành phần.
Nếu bạn đang xây dựng loại hệ thống thiết kế này, hãy tuỳ chỉnh một thành phần và Việc sử dụng lại tuỳ chỉnh này cho các thành phần khác có thể gây khó khăn cho các lý do sau:
- Mọi thành phần trong hệ thống thiết kế đều cần cùng một mã nguyên mẫu
- Bạn có thể dễ quên áp dụng hiệu ứng này cho các thành phần mới xây dựng và thành phần có thể nhấp
- Có thể bạn sẽ gặp khó khăn khi kết hợp hiệu ứng tuỳ chỉnh với các hiệu ứng khác
Để tránh những vấn đề này và dễ dàng mở rộng quy mô thành phần tuỳ chỉnh trên hệ thống của bạn,
bạn có thể dùng Indication
.
Indication
thể hiện hiệu ứng hình ảnh có thể tái sử dụng, có thể áp dụng cho
các thành phần trong một hệ thống thiết kế hoặc ứng dụng. Indication
được chia thành hai
phần:
IndicationNodeFactory
: Nhà máy tạo ra các thực thểModifier.Node
hiển thị hiệu ứng hình ảnh cho một thành phần. Đối với những cách triển khai đơn giản hơn mà không thay đổi giữa các thành phần, đây có thể là một singleton (đối tượng) và được sử dụng lại trên toàn bộ ứng dụng.Các thực thể này có thể có trạng thái hoặc không có trạng thái. Vì chúng được tạo trên mỗi thành phần, chúng có thể truy xuất các giá trị từ
CompositionLocal
để thay đổi cách chúng xuất hiện hoặc hoạt động bên trong một thành phần cụ thể, như với bất kỳ thành phần nào khácModifier.Node
Modifier.indication
: Một đối tượng sửa đổi vẽIndication
cho một thành phần.Modifier.clickable
và các hệ số sửa đổi tương tác cấp cao khác trực tiếp chấp nhận thông số chỉ báo để chúng không chỉ phátInteraction
, nhưng cũng có thể vẽ hiệu ứng hình ảnh choInteraction
mà chúng phát ra. Vì vậy, đối với các trường hợp đơn giản, bạn có thể chỉ cần sử dụngModifier.clickable
mà không cần cầnModifier.indication
.
Thay thế hiệu ứng bằng Indication
Phần này mô tả cách thay thế hiệu ứng tỷ lệ thủ công được áp dụng cho một hiệu ứng nút cụ thể có chỉ báo tương đương có thể được sử dụng lại trên nhiều thành phần.
Đoạn mã sau đây sẽ tạo ra một nút thu nhỏ khi nhấn vào:
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() val scale by animateFloatAsState(targetValue = if (isPressed) 0.9f else 1f, label = "scale") Button( modifier = Modifier.scale(scale), onClick = { }, interactionSource = interactionSource ) { Text(if (isPressed) "Pressed!" else "Not pressed") }
Để chuyển đổi hiệu ứng tỷ lệ trong đoạn mã ở trên thành Indication
, hãy làm theo
các bước sau:
Tạo
Modifier.Node
chịu trách nhiệm áp dụng hiệu ứng tỷ lệ. Khi được đính kèm, nút này sẽ quan sát nguồn tương tác, tương tự như trước ví dụ. Điểm khác biệt duy nhất ở đây là trực tiếp khởi chạy ảnh động thay vì chuyển đổi Lượt tương tác sắp tới sang trạng thái.Nút này cần triển khai
DrawModifierNode
để có thể ghi đèContentDrawScope#draw()
rồi kết xuất hiệu ứng tỷ lệ bằng cùng một bản vẽ giống như mọi API đồ hoạ khác trong Compose.Việc gọi
drawContent()
có từ bộ nhậnContentDrawScope
sẽ vẽ thành phần thực tế sẽ áp dụngIndication
. cần gọi hàm này trong phép biến đổi tỷ lệ. Hãy đảm bảo rằng Quá trình triển khaiIndication
luôn gọidrawContent()
tại một thời điểm nào đó; nếu không, thành phần bạn đang áp dụngIndication
sẽ không được vẽ.private class ScaleNode(private val interactionSource: InteractionSource) : Modifier.Node(), DrawModifierNode { var currentPressPosition: Offset = Offset.Zero val animatedScalePercent = Animatable(1f) private suspend fun animateToPressed(pressPosition: Offset) { currentPressPosition = pressPosition animatedScalePercent.animateTo(0.9f, spring()) } private suspend fun animateToResting() { animatedScalePercent.animateTo(1f, spring()) } override fun onAttach() { coroutineScope.launch { interactionSource.interactions.collectLatest { interaction -> when (interaction) { is PressInteraction.Press -> animateToPressed(interaction.pressPosition) is PressInteraction.Release -> animateToResting() is PressInteraction.Cancel -> animateToResting() } } } } override fun ContentDrawScope.draw() { scale( scale = animatedScalePercent.value, pivot = currentPressPosition ) { this@draw.drawContent() } } }
Tạo
IndicationNodeFactory
. Trách nhiệm duy nhất của đơn vị quảng cáo này là tạo bản sao nút mới cho nguồn tương tác đã cung cấp. Vì không có các tham số để định cấu hình chỉ báo, factory có thể là một đối tượng:object ScaleIndication : IndicationNodeFactory { override fun create(interactionSource: InteractionSource): DelegatableNode { return ScaleNode(interactionSource) } override fun equals(other: Any?): Boolean = other === ScaleIndication override fun hashCode() = 100 }
Modifier.clickable
sử dụngModifier.indication
nội bộ, vì vậy để tạo một thành phần có thể nhấp bằngScaleIndication
, bạn chỉ cần cung cấpIndication
làm tham số choclickable
:Box( modifier = Modifier .size(100.dp) .clickable( onClick = {}, indication = ScaleIndication, interactionSource = null ) .background(Color.Blue), contentAlignment = Alignment.Center ) { Text("Hello!", color = Color.White) }
Điều này cũng giúp bạn dễ dàng xây dựng các thành phần cấp cao, có thể tái sử dụng nhờ sử dụng một
Indication
— một nút có thể có dạng như sau:@Composable fun ScaleButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource? = null, shape: Shape = CircleShape, content: @Composable RowScope.() -> Unit ) { Row( modifier = modifier .defaultMinSize(minWidth = 76.dp, minHeight = 48.dp) .clickable( enabled = enabled, indication = ScaleIndication, interactionSource = interactionSource, onClick = onClick ) .border(width = 2.dp, color = Color.Blue, shape = shape) .padding(horizontal = 16.dp, vertical = 8.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, content = content ) }
Sau đó, bạn có thể dùng nút này theo cách sau:
ScaleButton(onClick = {}) { Icon(Icons.Filled.ShoppingCart, "") Spacer(Modifier.padding(10.dp)) Text(text = "Add to cart!") }
Tạo một Indication
nâng cao với đường viền động
Indication
không chỉ giới hạn ở hiệu ứng chuyển đổi, chẳng hạn như điều chỉnh tỷ lệ
thành phần. Vì IndicationNodeFactory
trả về Modifier.Node
nên bạn có thể vẽ
bất kỳ loại hiệu ứng nào bên trên hoặc bên dưới nội dung cũng như đối với các API vẽ khác. Cho
ví dụ: bạn có thể vẽ đường viền động xung quanh thành phần và lớp phủ trên
đầu thành phần khi được nhấn:
Cách triển khai Indication
ở đây rất giống với ví dụ trước —
nó chỉ tạo một nút với một số tham số. Vì đường viền động phụ thuộc
trên hình dạng và đường viền của thành phần mà Indication
được sử dụng,
Quá trình triển khai Indication
cũng yêu cầu cung cấp hình dạng và chiều rộng đường viền
dưới dạng tham số:
data class NeonIndication(private val shape: Shape, private val borderWidth: Dp) : IndicationNodeFactory { override fun create(interactionSource: InteractionSource): DelegatableNode { return NeonNode( shape, // Double the border size for a stronger press effect borderWidth * 2, interactionSource ) } }
Về mặt lý thuyết, việc triển khai Modifier.Node
cũng giống nhau, ngay cả khi
mã vẽ phức tạp hơn. Như trước, nó quan sát InteractionSource
khi đính kèm, khởi chạy ảnh động và triển khai DrawModifierNode
để vẽ
tác động lên nội dung:
private class NeonNode( private val shape: Shape, private val borderWidth: Dp, private val interactionSource: InteractionSource ) : Modifier.Node(), DrawModifierNode { var currentPressPosition: Offset = Offset.Zero val animatedProgress = Animatable(0f) val animatedPressAlpha = Animatable(1f) var pressedAnimation: Job? = null var restingAnimation: Job? = null private suspend fun animateToPressed(pressPosition: Offset) { // Finish any existing animations, in case of a new press while we are still showing // an animation for a previous one restingAnimation?.cancel() pressedAnimation?.cancel() pressedAnimation = coroutineScope.launch { currentPressPosition = pressPosition animatedPressAlpha.snapTo(1f) animatedProgress.snapTo(0f) animatedProgress.animateTo(1f, tween(450)) } } private fun animateToResting() { restingAnimation = coroutineScope.launch { // Wait for the existing press animation to finish if it is still ongoing pressedAnimation?.join() animatedPressAlpha.animateTo(0f, tween(250)) animatedProgress.snapTo(0f) } } override fun onAttach() { coroutineScope.launch { interactionSource.interactions.collect { interaction -> when (interaction) { is PressInteraction.Press -> animateToPressed(interaction.pressPosition) is PressInteraction.Release -> animateToResting() is PressInteraction.Cancel -> animateToResting() } } } } override fun ContentDrawScope.draw() { val (startPosition, endPosition) = calculateGradientStartAndEndFromPressPosition( currentPressPosition, size ) val brush = animateBrush( startPosition = startPosition, endPosition = endPosition, progress = animatedProgress.value ) val alpha = animatedPressAlpha.value drawContent() val outline = shape.createOutline(size, layoutDirection, this) // Draw overlay on top of content drawOutline( outline = outline, brush = brush, alpha = alpha * 0.1f ) // Draw border on top of overlay drawOutline( outline = outline, brush = brush, alpha = alpha, style = Stroke(width = borderWidth.toPx()) ) } /** * Calculates a gradient start / end where start is the point on the bounding rectangle of * size [size] that intercepts with the line drawn from the center to [pressPosition], * and end is the intercept on the opposite end of that line. */ private fun calculateGradientStartAndEndFromPressPosition( pressPosition: Offset, size: Size ): Pair<Offset, Offset> { // Convert to offset from the center val offset = pressPosition - size.center // y = mx + c, c is 0, so just test for x and y to see where the intercept is val gradient = offset.y / offset.x // We are starting from the center, so halve the width and height - convert the sign // to match the offset val width = (size.width / 2f) * sign(offset.x) val height = (size.height / 2f) * sign(offset.y) val x = height / gradient val y = gradient * width // Figure out which intercept lies within bounds val intercept = if (abs(y) <= abs(height)) { Offset(width, y) } else { Offset(x, height) } // Convert back to offsets from 0,0 val start = intercept + size.center val end = Offset(size.width - start.x, size.height - start.y) return start to end } private fun animateBrush( startPosition: Offset, endPosition: Offset, progress: Float ): Brush { if (progress == 0f) return TransparentBrush // This is *expensive* - we are doing a lot of allocations on each animation frame. To // recreate a similar effect in a performant way, it would be better to create one large // gradient and translate it on each frame, instead of creating a whole new gradient // and shader. The current approach will be janky! val colorStops = buildList { when { progress < 1 / 6f -> { val adjustedProgress = progress * 6f add(0f to Blue) add(adjustedProgress to Color.Transparent) } progress < 2 / 6f -> { val adjustedProgress = (progress - 1 / 6f) * 6f add(0f to Purple) add(adjustedProgress * MaxBlueStop to Blue) add(adjustedProgress to Blue) add(1f to Color.Transparent) } progress < 3 / 6f -> { val adjustedProgress = (progress - 2 / 6f) * 6f add(0f to Pink) add(adjustedProgress * MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } progress < 4 / 6f -> { val adjustedProgress = (progress - 3 / 6f) * 6f add(0f to Orange) add(adjustedProgress * MaxPinkStop to Pink) add(MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } progress < 5 / 6f -> { val adjustedProgress = (progress - 4 / 6f) * 6f add(0f to Yellow) add(adjustedProgress * MaxOrangeStop to Orange) add(MaxPinkStop to Pink) add(MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } else -> { val adjustedProgress = (progress - 5 / 6f) * 6f add(0f to Yellow) add(adjustedProgress * MaxYellowStop to Yellow) add(MaxOrangeStop to Orange) add(MaxPinkStop to Pink) add(MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } } } return linearGradient( colorStops = colorStops.toTypedArray(), start = startPosition, end = endPosition ) } companion object { val TransparentBrush = SolidColor(Color.Transparent) val Blue = Color(0xFF30C0D8) val Purple = Color(0xFF7848A8) val Pink = Color(0xFFF03078) val Orange = Color(0xFFF07800) val Yellow = Color(0xFFF0D800) const val MaxYellowStop = 0.16f const val MaxOrangeStop = 0.33f const val MaxPinkStop = 0.5f const val MaxPurpleStop = 0.67f const val MaxBlueStop = 0.83f } }
Điểm khác biệt chính ở đây là hiện tại có một thời lượng tối thiểu cho
ảnh động bằng hàm animateToResting()
, vì vậy, ngay cả khi thao tác nhấn là
phát hành ngay lập tức, ảnh động báo chí sẽ tiếp tục. Ngoài ra còn có quy trình xử lý
cho nhiều lần nhấn nhanh ở đầu animateToPressed
– nếu một lần nhấn
xảy ra trong khi ảnh động đang nhấn hoặc nghỉ, hoạt ảnh trước đó là
bị huỷ và hoạt ảnh nhấn sẽ bắt đầu từ đầu. Để hỗ trợ nhiều
hiệu ứng đồng thời (chẳng hạn như hiệu ứng gợn sóng, trong đó ảnh động gợn sóng mới sẽ vẽ
bên trên các hiệu ứng gợn sóng khác), bạn có thể theo dõi ảnh động trong một danh sách, thay vì
việc huỷ các hoạt ảnh hiện có và bắt đầu các hoạt ảnh mới.
Đề xuất cho bạn
- Lưu ý: văn bản có đường liên kết sẽ hiện khi JavaScript tắt
- Tìm hiểu về cử chỉ
- Kotlin cho Jetpack Compose
- Thành phần và bố cục Material