ภาพเคลื่อนไหวตามมูลค่า

หน้านี้อธิบายวิธีสร้างภาพเคลื่อนไหวตามมูลค่าใน Jetpack Compose โดยเน้นที่ API ที่สร้างภาพเคลื่อนไหวของค่าตามสถานะปัจจุบันและสถานะเป้าหมาย

สร้างภาพเคลื่อนไหวของค่าเดียวด้วย animate*AsState

ฟังก์ชัน animate*AsState เป็น API ภาพเคลื่อนไหวที่ตรงไปตรงมาใน Compose สำหรับสร้างภาพเคลื่อนไหวของค่าเดียว คุณเพียงแค่ระบุค่าเป้าหมาย (หรือค่าสิ้นสุด) และ API จะเริ่มภาพเคลื่อนไหวจากค่าปัจจุบันไปยังค่าที่ระบุ

ตัวอย่างต่อไปนี้สร้างภาพเคลื่อนไหวของค่าอัลฟ่าโดยใช้ API นี้ เมื่อห่อค่าเป้าหมาย ใน animateFloatAsState ค่าอัลฟ่าจะเป็นค่าภาพเคลื่อนไหว ระหว่างค่าที่ระบุ (1f หรือ 0.5f ในกรณีนี้)

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)
)

คุณไม่จำเป็นต้องสร้างอินสแตนซ์ของคลาสภาพเคลื่อนไหวหรือจัดการการหยุดชะงัก เบื้องหลัง ระบบจะสร้างและจดจำออบเจ็กต์ภาพเคลื่อนไหว (กล่าวคือ อินสแตนซ์ Animatable) ที่ตำแหน่งการเรียก โดยมีค่าเป้าหมายแรกเป็นค่าเริ่มต้น จากนั้น เมื่อใดก็ตามที่คุณระบุค่าเป้าหมายอื่นให้กับ Composable นี้ ระบบจะเริ่มภาพเคลื่อนไหวไปยังค่านั้นโดยอัตโนมัติ หากมีภาพเคลื่อนไหวที่กำลังทำงานอยู่ ระบบจะเริ่มภาพเคลื่อนไหวจากค่าปัจจุบัน (และความเร็ว) และสร้างภาพเคลื่อนไหวไปยังค่าเป้าหมาย ระหว่างภาพเคลื่อนไหว Composable นี้จะได้รับการคอมโพสใหม่และแสดงผลค่าภาพเคลื่อนไหวที่อัปเดตทุกเฟรม

โดยค่าเริ่มต้น Compose จะมีฟังก์ชัน animate*AsState สำหรับ Float, Color, Dp, Size, Offset, Rect, Int, IntOffset, และ IntSize คุณสามารถเพิ่มการรองรับประเภทข้อมูลอื่นๆ ได้โดยระบุ TwoWayConverter ให้กับ animateValueAsState ที่ใช้ประเภททั่วไป

คุณสามารถปรับแต่งข้อมูลจำเพาะของภาพเคลื่อนไหวได้โดยระบุ AnimationSpec ดูข้อมูลเพิ่มเติมได้ที่ AnimationSpec

สร้างภาพเคลื่อนไหวของพร็อพเพอร์ตี้หลายรายการพร้อมกันด้วยการทรานซิชัน

Transition จัดการภาพเคลื่อนไหวอย่างน้อย 1 รายการเป็นภาพเคลื่อนไหวลูก และเรียกใช้ภาพเคลื่อนไหวเหล่านั้นพร้อมกันระหว่างสถานะต่างๆ

สถานะอาจเป็นประเภทข้อมูลใดก็ได้ ในหลายๆ กรณี คุณสามารถใช้ประเภท enum ที่กำหนดเองเพื่อยืนยันความปลอดภัยของประเภทได้ ดังตัวอย่างต่อไปนี้

enum class BoxState {
    Collapsed,
    Expanded
}

updateTransition จะสร้างและจดจำอินสแตนซ์ของ Transition และ อัปเดตสถานะ

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

จากนั้นคุณสามารถใช้ฟังก์ชันส่วนขยาย animate* รายการใดรายการหนึ่งเพื่อกำหนดภาพเคลื่อนไหวลูกในการทรานซิชันนี้ ระบุค่าเป้าหมายสำหรับแต่ละสถานะ ฟังก์ชัน animate* เหล่านี้จะแสดงผลค่าภาพเคลื่อนไหวที่อัปเดตทุกเฟรมระหว่างภาพเคลื่อนไหวเมื่อสถานะการทรานซิชันได้รับการอัปเดตด้วย 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
    }
}

