Ảnh động dựa trên giá trị

Trang này mô tả cách tạo ảnh động dựa trên giá trị trong Jetpack Compose, tập trung vào các API tạo ảnh động cho giá trị dựa trên trạng thái hiện tại và trạng thái mục tiêu của giá trị đó.

Tạo ảnh động cho một giá trị duy nhất bằng animate*AsState

Các hàm animate*AsState là các API ảnh động đơn giản trong Compose để tạo ảnh động cho một giá trị duy nhất. Bạn chỉ cung cấp giá trị đích (hoặc giá trị cuối) và API sẽ bắt đầu tạo ảnh động từ giá trị hiện tại đến giá trị được chỉ định.

Ví dụ sau đây tạo ảnh động alpha bằng API này. Bằng cách gói giá trị mục tiêu trong animateFloatAsState, giá trị alpha giờ là giá trị ảnh động giữa các giá trị đã cung cấp (1f hoặc 0.5f trong trường hợp này).

var enabled by remember { mutableStateOf(true) }

val animatedAlpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f, label = "alpha")
Box(
    Modifier
        .fillMaxSize()
        .graphicsLayer { alpha = animatedAlpha }
        .background(Color.Red)
)

Bạn không cần tạo một phiên bản của bất kỳ lớp ảnh động nào hoặc xử lý gián đoạn. Trong trường hợp này, một đối tượng ảnh động (cụ thể là một thực thể Animatable) sẽ được tạo và ghi nhớ tại nơi hàm được gọi, với giá trị mục tiêu đầu tiên chính là giá trị ban đầu. Kể từ đó, bất cứ khi nào bạn cung cấp cho thành phần kết hợp này một giá trị mục tiêu khác, hệ thống sẽ tự động bắt đầu một ảnh động theo giá trị đó. Nếu đã có ảnh động trong giai đoạn hiển thị, thì ảnh động sẽ bắt đầu từ giá trị hiện tại (và vận tốc) và tạo ảnh động hướng tới giá trị mục tiêu. Trong quá trình ảnh động, thành phần kết hợp này sẽ được ghép lại và trả về một giá trị ảnh động được cập nhật cho mọi khung hình.

Theo mặc định, Compose cung cấp các hàm animate*AsState cho Float, Color, Dp, Size, Offset, Rect, Int, IntOffsetIntSize. Bạn có thể thêm tính năng hỗ trợ cho các loại dữ liệu khác bằng cách cung cấp TwoWayConverter cho animateValueAsState nhận một loại chung.

Bạn có thể sử dụng AnimationSpec để tuỳ chỉnh thông số kỹ thuật của ảnh động. Hãy xem AnimationSpec để biết thêm thông tin.

Tạo ảnh động đồng thời cho nhiều thuộc tính bằng hiệu ứng chuyển đổi

Transition quản lý một hoặc nhiều ảnh động dưới dạng thành phần con và chạy đồng thời giữa nhiều trạng thái.

Các trạng thái có thể thuộc bất kỳ loại dữ liệu nào. Trong nhiều trường hợp, bạn có thể sử dụng loại enum tuỳ chỉnh để xác minh tính an toàn của loại, như trong ví dụ này:

enum class BoxState {
    Collapsed,
    Expanded
}

updateTransition tạo và ghi nhớ một thực thể của Transition và cập nhật trạng thái của thực thể đó.

var currentState by remember { mutableStateOf(BoxState.Collapsed) }
val transition = updateTransition(currentState, label = "box state")

Bạn có thể sử dụng một trong các hàm mở rộng animate* để xác định ảnh động bố cục con trong hiệu ứng chuyển đổi này. Chỉ định các giá trị mục tiêu cho mỗi trạng thái. Các hàm animate* này trả về một giá trị ảnh động. Mọi khung hình đều cập nhật trong suốt chế độ ảnh động khi trạng thái chuyển đổi được cập nhật với updateTransition.

