Ảnh động dựa trên 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à API ảnh động đơn giản nhất trong Compose để tạo ảnh động cho một giá trị duy nhất. Bạn chỉ cung cấp giá trị nhắm mục tiêu (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.

Dưới đây là ví dụ về cách tạo ảnh động alpha bằng API này. Chỉ cần 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 alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f, label = "alpha")
Box(
    Modifier
        .fillMaxSize()
        .graphicsLayer(alpha = alpha)
        .background(Color.Red)
)

Lưu ý rằng 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.

Ngay lập tức, Compose cung cấp các hàm animate*AsState cho Float, Color, Dp, Size, Offset, Rect, Int , IntOffsetIntSize. Bạn có thể dễ dàng 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 để đảm bảo loại an toàn, 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, chúng ta 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. Chúng ta có thể sử dụng updateTransition cùng với MutableTransitionState để đạt được điều này. Ví dụ: chúng ta được quyền 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 các nội dung chuyển đổi nhập/thoát cần thiết khi targetState của Transition thay đổi. Các hàm mở rộng này cho phép tất cả các ảnh động enter/exit/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ì thông 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.

Xem AnimatedVisibilityAnimatedContent để biết thêm 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à tuỳ chọn hoàn toà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 đó. Việc triển khai chuyển đổi có thể được trích xuất vào hàm riêng mới. Mẫu này rất hữu ích khi cần phải 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 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. Ảnh động con có thể được thêm vào bằng animateColor, animatedFloat hoặc animatedValue. Ngoài ra, bạn còn cần chỉ định infiniteRepeatable làm 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 nền tảng của các API ảnh động cấp thấp.

Các hàm animate*AsState là các API đơn giản nhất để hiển thị thay đổi giá trị tức thì dưới dạng giá trị ảnh động. Hàm này được Animatable hỗ trợ vì đây là một API dựa vào 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ị đó dựa trên sự thay đổi về trạng thái. rememberInfiniteTransition tương tự như vậy, nhưng hàm này tạo một sự 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à các ảnh động này có thể được tạo 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 một số khả năng tuỳ chỉnh dành cho Animation lại có sẵn thông qua các API cấp cao hơn. Hãy xem Tuỳ chỉnh ảnh động để biết thêm thông tin về AnimationVectorAnimationSpec.

Sơ đồ chỉ ra mối quan hệ giữa nhiều 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 thông qua 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ừ nhau, nghĩa là việc thay đổi giá trị luôn diễn ra liên tục và mọi ảnh động đang diễn ra sẽ bị huỷ.

Nhiều tính năng của Animatable, bao gồm cả animateTo, được cung cấp dưới dạng hàm tạm ngưng. Nghĩa là các tính năng này cần được chứa 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ên, chúng ta 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 có một ảnh động đang hoạt động khi giá trị bị thay đổi, thì ảnh động sẽ bị huỷ 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.

Đây là triển khai ảnh động sao lưu API animate*AsState đã đề cập trong phần trước. So với animate*AsState, việc sử dụng Animatable trực tiếp giúp kiểm soát chặt chẽ hơn trên một vài khía cạnh. 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ên cho thấy một hộp màu xám ở đầu tiên, ngay lập tức bắt đầu tạo ảnh động sang màu xanh lục hoặc đỏ. Thứ hai, Animatable cung cấp nhiều thao tác 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. Đ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 được đồ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 Cử chỉ và ảnh động để biết thêm thông tin.

Ngay từ đầu, 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. 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 trang Ảnh động. Có hai kiểu Animation phụ: TargetBasedAnimationDecayAnimation.

Bạn chỉ nên 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 mà các API cấp cao hơn sử dụng.

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 tự kiểm soát thời lượng phát ảnh động. Trong ví dụ bên dưới, thời gian phát của TargetAnimation được kiểm soát 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ị do initialVelocityVector thiết lập và sẽ chậm lại theo thời gian.