หรือคุณจะส่งพารามิเตอร์ transitionSpec เพื่อระบุ AnimationSpec ที่แตกต่างกันสำหรับการเปลี่ยนแปลงสถานะการทรานซิชันแต่ละชุดก็ได้ ดูข้อมูลเพิ่มเติมได้ที่ AnimationSpec

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
    }
}

เมื่อการทรานซิชันไปถึงสถานะเป้าหมายแล้ว Transition.currentState จะเหมือนกับ Transition.targetState คุณสามารถใช้ค่านี้เป็นสัญญาณเพื่อดูว่าการเปลี่ยนผ่านเสร็จสิ้นแล้วหรือไม่

บางครั้งคุณอาจต้องการให้สถานะเริ่มต้นแตกต่างจากสถานะเป้าหมายแรก คุณสามารถใช้ updateTransition กับ MutableTransitionState เพื่อให้ได้ผลลัพธ์นี้ เช่น ฟังก์ชันนี้ช่วยให้คุณเริ่มภาพเคลื่อนไหวได้ทันทีที่โค้ดเข้าสู่การคอมโพส

// 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")
// ……

สำหรับการทรานซิชันที่ซับซ้อนมากขึ้นซึ่งเกี่ยวข้องกับฟังก์ชัน Composable หลายรายการ คุณสามารถ ใช้ createChildTransition เพื่อสร้างการทรานซิชันลูกได้ เทคนิคนี้มีประโยชน์สำหรับการแยกความกังวลระหว่างคอมโพเนนต์ย่อยหลายรายการใน Composable ที่ซับซ้อน การทรานซิชันระดับบนจะทราบค่าภาพเคลื่อนไหวทั้งหมดในการทรานซิชันลูก

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
            }
        )
    }
}

ใช้การทรานซิชันกับ AnimatedVisibility และ AnimatedContent

AnimatedVisibility และ AnimatedContent พร้อมใช้งานเป็นฟังก์ชันส่วนขยายของ Transition targetState สำหรับ Transition.AnimatedVisibility และ Transition.AnimatedContent ได้มาจาก Transition และทริกเกอร์ ภาพเคลื่อนไหวเข้า ออก และ sizeTransform ตามความจำเป็นเมื่อ Transition's targetState เปลี่ยนไป ฟังก์ชันส่วนขยายเหล่านี้ช่วยให้คุณยกภาพเคลื่อนไหวเข้า ออก และ sizeTransform ทั้งหมดที่ปกติจะอยู่ภายใน AnimatedVisibility/AnimatedContent ไปไว้ใน Transition ฟังก์ชันส่วนขยายเหล่านี้ช่วยให้คุณสังเกตการเปลี่ยนแปลงสถานะของ AnimatedVisibility/AnimatedContent จากภายนอกได้ AnimatedVisibility เวอร์ชันนี้ใช้แลมบ์ดาที่แปลงสถานะเป้าหมายของการทรานซิชันระดับบนเป็นบูลีน แทนที่จะใช้พารามิเตอร์ visible แบบบูลีน

ดูรายละเอียดได้ที่ AnimatedVisibility และ AnimatedContent

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")
            }
        }
    }
}

ห่อหุ้มการเปลี่ยนผ่านและทำให้การเปลี่ยนผ่านนำกลับมาใช้ซ้ำได้

สำหรับกรณีการใช้งานที่ตรงไปตรงมา การกำหนดภาพเคลื่อนไหวการทรานซิชันใน Composable เดียวกันกับ UI เป็นตัวเลือกที่ใช้ได้ อย่างไรก็ตาม เมื่อทำงานกับคอมโพเนนต์ที่ซับซ้อนซึ่งมีค่าภาพเคลื่อนไหวจำนวนมาก คุณอาจต้องการแยกการใช้งานภาพเคลื่อนไหวออกจาก UI ของ Composable

คุณสามารถทำได้โดยสร้างคลาสที่เก็บค่าภาพเคลื่อนไหวทั้งหมดและฟังก์ชัน update ที่แสดงผลอินสแตนซ์ของคลาสนั้น คุณสามารถแยกการใช้งานการทรานซิชันไปไว้ในฟังก์ชันแยกต่างหากใหม่ได้ รูปแบบนี้มีประโยชน์เมื่อคุณต้องการรวมตรรกะภาพเคลื่อนไหวไว้ที่ส่วนกลางหรือทำให้ภาพเคลื่อนไหวที่ซับซ้อนนำกลับมาใช้ซ้ำได้

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) }
}

สร้างภาพเคลื่อนไหวที่เล่นซ้ำไม่สิ้นสุดด้วย rememberInfiniteTransition