val rect by transition.animateRect(label = "rectangle") { state ->
    when (state) {
        BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
        BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
    }
}
val borderWidth by transition.animateDp(label = "border width") { state ->
    when (state) {
        BoxState.Collapsed -> 1.dp
        BoxState.Expanded -> 0.dp
    }
}

Bạn có thể chuyển tham số transitionSpec để chỉ định một AnimationSpec khác cho mỗi kiểu kết hợp của các thay đổi trạng thái chuyển đổi. Hãy xem AnimationSpec để biết thêm thông tin.

val color by transition.animateColor(
    transitionSpec = {
        when {
            BoxState.Expanded isTransitioningTo BoxState.Collapsed ->
                spring(stiffness = 50f)

            else ->
                tween(durationMillis = 500)
        }
    }, label = "color"
) { state ->
    when (state) {
        BoxState.Collapsed -> MaterialTheme.colorScheme.primary
        BoxState.Expanded -> MaterialTheme.colorScheme.background
    }
}

Khi hiệu ứng chuyển đổi đạt đến trạng thái mục tiêu, Transition.currentState sẽ giống với Transition.targetState. Bạn có thể sử dụng trạng thái này như một tín hiệu cho biết hiệu ứng chuyển đổi đã hoàn tất hay chưa.

Đôi khi, bạn có thể muốn có một trạng thái ban đầu khác với trạng thái mục tiêu đầu tiên. Bạn có thể sử dụng updateTransition với MutableTransitionState để đạt được điều này. Ví dụ: bạn có thể khởi động ảnh động ngay khi mã nhập vào phương thức hợp thành.

// Start in collapsed state and immediately animate to expanded
var currentState = remember { MutableTransitionState(BoxState.Collapsed) }
currentState.targetState = BoxState.Expanded
val transition = rememberTransition(currentState, label = "box state")
// ……

Đối với quá trình chuyển đổi phức tạp hơn liên quan đến nhiều hàm có khả năng kết hợp, bạn có thể sử dụng createChildTransition để tạo nội dung chuyển đổi con. Kỹ thuật này dùng để phân biệt các mối lo ngại giữa nhiều thành phần phụ trong một thành phần kết hợp phức tạp. Quá trình chuyển đổi thành phần mẹ sẽ nhận ra được tất cả các giá trị ảnh động trong các hiệu ứng chuyển đổi thành phần con.

enum class DialerState { DialerMinimized, NumberPad }

@Composable
fun DialerButton(isVisibleTransition: Transition<Boolean>) {
    // `isVisibleTransition` spares the need for the content to know
    // about other DialerStates. Instead, the content can focus on
    // animating the state change between visible and not visible.
}

@Composable
fun NumberPad(isVisibleTransition: Transition<Boolean>) {
    // `isVisibleTransition` spares the need for the content to know
    // about other DialerStates. Instead, the content can focus on
    // animating the state change between visible and not visible.
}

@Composable
fun Dialer(dialerState: DialerState) {
    val transition = updateTransition(dialerState, label = "dialer state")
    Box {
        // Creates separate child transitions of Boolean type for NumberPad
        // and DialerButton for any content animation between visible and
        // not visible
        NumberPad(
            transition.createChildTransition {
                it == DialerState.NumberPad
            }
        )
        DialerButton(
            transition.createChildTransition {
                it == DialerState.DialerMinimized
            }
        )
    }
}

Sử dụng hiệu ứng chuyển đổi với AnimatedVisibilityAnimatedContent

AnimatedVisibilityAnimatedContent có thể sử dụng dưới dạng các hàm mở rộng của Transition. targetState cho Transition.AnimatedVisibilityTransition.AnimatedContent có nguồn gốc từ Transition và kích hoạt ảnh động nhập, thoát và sizeTransform khi cần thiết khi targetState của Transition thay đổi. Các hàm mở rộng này cho phép bạn nâng tất cả ảnh động enter, exit và sizeTransform mà nếu không nội bộ bên trong AnimatedVisibility/AnimatedContent sẽ được nâng lên thành Transition. Bạn có thể ghi nhận từ bên ngoài sự thay đổi trạng thái của AnimatedVisibility/AnimatedContent ở các hàm mở rộng này. Thay vì tham số visible boolean, phiên bản này của AnimatedVisibility sẽ lấy một hàm lambda chuyển đổi trạng thái mục tiêu của lệnh chuyển đổi thành phần mẹ thành một boolean.

