Trạng thái và ảnh động trong Styles

Styles API cung cấp một phương pháp khai báo và tinh giản để quản lý các thay đổi về giao diện người dùng trong các trạng thái tương tác như hovered, focusedpressed. Với API này, bạn có thể giảm đáng kể mã nguyên mẫu thường cần thiết khi sử dụng các đối tượng sửa đổi.

Để tạo điều kiện cho việc định kiểu phản ứng, StyleState hoạt động như một giao diện ổn định, chỉ đọc, theo dõi trạng thái hoạt động của một phần tử (chẳng hạn như trạng thái đã bật, đã nhấn hoặc đã lấy tiêu điểm). Trong StyleScope, bạn có thể truy cập vào thuộc tính này thông qua thuộc tính state để triển khai logic có điều kiện ngay trong các định nghĩa Kiểu.

Tương tác dựa trên trạng thái: Di chuột, đặt tiêu điểm, nhấn, chọn, bật, chuyển đổi

Các kiểu có sẵn tính năng hỗ trợ cho các hoạt động tương tác phổ biến:

  • Đã nhấn
  • Được di chuột
  • Đã chọn
  • Đã bật
  • Đã chuyển đổi

Bạn cũng có thể hỗ trợ các trạng thái tuỳ chỉnh. Hãy xem phần Tạo kiểu cho trạng thái tuỳ chỉnh bằng StyleState để biết thêm thông tin.

Xử lý các trạng thái tương tác bằng tham số Kiểu

Ví dụ sau đây minh hoạ cách sửa đổi backgroundborderColor để phản hồi các trạng thái tương tác, cụ thể là chuyển sang màu tím khi di chuột và màu xanh dương khi được lấy tiêu điểm:

@Preview
@Composable
private fun OpenButton() {
    BaseButton(
        style = outlinedButtonStyle then {
            background(Color.White)
            hovered {
                background(lightPurple)
                border(2.dp, lightPurple)
            }
            focused {
                background(lightBlue)
            }
        },
        onClick = {  },
        content = {
            BaseText("Open in Studio", style = {
                contentColor(Color.Black)
                fontSize(26.sp)
                textAlign(TextAlign.Center)
            })
        }
    )
}

Hình 1. Thay đổi màu nền dựa trên trạng thái di chuột và trạng thái tiêu điểm.

Bạn cũng có thể tạo các định nghĩa trạng thái lồng ghép. Ví dụ: bạn có thể xác định một kiểu cụ thể cho trường hợp nút vừa được nhấn vừa được di chuột đồng thời:

@Composable
private fun OpenButton_CombinedStates() {
    BaseButton(
        style = outlinedButtonStyle then {
            background(Color.White)
            hovered {
                // light purple
                background(lightPurple)
                pressed {
                    // When running on a device that can hover, whilst hovering and then pressing the button this would be invoked
                    background(lightOrange)
                }
            }
            pressed {
                // when running on a device without a mouse attached, this would be invoked as you wouldn't be in a hovered state only
                background(lightRed)
            }
            focused {
                background(lightBlue)
            }
        },
        onClick = {  },
        content = {
            BaseText("Open in Studio", style = {
                contentColor(Color.Black)
                fontSize(26.sp)
                textAlign(TextAlign.Center)
            })
        }
    )
}

Hình 2. Trạng thái di chuột và nhấn cùng nhau trên một nút.

Thành phần kết hợp tuỳ chỉnh có Modifier.styleable

Khi tạo các thành phần styleable của riêng mình, bạn phải kết nối một interactionSource với một styleState. Sau đó, hãy truyền trạng thái này vào Modifier.styleable để sử dụng.

Hãy xem xét trường hợp hệ thống thiết kế của bạn có chứa một GradientButton. Bạn có thể muốn tạo một LoginButton kế thừa từ GradientButton, nhưng thay đổi màu sắc của LoginButton trong các hoạt động tương tác, chẳng hạn như khi được nhấn.

  • Để bật nội dung cập nhật kiểu interactionSource, hãy thêm interactionSource làm tham số trong thành phần kết hợp của bạn. Sử dụng tham số được cung cấp hoặc nếu không có tham số nào được cung cấp, hãy khởi tạo một MutableInteractionSource mới.
  • Khởi động styleState bằng cách cung cấp interactionSource. Đảm bảo trạng thái đã bật của styleState phản ánh giá trị của thông số đã bật được cung cấp.
  • Chỉ định interactionSource cho đối tượng sửa đổi focusableclickable. Cuối cùng, hãy áp dụng styleState cho tham số styleable của đối tượng sửa đổi.

