वैल्यू के हिसाब से ऐनिमेशन

इस पेज पर, Jetpack Compose में वैल्यू के आधार पर ऐनिमेशन बनाने का तरीका बताया गया है. इसमें उन एपीआई पर फ़ोकस किया गया है जो वैल्यू की मौजूदा और टारगेट स्टेट के आधार पर ऐनिमेशन बनाते हैं.

animate*AsState की मदद से, किसी एक वैल्यू को ऐनिमेट करना

animate*AsState फ़ंक्शन, Compose में मौजूद आसान ऐनिमेशन एपीआई हैं. इनका इस्तेमाल, किसी एक वैल्यू को ऐनिमेट करने के लिए किया जाता है. आपको सिर्फ़ टारगेट वैल्यू (या आखिरी वैल्यू) देनी होती है. इसके बाद, एपीआई मौजूदा वैल्यू से लेकर तय की गई वैल्यू तक ऐनिमेशन शुरू करता है.

इस उदाहरण में, इस एपीआई का इस्तेमाल करके ऐल्फ़ा को ऐनिमेट किया गया है. टारगेट वैल्यू को 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 इंस्टेंस) बनाया जाएगा और कॉल साइट पर याद रखा जाएगा. इसमें पहली टारगेट वैल्यू को शुरुआती वैल्यू के तौर पर इस्तेमाल किया जाएगा. इसके बाद, जब भी इस कंपोज़ेबल को कोई अलग टारगेट वैल्यू दी जाएगी, तब उस वैल्यू के लिए ऐनिमेशन अपने-आप शुरू हो जाएगा. अगर कोई ऐनिमेशन पहले से ही चल रहा है, तो ऐनिमेशन अपनी मौजूदा वैल्यू (और रफ़्तार) से शुरू होता है और टारगेट वैल्यू की ओर बढ़ता है. ऐनिमेशन के दौरान, यह कंपोज़ेबल फिर से कंपोज़ होता है और हर फ़्रेम में ऐनिमेशन की अपडेट की गई वैल्यू दिखाता है.

Compose, डिफ़ॉल्ट रूप से Float, Color, Dp, Size, Offset, Rect, Int, IntOffset, और IntSize के लिए animate*AsState फ़ंक्शन उपलब्ध कराता है. TwoWayConverter को animateValueAsState में जोड़कर, अन्य तरह के डेटा के लिए सहायता जोड़ी जा सकती है. यह सामान्य टाइप का डेटा लेता है.

AnimationSpec देकर, ऐनिमेशन की खास बातों को पसंद के मुताबिक बनाया जा सकता है. ज़्यादा जानकारी के लिए, AnimationSpec पर जाएं.

ट्रांज़िशन की मदद से, एक साथ कई प्रॉपर्टी को ऐनिमेट करना

Transition एक या उससे ज़्यादा ऐनिमेशन को अपने चाइल्ड के तौर पर मैनेज करता है. साथ ही, उन्हें कई स्थितियों के बीच एक साथ चलाता है.

स्टेट, किसी भी तरह का डेटा हो सकता है. कई मामलों में, टाइप सेफ़्टी की पुष्टि करने के लिए, कस्टम 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
    }
}

इसके अलावा, ट्रांज़िशन की स्थिति में होने वाले बदलावों के हर कॉम्बिनेशन के लिए, अलग AnimationSpec तय करने के लिए, transitionSpec पैरामीटर पास किया जा सकता है. ज़्यादा जानकारी के लिए, 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 एक जैसे होते हैं. इसका इस्तेमाल, यह जानने के लिए किया जा सकता है कि ट्रांज़िशन पूरा हुआ है या नहीं.

कभी-कभी, हो सकता है कि आपको शुरुआती स्थिति को पहली टारगेट स्थिति से अलग रखना हो. ऐसा करने के लिए, MutableTransitionState के साथ updateTransition का इस्तेमाल किया जा सकता है. उदाहरण के लिए, इसकी मदद से ऐनिमेशन को तब शुरू किया जा सकता है, जब कोड कंपोज़िशन में शामिल हो.

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