Hãy xem AnimatedVisibilityAnimatedContent để biết thông tin chi tiết.

var selected by remember { mutableStateOf(false) }
// Animates changes when `selected` is changed.
val transition = updateTransition(selected, label = "selected state")
val borderColor by transition.animateColor(label = "border color") { isSelected ->
    if (isSelected) Color.Magenta else Color.White
}
val elevation by transition.animateDp(label = "elevation") { isSelected ->
    if (isSelected) 10.dp else 2.dp
}
Surface(
    onClick = { selected = !selected },
    shape = RoundedCornerShape(8.dp),
    border = BorderStroke(2.dp, borderColor),
    shadowElevation = elevation
) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
    ) {
        Text(text = "Hello, world!")
        // AnimatedVisibility as a part of the transition.
        transition.AnimatedVisibility(
            visible = { targetSelected -> targetSelected },
            enter = expandVertically(),
            exit = shrinkVertically()
        ) {
            Text(text = "It is fine today.")
        }
        // AnimatedContent as a part of the transition.
        transition.AnimatedContent { targetState ->
            if (targetState) {
                Text(text = "Selected")
            } else {
                Icon(imageVector = Icons.Default.Phone, contentDescription = "Phone")
            }
        }
    }
}

Đóng gói hiệu ứng chuyển đổi và làm cho nó tái sử dụng được

Đối với các trường hợp sử dụng đơn giản, việc xác định ảnh động chuyển đổi trong cùng một thành phần kết hợp như giao diện người dùng là một lựa chọn hợp lệ. Tuy nhiên, khi thao tác trên một thành phần phức tạp có một số giá trị được tạo ảnh động, bạn có thể muốn tách riêng việc triển khai ảnh động với giao diện người dùng có thể kết hợp.

Bạn có thể thực hiện việc này bằng cách tạo một lớp chứa tất cả các giá trị ảnh động và hàm update để trả về một thực thể của lớp đó. Bạn có thể trích xuất quá trình triển khai chuyển đổi vào hàm riêng biệt mới. Mẫu này rất hữu ích khi bạn cần tập trung logic ảnh động hoặc làm cho các ảnh động phức tạp có thể sử dụng lại.

enum class BoxState { Collapsed, Expanded }

@Composable
fun AnimatingBox(boxState: BoxState) {
    val transitionData = updateTransitionData(boxState)
    // UI tree
    Box(
        modifier = Modifier
            .background(transitionData.color)
            .size(transitionData.size)
    )
}

// Holds the animation values.
private class TransitionData(
    color: State<Color>,
    size: State<Dp>
) {
    val color by color
    val size by size
}

// Create a Transition and return its animation values.
@Composable
private fun updateTransitionData(boxState: BoxState): TransitionData {
    val transition = updateTransition(boxState, label = "box state")
    val color = transition.animateColor(label = "color") { state ->
        when (state) {
            BoxState.Collapsed -> Color.Gray
            BoxState.Expanded -> Color.Red
        }
    }
    val size = transition.animateDp(label = "size") { state ->
        when (state) {
            BoxState.Collapsed -> 64.dp
            BoxState.Expanded -> 128.dp
        }
    }
    return remember(transition) { TransitionData(color, size) }
}

Tạo ảnh động lặp lại vô hạn bằng rememberInfiniteTransition