@Composable
private fun GradientButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    style: Style = Style,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource? = null,
    content: @Composable RowScope.() -> Unit,
) {
    val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
    val styleState = remember(interactionSource) { MutableStyleState(interactionSource) }
    styleState.isEnabled = enabled
    Row(
        modifier =
            modifier
                .clickable(
                    onClick = onClick,
                    enabled = enabled,
                    interactionSource = interactionSource,
                    indication = null,
                )
                .styleable(styleState, baseGradientButtonStyle then style),
        content = content,
    )
}

Giờ đây, bạn có thể sử dụng trạng thái interactionSource để điều chỉnh kiểu bằng các lựa chọn đã nhấn, được lấy tiêu điểm và di chuột bên trong khối kiểu:

@Preview
@Composable
fun LoginButton() {
    val loginButtonStyle = Style {
        pressed {
            background(
                Brush.linearGradient(
                    listOf(Color.Magenta, Color.Red)
                )
            )
        }
    }
    GradientButton(onClick = {
        // Login logic
    }, style = loginButtonStyle) {
        BaseText("Login")
    }
}

Hình 3. Thay đổi trạng thái của thành phần kết hợp tuỳ chỉnh dựa trên interactionSource.

Tạo ảnh động cho các thay đổi về kiểu

Các thay đổi về trạng thái kiểu đi kèm với tính năng hỗ trợ ảnh động tích hợp. Bạn có thể bao bọc thuộc tính mới trong bất kỳ khối thay đổi trạng thái nào bằng animate để tự động thêm ảnh động giữa các trạng thái. Điều này tương tự như các API animate*AsState. Ví dụ sau đây tạo hiệu ứng chuyển màu cho borderColor từ đen sang xanh dương khi trạng thái thay đổi thành trạng thái được lấy tiêu điểm:

val animatingStyle = Style {
    externalPadding(48.dp)
    border(3.dp, Color.Black)
    background(Color.White)
    size(100.dp)

    pressed {
        animate {
            borderColor(Color.Magenta)
            background(Color(0xFFB39DDB))
        }
    }
}

@Preview
@Composable
private fun AnimatingStyleChanges() {
    val interactionSource = remember { MutableInteractionSource() }
    val styleState = remember(interactionSource) { MutableStyleState(interactionSource) }
    Box(modifier = Modifier
        .clickable(
            interactionSource,
            enabled = true,
            indication = null,
            onClick = {

            }
        )
        .styleable(styleState, animatingStyle)) {

    }
}

Hình 4. Tạo hiệu ứng cho các thay đổi về màu sắc khi nhấn.

API animate chấp nhận một animationSpec để thay đổi thời lượng hoặc hình dạng của đường cong hoạt ảnh. Ví dụ sau đây minh hoạ kích thước của hộp bằng thông số spring:

val animatingStyleSpec = Style {
    externalPadding(48.dp)
    border(3.dp, Color.Black)
    background(Color.White)
    size(100.dp)
    transformOrigin(TransformOrigin.Center)
    pressed {
        animate {
            borderColor(Color.Magenta)
            background(Color(0xFFB39DDB))
        }
        animate(spring(dampingRatio = Spring.DampingRatioMediumBouncy)) {
            scale(1.2f)
        }
    }
}

@Preview(showBackground = true)
@Composable
fun AnimatingStyleChangesSpec() {
    val interactionSource = remember { MutableInteractionSource() }
    val styleState = remember(interactionSource) { MutableStyleState(interactionSource) }
    Box(modifier = Modifier
        .clickable(
            interactionSource,
            enabled = true,
            indication = null,
            onClick = {

            }
        )
        .styleable(styleState, animatingStyleSpec))
}

Hình 5. Tạo hiệu ứng thay đổi kích thước và màu sắc khi nhấn.

Tạo kiểu trạng thái tuỳ chỉnh bằng StyleState