एक से ज़्यादा ऐप्लिकेशन बनाने की सुविधा वाले ज़्यादा जटिल ट्रांज़िशन के लिए, createChildTransition का इस्तेमाल करके चाइल्ड ट्रांज़िशन बनाया जा सकता है. यह तकनीक, किसी जटिल कंपोज़ेबल में मौजूद कई सब-कॉम्पोनेंट के बीच की समस्याओं को अलग करने के लिए मददगार है. पैरंट ट्रांज़िशन को चाइल्ड ट्रांज़िशन में मौजूद सभी ऐनिमेशन वैल्यू की जानकारी होती है.

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 के एक्सटेंशन फ़ंक्शन के तौर पर उपलब्ध हैं. Transition.AnimatedVisibility और Transition.AnimatedContent के लिए targetState, Transition से लिया जाता है. साथ ही, Transition के targetState में बदलाव होने पर, ज़रूरत के हिसाब से एंटर, एग्ज़िट, और sizeTransform ऐनिमेशन ट्रिगर होते हैं. इन एक्सटेंशन फ़ंक्शन की मदद से, सभी एंटर, एग्ज़िट, और sizeTransform ऐनिमेशन को होस्ट किया जा सकता है. ऐसा न करने पर, ये AnimatedVisibility/AnimatedContent के लिए इंटरनल हो जाएंगे और Transition में चले जाएंगे. इन एक्सटेंशन फ़ंक्शन की मदद से, AnimatedVisibility/AnimatedContent की स्थिति में होने वाले बदलाव को बाहर से देखा जा सकता है. बूलियन visible पैरामीटर के बजाय, AnimatedVisibility का यह वर्शन एक लैम्डा लेता है. यह लैम्डा, पैरंट ट्रांज़िशन की टारगेट स्टेट को बूलियन में बदलता है.

ज़्यादा जानकारी के लिए, 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")
            }
        }
    }
}

ट्रांज़िशन को एनकैप्सुलेट करना और उसे फिर से इस्तेमाल करने लायक बनाना

इस्तेमाल के आसान उदाहरणों के लिए, ट्रांज़िशन ऐनिमेशन को अपने यूज़र इंटरफ़ेस (यूआई) के जैसे ही कंपोज़ेबल में तय करना एक मान्य विकल्प है. हालांकि, कई ऐनिमेटेड वैल्यू वाले किसी जटिल कॉम्पोनेंट पर काम करते समय, हो सकता है कि आप ऐनिमेशन को कंपोज़ेबल यूज़र इंटरफ़ेस (यूआई) से अलग करना चाहें.

ऐसा करने के लिए, एक ऐसी क्लास बनाएं जिसमें ऐनिमेशन की सभी वैल्यू हों. साथ ही, एक ऐसा 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 में एक या उससे ज़्यादा चाइल्ड ऐनिमेशन होते हैं, जैसे कि Transition. हालांकि, ऐनिमेशन कंपोज़िशन में शामिल होते ही चलने लगते हैं और हटाए जाने तक नहीं रुकते. rememberInfiniteTransition का इस्तेमाल करके, InfiniteTransition का इंस्टेंस बनाया जा सकता है. साथ ही, 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)
)

लो-लेवल ऐनिमेशन एपीआई

पिछले सेक्शन में बताए गए सभी हाई-लेवल ऐनिमेशन एपीआई, लो-लेवल ऐनिमेशन एपीआई पर आधारित होते हैं.

animate*AsState फ़ंक्शन, आसान एपीआई होते हैं. ये वैल्यू में होने वाले बदलाव को ऐनिमेशन वैल्यू के तौर पर तुरंत रेंडर करते हैं. यह सुविधा, Animatable की मदद से काम करती है. यह एक कोरूटीन-आधारित एपीआई है, जो किसी एक वैल्यू को ऐनिमेट करने के लिए इस्तेमाल किया जाता है.

updateTransition एक ट्रांज़िशन ऑब्जेक्ट बनाता है, जो कई ऐनिमेशन वैल्यू को मैनेज कर सकता है. साथ ही, स्थिति बदलने पर उन्हें चला सकता है. rememberInfiniteTransition भी इसी तरह काम करता है. हालांकि, यह एक ऐसा इनफ़ाइनाइट ट्रांज़िशन बनाता है जो कई ऐनिमेशन को मैनेज कर सकता है. ये ऐनिमेशन अनिश्चित समय तक चलते रहते हैं. Animatable को छोड़कर, ये सभी एपीआई कंपोज़ेबल हैं. इसका मतलब है कि इन ऐनिमेशन को कंपोज़िशन के बाहर बनाया जा सकता है.

ये सभी एपीआई, ज़्यादा बुनियादी Animation एपीआई पर आधारित हैं. ज़्यादातर ऐप्लिकेशन, Animation के साथ सीधे तौर पर इंटरैक्ट नहीं करते. हालांकि, आपके पास इसके कुछ कस्टमाइज़ेशन फ़ीचर को ऐक्सेस करने का विकल्प होता है. इसके लिए, आपको हाई-लेवल एपीआई का इस्तेमाल करना होगा. AnimationVector और AnimationSpec के बारे में ज़्यादा जानने के लिए, ऐनिमेशन को पसंद के मुताबिक बनाना लेख पढ़ें.

लो-लेवल ऐनिमेशन एपीआई के बीच संबंध
पहली इमेज. लो-लेवल ऐनिमेशन एपीआई के बीच संबंध.

Animatable: कोरूटीन पर आधारित सिंगल वैल्यू ऐनिमेशन