InfiniteTransition เก็บภาพเคลื่อนไหวลูกอย่างน้อย 1 รายการ เช่น Transition, แต่ภาพเคลื่อนไหวจะเริ่มทำงานทันทีที่เข้าสู่การคอมโพสและจะไม่ หยุดจนกว่าจะนำออก คุณสามารถสร้างอินสแตนซ์ของ InfiniteTransition ด้วย rememberInfiniteTransition และเพิ่มภาพเคลื่อนไหวลูกด้วย animateColor, animateFloat หรือ animateValue นอกจากนี้ คุณยังต้องระบุ infiniteRepeatable เพื่อระบุข้อมูลจำเพาะของภาพเคลื่อนไหว

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 ภาพเคลื่อนไหวระดับต่ำ

API ภาพเคลื่อนไหวระดับสูงทั้งหมดที่กล่าวถึงในส่วนก่อนหน้าสร้างขึ้นจาก API ภาพเคลื่อนไหวระดับต่ำ

ฟังก์ชัน animate*AsState เป็น API ที่ตรงไปตรงมาซึ่งแสดงการเปลี่ยนแปลงค่าทันทีเป็นค่าภาพเคลื่อนไหว ฟังก์ชันนี้ได้รับการสนับสนุนโดย Animatable ซึ่งเป็น API ที่อิงตามโครูทีนสำหรับสร้างภาพเคลื่อนไหวของค่าเดียว

updateTransition จะสร้างออบเจ็กต์การทรานซิชันที่จัดการค่าภาพเคลื่อนไหวหลายค่าและเรียกใช้ค่าเหล่านั้นได้เมื่อสถานะเปลี่ยนไป rememberInfiniteTransition มีลักษณะคล้ายกัน แต่จะสร้างการทรานซิชันแบบไม่สิ้นสุดที่จัดการภาพเคลื่อนไหวหลายรายการที่ทำงานต่อไปเรื่อยๆ ได้ API เหล่านี้ทั้งหมดเป็น Composable ยกเว้น Animatable ซึ่งหมายความว่าคุณสามารถสร้างภาพเคลื่อนไหวเหล่านี้ภายนอกการคอมโพสได้

API เหล่านี้ทั้งหมดอิงตาม API Animation ที่เป็นพื้นฐานมากกว่า แม้ว่าแอปส่วนใหญ่จะไม่ได้โต้ตอบกับ Animation โดยตรง แต่คุณก็เข้าถึงความสามารถในการปรับแต่งบางอย่างได้ผ่าน API ระดับสูงกว่า ดูข้อมูลเพิ่มเติมเกี่ยวกับ AnimationVector และ AnimationSpec ได้ที่ ปรับแต่ง ภาพเคลื่อนไหว

ความสัมพันธ์ระหว่าง API ภาพเคลื่อนไหวระดับต่ำ
รูปที่ 1 ความสัมพันธ์ระหว่าง API ภาพเคลื่อนไหวระดับต่ำ

Animatable: ภาพเคลื่อนไหวค่าเดียวที่อิงตามโครูทีน

Animatable เป็นตัวยึดค่าที่สร้างภาพเคลื่อนไหวของค่าได้เมื่อมีการเปลี่ยนแปลง โดยใช้ animateTo ซึ่งเป็น API ที่สนับสนุนการใช้งาน animate*AsState โดยจะช่วยให้มั่นใจได้ว่าการดำเนินการจะต่อเนื่องและเป็นไปในทิศทางเดียวกัน ซึ่งหมายความว่าการเปลี่ยนแปลงค่าจะต่อเนื่องเสมอ และ Compose จะยกเลิกภาพเคลื่อนไหวที่กำลังทำงานอยู่

ฟีเจอร์หลายอย่างของ Animatable รวมถึง animateTo เป็นฟังก์ชันระงับ ซึ่งหมายความว่าคุณต้องห่อฟังก์ชันเหล่านี้ไว้ในขอบเขตโครูทีนที่เหมาะสม ตัวอย่างเช่น คุณสามารถใช้ Composable LaunchedEffect เพื่อสร้างขอบเขตสำหรับระยะเวลาของค่าคีย์ที่ระบุเท่านั้น

// 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)
)