InfiniteTransition lưu giữ một hoặc nhiều ảnh động bố cục con như Transition, nhưng các ảnh động bắt đầu chạy ngay khi chúng vào cấu trúc và không dừng lại trừ phi bạn xoá chúng. Bạn có thể tạo một phiên bản của InfiniteTransition bằng rememberInfiniteTransition và thêm ảnh động con bằng animateColor, animatedFloat hoặc animatedValue. Bạn cũng cần chỉ định một infiniteRepeatable để chỉ định thông số kỹ thuật của ảnh động.

val infiniteTransition = rememberInfiniteTransition(label = "infinite")
val color by infiniteTransition.animateColor(
    initialValue = Color.Red,
    targetValue = Color.Green,
    animationSpec = infiniteRepeatable(
        animation = tween(1000, easing = LinearEasing),
        repeatMode = RepeatMode.Reverse
    ),
    label = "color"
)

Box(
    Modifier
        .fillMaxSize()
        .background(color)
)

API ảnh động cấp thấp

Tất cả các API ảnh động cấp cao đã đề cập trong phần trước đều được xây dựng dựa trên các API ảnh động cấp thấp.

Các hàm animate*AsState là các API đơn giản để hiển thị thay đổi giá trị tức thì dưới dạng giá trị ảnh động. Chức năng này được hỗ trợ bởi Animatable, một API dựa trên coroutine để tạo ảnh động cho một giá trị duy nhất.

updateTransition tạo một đối tượng chuyển đổi có thể quản lý nhiều giá trị ảnh động và chạy các giá trị đó khi trạng thái thay đổi. rememberInfiniteTransition cũng tương tự, nhưng hàm này tạo một hiệu ứng chuyển đổi vô hạn có thể quản lý nhiều ảnh động tiếp tục chạy vô thời hạn. Tất cả các API này đều là thành phần kết hợp ngoại trừ Animatable, nghĩa là bạn có thể tạo các ảnh động này bên ngoài phương thức hợp thành.

Tất cả các API này đều dựa trên API Animation nền tảng hơn. Mặc dù hầu hết các ứng dụng sẽ không tương tác trực tiếp với Animation, nhưng bạn có thể truy cập vào một số khả năng tuỳ chỉnh của Animation thông qua các API cấp cao hơn. Hãy xem bài viết Tuỳ chỉnh ảnh động để biết thêm thông tin về AnimationVectorAnimationSpec.

Mối quan hệ giữa các API ảnh động cấp thấp
Hình 1. Mối quan hệ giữa các API ảnh động cấp thấp.

Animatable: Tạo ảnh động cho một giá trị duy nhất dựa trên coroutine

Animatable là trình lưu giữ giá trị có thể tạo ảnh động cho giá trị khi được thay đổi bằng animateTo. Đây là API đang sao lưu nội dung triển khai animate*AsState. Giao diện này đảm bảo tiếp tục duy trì tính nhất quán và loại trừ lẫn nhau, nghĩa là việc thay đổi giá trị luôn diễn ra liên tục và Compose sẽ huỷ mọi ảnh động đang diễn ra.

Nhiều tính năng của Animatable, bao gồm cả animateTo, là các hàm tạm ngưng. Điều này có nghĩa là bạn phải gói các tính năng này trong một phạm vi coroutine thích hợp. Ví dụ: bạn có thể sử dụng thành phần kết hợp LaunchedEffect để tạo một phạm vi chỉ trong khoảng thời gian của khoá-giá trị được chỉ định.

// Start out gray and animate to green/red based on `ok`
val color = remember { Animatable(Color.Gray) }
LaunchedEffect(ok) {
    color.animateTo(if (ok) Color.Green else Color.Red)
}
Box(
    Modifier
        .fillMaxSize()
        .background(color.value)
)

Trong ví dụ trước, bạn tạo và ghi nhớ một thực thể của Animatable với giá trị ban đầu là Color.Gray. Tuỳ thuộc vào giá trị của cờ boolean ok, màu sắc kết hợp ảnh động cho một trong hai hàm Color.Green hoặc Color.Red. Bất kỳ thay đổi nào tiếp theo đối với giá trị boolean sẽ bắt đầu chuyển ảnh động thành màu khác. Nếu một ảnh động đang diễn ra khi giá trị thay đổi, thì Compose sẽ huỷ ảnh động đó và ảnh động mới sẽ bắt đầu từ giá trị hiện tại của ảnh chụp nhanh với vận tốc hiện tại.

