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

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

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

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

कभी-कभी हमें शुरुआती स्थिति को पहले टारगेट की स्थिति से अलग रखना होता है. इसके लिए, 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 में बदलाव होने पर, ज़रूरत के मुताबिक ट्रांज़िशन को चालू/बंद करता है. इन एक्सटेंशन फ़ंक्शन की मदद से, AnimatedVisibility/AnimatedContent के अंदर मौजूद सभी enter/exit/sizeTransform ऐनिमेशन को 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, animatedFloat या animatedValue का इस्तेमाल करके, चाइल्ड ऐनिमेशन जोड़े जा सकते हैं. ऐनिमेशन की खास जानकारी देने के लिए, आपको 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 के साथ सीधे तौर पर इंटरैक्ट नहीं करेंगे. हालांकि, Animation को पसंद के मुताबिक बनाने की कुछ सुविधाएं, ज़्यादा लेवल वाले एपीआई के ज़रिए उपलब्ध हैं. AnimationVector और AnimationSpec के बारे में ज़्यादा जानने के लिए, ऐनिमेशन को पसंद के मुताबिक बनाना लेख पढ़ें.

इस डायग्राम में, लो-लेवल के अलग-अलग ऐनिमेशन एपीआई के बीच का संबंध दिखाया गया है

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

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

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 में बदलता है. बूलियन वैल्यू में बाद में होने वाला कोई भी बदलाव, ऐनिमेशन को दूसरे रंग में बदल देता है. अगर वैल्यू बदलते समय कोई ऐनिमेशन चल रहा है, तो ऐनिमेशन रद्द हो जाता है. इसके बाद, नया ऐनिमेशन मौजूदा स्नैपशॉट वैल्यू से शुरू होता है. इसकी स्पीड वही होती है जो पहले वाले ऐनिमेशन की थी.

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

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

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

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

Animation, सबसे कम लेवल वाला Animation API है. अब तक हमने जितने भी ऐनिमेशन देखे हैं उनमें से ज़्यादातर, ऐनिमेशन के ऊपर बनाए गए हैं. 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 में सेट की गई वैल्यू से शुरू होती है और समय के साथ कम होती जाती है.