Tuỳ thuộc vào trường hợp sử dụng thành phần kết hợp, bạn có thể có nhiều kiểu được hỗ trợ bởi các trạng thái tuỳ chỉnh. Ví dụ: nếu có một ứng dụng đa phương tiện, bạn có thể muốn có kiểu riêng cho các nút trong thành phần kết hợp MediaPlayer tuỳ thuộc vào trạng thái phát của trình phát. Hãy làm theo các bước sau để tạo và sử dụng trạng thái tuỳ chỉnh của riêng bạn:

  1. Xác định khoá tuỳ chỉnh
  2. Tạo phần mở rộng StyleState
  3. Liên kết đến trạng thái tuỳ chỉnh

Xác định khoá tuỳ chỉnh

Để tạo kiểu tuỳ chỉnh dựa trên trạng thái, trước tiên, hãy tạo một StyleStateKey và truyền giá trị trạng thái mặc định vào. Khi ứng dụng khởi chạy, trình phát nội dung nghe nhìn sẽ ở trạng thái Stopped, vì vậy, trình phát sẽ được khởi chạy theo cách này:

enum class PlayerState {
    Stopped,
    Playing,
    Paused
}

val playerStateKey = StyleStateKey(PlayerState.Stopped)

Tạo các hàm mở rộng StyleState

Xác định một hàm mở rộng trên StyleState để truy vấn playState hiện tại. Sau đó, hãy tạo các hàm mở rộng trên StyleScope bằng các trạng thái tuỳ chỉnh của bạn bằng cách truyền vào playStateKey, một hàm lambda có trạng thái cụ thể và kiểu.

// Extension Function on MutableStyleState to query and set the current playState
var MutableStyleState.playerState
    get() = this[playerStateKey]
    set(value) { this[playerStateKey] = value }

fun StyleScope.playerPlaying(value: Style) {
    state(playerStateKey, value, { key, state -> state[key] == PlayerState.Playing })
}
fun StyleScope.playerPaused(value: Style) {
    state(playerStateKey, value, { key, state -> state[key] == PlayerState.Paused })
}

Xác định styleState trong thành phần kết hợp và đặt styleState.playState bằng với trạng thái đến. Truyền styleState vào hàm styleable trên đối tượng sửa đổi.

Trong lambda style, bạn có thể áp dụng kiểu dựa trên trạng thái cho các trạng thái tuỳ chỉnh bằng cách sử dụng các hàm mở rộng đã xác định trước đó.

Đoạn mã sau đây là đoạn mã đầy đủ cho ví dụ này:

enum class PlayerState {
    Stopped,
    Playing,
    Paused
}
val playerStateKey = StyleStateKey<PlayerState>(PlayerState.Stopped)
var MutableStyleState.playerState
    get() = this[playerStateKey]
    set(value) { this[playerStateKey] = value }

fun StyleScope.playerPlaying(value: Style) {
    state(playerStateKey, value, { key, state -> state[key] == PlayerState.Playing })
}
fun StyleScope.playerPaused(value: Style) {
    state(playerStateKey, value, { key, state -> state[key] == PlayerState.Paused })

}

@Composable
fun MediaPlayer(
    url: String,
    modifier: Modifier = Modifier,
    style: Style = Style,
    state: PlayerState = remember { PlayerState.Paused }
) {
    // Hoist style state, set playstate as a parameter,
    val styleState = remember { MutableStyleState(null) }
    // Set equal to incoming state to link the two together
    styleState.playerState = state
    Box(
        modifier = modifier.styleable(styleState, Style {
            size(100.dp)
            border(2.dp, Color.Red)

        }, style, )) {

        ///..
    }
}
@Composable
fun StyleStateKeySample() {
    // Using the extension function to change the border color to green while playing
    val style = Style {
        borderColor(Color.Gray)
        playerPlaying {
            animate {
                borderColor(Color.Green)
            }
        }
        playerPaused {
            animate {
                borderColor(Color.Blue)
            }
        }
    }
    val styleState = remember { MutableStyleState(null) }
    styleState[playerStateKey] = PlayerState.Playing

    // Using the style in a composable that sets the state -> notice if you change the state parameter, the style changes. You can link this up to an ViewModel and change the state from there too.
    MediaPlayer(url = "https://example.com/media/video",
        style = style,
        state = PlayerState.Stopped)
}