ในตัวอย่างก่อนหน้า คุณจะสร้างและจดจำอินสแตนซ์ของ Animatable ที่มีค่าเริ่มต้นเป็น Color.Gray สีจะสร้างภาพเคลื่อนไหวเป็น Color.Green หรือ Color.Red ทั้งนี้ขึ้นอยู่กับค่าของแฟล็กบูลีน ok การเปลี่ยนแปลงค่าบูลีนในภายหลังจะเริ่มภาพเคลื่อนไหวเป็นสีอื่น หากภาพเคลื่อนไหวกำลังทำงานอยู่เมื่อค่าเปลี่ยนไป Compose จะยกเลิกภาพเคลื่อนไหว และภาพเคลื่อนไหวใหม่จะเริ่มจากค่าสแนปช็อตปัจจุบันด้วยความเร็วปัจจุบัน

API Animatable นี้เป็นการใช้งานพื้นฐานสำหรับ animate*AsState ที่กล่าวถึงในส่วนก่อนหน้า การใช้ Animatable โดยตรงช่วยให้ควบคุมได้ละเอียดยิ่งขึ้นในหลายๆ ด้าน ดังนี้

  • ประการแรก Animatable อาจมีค่าเริ่มต้นที่แตกต่างจากค่าเป้าหมายแรก ตัวอย่างเช่น ตัวอย่างโค้ดก่อนหน้าแสดงกล่องสีเทาก่อน ซึ่งจะสร้างภาพเคลื่อนไหวเป็นสีเขียวหรือสีแดงทันที
  • ประการที่สอง Animatable มีการดำเนินการเพิ่มเติมกับค่าเนื้อหา ได้แก่ snapTo และ animateDecay
    • snapTo จะตั้งค่าปัจจุบันเป็นค่าเป้าหมายทันที ฟังก์ชันนี้มีประโยชน์เมื่อภาพเคลื่อนไหวไม่ใช่แหล่งข้อมูลที่เชื่อถือได้เพียงแหล่งเดียวและต้องซิงค์กับสถานะอื่นๆ เช่น เหตุการณ์การสัมผัส
    • animateDecay จะเริ่มภาพเคลื่อนไหวที่ช้าลงจากความเร็วที่กำหนด ฟังก์ชันนี้มีประโยชน์สำหรับการใช้งานลักษณะการทำงานแบบปัด

ดูข้อมูลเพิ่มเติมได้ที่ ท่าทางสัมผัสและภาพเคลื่อนไหว

โดยค่าเริ่มต้น Animatable รองรับ Float และ Color แต่คุณสามารถใช้ประเภทข้อมูลใดก็ได้โดยระบุ TwoWayConverter ดูข้อมูลเพิ่มเติมได้ที่ AnimationVector

คุณสามารถปรับแต่งข้อมูลจำเพาะของภาพเคลื่อนไหวได้โดยระบุ AnimationSpec ดูข้อมูลเพิ่มเติมได้ที่ AnimationSpec

Animation: ภาพเคลื่อนไหวที่ควบคุมด้วยตนเอง

Animation เป็น API ภาพเคลื่อนไหวระดับต่ำสุดที่มี ภาพเคลื่อนไหวหลายรายการที่เราเห็นมาจนถึงตอนนี้สร้างขึ้นจาก Animation มี Animation ประเภทย่อย 2 ประเภท ได้แก่ TargetBasedAnimation และ DecayAnimation

ใช้ Animation เพื่อควบคุมเวลาของภาพเคลื่อนไหวด้วยตนเองเท่านั้น Animation ไม่มีสถานะและไม่มีแนวคิดเรื่องวงจรการทำงาน โดยทำหน้าที่เป็นกลไกการคำนวณภาพเคลื่อนไหวสำหรับ API ระดับสูงกว่า

TargetBasedAnimation

API อื่นๆ ครอบคลุมกรณีการใช้งานส่วนใหญ่ แต่การใช้ TargetBasedAnimation โดยตรงจะช่วยให้คุณควบคุมเวลาเล่นของภาพเคลื่อนไหวได้ ในตัวอย่างต่อไปนี้ คุณจะควบคุมเวลาเล่นของ TargetAnimation ด้วยตนเองตามเวลาที่ใช้ในการแสดงผลเฟรมที่ระบุโดย withFrameNanos

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

DecayAnimation ไม่จำเป็นต้องระบุ targetValue ซึ่งแตกต่างจาก TargetBasedAnimation แต่จะคำนวณ targetValue ตามเงื่อนไขเริ่มต้นที่กำหนดโดย initialVelocity และ initialValue รวมถึง DecayAnimationSpec ที่ระบุ

โดยมักใช้ภาพเคลื่อนไหวแบบลดความเร็วหลังจากท่าทางสัมผัสแบบปัดเพื่อชะลอองค์ประกอบให้หยุด ความเร็วของภาพเคลื่อนไหวจะเริ่มต้นที่ค่าที่ initialVelocityVector กำหนดและช้าลงเมื่อเวลาผ่านไป