API Animatable này là phương thức triển khai cơ bản cho animate*AsState được đề cập trong phần trước. Việc sử dụng trực tiếp Animatable giúp kiểm soát chặt chẽ hơn theo một số cách:

  • Trước tiên, Animatable có thể có giá trị ban đầu khác với giá trị mục tiêu đầu tiên. Ví dụ: ví dụ về mã trước đó cho thấy một hộp màu xám ở đầu tiên, ngay lập tức tạo ảnh động sang màu xanh lục hoặc đỏ.
  • Thứ hai, Animatable cung cấp nhiều thao tác hơn trên giá trị nội dung, cụ thể là snapToanimateDecay.
    • snapTo đặt giá trị hiện tại thành giá trị mục tiêu ngay lập tức. Điều này hữu ích khi ảnh động không phải là nguồn đáng tin cậy duy nhất và phải đồng bộ hoá với các trạng thái khác, chẳng hạn như các sự kiện chạm.
    • animateDecay bắt đầu một ảnh động chậm lại từ tốc độ đã cho. Điều này rất hữu ích trong việc triển khai hành vi vuốt nhanh.

Hãy xem phần Cử chỉ và ảnh động để biết thêm thông tin.

Theo mặc định, Animatable hỗ trợ FloatColor, nhưng bạn có thể dùng bất kỳ loại dữ liệu nào bằng cách cung cấp một TwoWayConverter. Hãy xem AnimationVector để biết thêm thông tin.

Bạn có thể sử dụng AnimationSpec để tuỳ chỉnh thông số kỹ thuật của ảnh động. Hãy xem AnimationSpec để biết thêm thông tin.

Animation: Ảnh động được kiểm soát bằng phương pháp thủ công

Animation là API Ảnh động cấp thấp nhất hiện có. Nhiều ảnh động mà chúng ta thấy cho đến nay đã xây dựng trên Animation. Có hai kiểu Animation phụ: TargetBasedAnimationDecayAnimation.

Chỉ dùng Animation để kiểm soát thời lượng của ảnh động theo cách thủ công. Animation không có trạng thái và không có bất kỳ khái niệm nào về vòng đời. Nó đóng vai trò như một công cụ tính toán ảnh động cho các API cấp cao hơn.

TargetBasedAnimation

Các API khác dùng được trong hầu hết các trường hợp, nhưng việc sử dụng trực tiếp TargetBasedAnimation cho phép bạn kiểm soát thời lượng phát ảnh động. Trong ví dụ sau, bạn kiểm soát thời gian phát của TargetAnimation theo cách thủ công dựa trên khung thời gian do withFrameNanos cung cấp.

val anim = remember {
    TargetBasedAnimation(
        animationSpec = tween(200),
        typeConverter = Float.VectorConverter,
        initialValue = 200f,
        targetValue = 1000f
    )
}
var playTime by remember { mutableLongStateOf(0L) }

LaunchedEffect(anim) {
    val startTime = withFrameNanos { it }

    do {
        playTime = withFrameNanos { it } - startTime
        val animationValue = anim.getValueFromNanos(playTime)
    } while (someCustomCondition())
}

DecayAnimation

Không giống như TargetBasedAnimation, DecayAnimation không yêu cầu cung cấp targetValue. Thay vào đó, phương thức này tính toán targetValue dựa trên các điều kiện bắt đầu do initialVelocityinitialValue cung cấp, cũng như DecayAnimationSpec được cung cấp.

Ảnh động phân rã thường được sử dụng sau cử chỉ hất để làm chậm các phần tử xuống điểm dừng. Tốc độ ảnh động bắt đầu ở giá trị mà initialVelocityVector thiết lập và sẽ chậm lại theo thời gian.