Animatable एक वैल्यू होल्डर है. यह वैल्यू में बदलाव होने पर, उसे ऐनिमेट कर सकता है. इसके लिए, animateTo का इस्तेमाल किया जाता है. यह एपीआई, animate*AsState को लागू करने में मदद करता है. इससे यह पक्का होता है कि वैल्यू में लगातार बदलाव होता रहे और एक समय पर एक ही ऐनिमेशन चले. इसका मतलब है कि वैल्यू में बदलाव हमेशा लगातार होता है और Compose, चल रहे किसी भी ऐनिमेशन को रद्द कर देता है.

Animatable की कई सुविधाएं, सस्पेंड फ़ंक्शन हैं. इनमें animateTo भी शामिल है. इसका मतलब है कि आपको उन्हें सही कोरूटीन स्कोप में रैप करना होगा. उदाहरण के लिए, 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 है. बूलियन फ़्लैग ok की वैल्यू के आधार पर, रंग Color.Green या Color.Red में ऐनिमेट होता है. बूलियन वैल्यू में बाद में किए गए किसी भी बदलाव से, दूसरे रंग में ऐनिमेशन शुरू हो जाता है. अगर वैल्यू बदलने के दौरान कोई ऐनिमेशन चल रहा है, तो Compose उस ऐनिमेशन को रद्द कर देता है. इसके बाद, नया ऐनिमेशन मौजूदा स्नैपशॉट वैल्यू और मौजूदा रफ़्तार से शुरू होता है.

यह Animatable एपीआई, पिछले सेक्शन में बताए गए animate*AsState को लागू करने के लिए इस्तेमाल किया जाता है. Animatable का सीधे तौर पर इस्तेमाल करने से, आपको कई तरीकों से ज़्यादा बेहतर कंट्रोल मिलता है:

  • पहला, Animatable की शुरुआती वैल्यू, उसकी पहली टारगेट वैल्यू से अलग हो सकती है. उदाहरण के लिए, ऊपर दिए गए कोड के उदाहरण में, पहले एक ग्रे बॉक्स दिखता है. इसके बाद, वह तुरंत हरे या लाल रंग में ऐनिमेट हो जाता है.
  • दूसरा, Animatable कॉन्टेंट की वैल्यू पर ज़्यादा ऑपरेशन उपलब्ध कराता है. खास तौर पर, snapTo और animateDecay.
    • snapTo, मौजूदा वैल्यू को तुरंत टारगेट वैल्यू पर सेट कर देता है. यह तब काम आता है, जब ऐनिमेशन ही सोर्स ऑफ़ ट्रुथ न हो और उसे अन्य स्थितियों के साथ सिंक करना ज़रूरी हो. जैसे, टच इवेंट.
    • animateDecay एक ऐसा ऐनिमेशन शुरू करता है जो दी गई रफ़्तार से धीमा हो जाता है. यह फ़्लिंग बिहेवियर लागू करने के लिए मददगार है.

ज़्यादा जानकारी के लिए, जेस्चर और ऐनिमेशन देखें.

डिफ़ॉल्ट रूप से, Animatable में Float और Color का इस्तेमाल किया जा सकता है. हालांकि, TwoWayConverter देकर किसी भी डेटा टाइप का इस्तेमाल किया जा सकता है. ज़्यादा जानकारी के लिए, AnimationVector देखें.

AnimationSpec देकर, ऐनिमेशन की खास बातों को पसंद के मुताबिक बनाया जा सकता है. ज़्यादा जानकारी के लिए, AnimationSpec पर जाएं.

Animation: मैन्युअल तरीके से कंट्रोल किया जाने वाला ऐनिमेशन

Animation, सबसे निचले लेवल का Animation API है. अब तक हमने जो ऐनिमेशन देखे हैं उनमें से कई Animation पर आधारित हैं. Animation के दो सब-टाइप हैं: TargetBasedAnimation और DecayAnimation.

ऐनिमेशन के समय को मैन्युअल तरीके से कंट्रोल करने के लिए, सिर्फ़ Animation का इस्तेमाल करें. Animation स्टेटलेस है और इसमें लाइफ़साइकल का कोई कॉन्सेप्ट नहीं है. यह हाई-लेवल एपीआई के लिए, ऐनिमेशन कैलकुलेशन इंजन के तौर पर काम करता है.

TargetBasedAnimation

अन्य एपीआई, इस्तेमाल के ज़्यादातर मामलों को कवर करते हैं. हालांकि, सीधे 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

TargetBasedAnimation के उलट, DecayAnimation के लिए targetValue की वैल्यू देना ज़रूरी नहीं है. इसके बजाय, यह initialVelocity और initialValue की ओर से सेट की गई शुरुआती शर्तों और दिए गए DecayAnimationSpec के आधार पर, targetValue का हिसाब लगाता है.

डिके ऐनिमेशन का इस्तेमाल अक्सर फ़्लिंग जेस्चर के बाद किया जाता है, ताकि एलिमेंट की रफ़्तार को कम करके उन्हें रोका जा सके. ऐनिमेशन की रफ़्तार, initialVelocityVector से सेट की गई वैल्यू से शुरू होती है और समय के साथ कम होती जाती है.