ভ্যালু-ভিত্তিক অ্যানিমেশন

এই পৃষ্ঠায় Jetpack Compose-এ কীভাবে ভ্যালু-ভিত্তিক অ্যানিমেশন তৈরি করতে হয় তা বর্ণনা করা হয়েছে, এর মধ্যে সেইসব API-এর উপর ফোকাস করা হয়েছে যেগুলি বর্তমান ও টার্গেট স্টেটের উপর ভিত্তি করে ভ্যালু অ্যানিমেট করে।

animate*AsState-এর সাহায্যে একটি ভ্যালু অ্যানিমেট করা

animate*AsState ফাংশন হল একটি ভ্যালু অ্যানিমেট করার জন্য 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 ইনস্ট্যান্স) তৈরি করা হবে এবং কল সাইটে মনে রাখা হবে, যার প্রাথমিক ভ্যালু হিসেবে প্রথম টার্গেট ভ্যালু থাকবে। এর পর থেকে, আপনি যখনই এই কম্পোজ করার উপযুক্ত আইটেমে আলাদা টার্গেট ভ্যালু প্রদান করবেন, তখন সেই ভ্যালুর দিকে একটি অ্যানিমেশন অটোমেটিক শুরু হয়ে যাবে। আগে থেকেই কোনও অ্যানিমেশন ফ্লাইটে থাকলে, অ্যানিমেশনটি তার বর্তমান ভ্যালু (ও ভেলোসিটি) থেকে শুরু হয় এবং টার্গেট ভ্যালুর দিকে অ্যানিমেট করে। অ্যানিমেশনের সময়, এই কম্পোজ করার উপযুক্ত আইটেমটি আবার কম্পোজ করা হয় এবং প্রতিটি ফ্রেমে আপডেট করা অ্যানিমেশন ভ্যালু রিটার্ন করে।

ডিফল্ট হিসেবে, Compose Float, Color, Dp, Size, Offset, Rect, Int, IntOffset এবং IntSize-এর জন্য animate*AsState ফাংশন প্রদান করে। আপনি animateValueAsState-এ TwoWayConverter প্রদান করে অন্যান্য ডেটা ধরনের জন্য সহায়তা যোগ করতে পারেন যা একটি জেনেরিক ধরন নেয়।

আপনি একটি 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 একই হবে। ট্রানজিশন সম্পূর্ণ হয়েছে কিনা তা বোঝার জন্য আপনি এটি সিগন্যাল হিসেবে ব্যবহার করতে পারেন।

কখনও কখনও, আপনি হয়ত প্রথম টার্গেট স্টেট থেকে আলাদা প্রাথমিক স্টেট রাখতে চাইবেন। এটি করতে, আপনি 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")
// ……

একাধিক কম্পোজ করার মতো ফাংশন সহ আরও জটিল ট্রানজিশনের জন্য, আপনি চাইল্ড ট্রানজিশন তৈরি করতে 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 অ্যানিমেশন ট্রিগার করে। এইসব এক্সটেনশন ফাংশন আপনাকে Transition-এ সব এন্টার, এক্সিট, এবং sizeTransform অ্যানিমেশন হোয়েস্ট করতে দেয়, যেগুলি অন্যথায় AnimatedVisibility/AnimatedContent-এর ইন্টার্নাল হবে। এইসব এক্সটেনশন ফাংশনের সাহায্যে, আপনি বাইরে থেকে 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")
            }
        }
    }
}

ট্রানজিশন এনক্যাপসুলেট করা এবং এটিকে আবার ব্যবহারযোগ্য করে তোলা

সহজ ব্যবহারের ক্ষেত্রে, আপনার UI-এর মতো একই কম্পোজেবলে ট্রানজিশন অ্যানিমেশনকে সংজ্ঞায়িত করা একটি বৈধ বিকল্প। তবে, অ্যানিমেটেড ভ্যালুর সংখ্যা বেশি হলে, কোনও জটিল কম্পোনেন্ট নিয়ে কাজ করার সময়, আপনি কম্পোজ করা যায় এমন UI থেকে অ্যানিমেশন প্রয়োগ করার পদ্ধতি আলাদা করতে চাইতে পারেন।

আপনি এমন একটি ক্লাস তৈরি করে এটি করতে পারেন যা সমস্ত অ্যানিমেশন ভ্যালু এবং একটি 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)
)

লো-লেভেল অ্যানিমেশন API

আগের বিভাগে উল্লেখ করা সব হাই-লেভেল অ্যানিমেশন API, লো-লেভেল অ্যানিমেশন API-এর উপর ভিত্তি করে তৈরি করা হয়েছে।

animate*AsState ফাংশন হল সহজ API যা অ্যানিমেশন ভ্যালু হিসেবে তাৎক্ষণিক ভ্যালু পরিবর্তন রেন্ডার করে। এই ফাংশনালিটি Animatable-এর মাধ্যমে পরিচালিত হয়, এটি একটি কোরাউটিন-ভিত্তিক API যা একটি ভ্যালুকে অ্যানিমেট করে।

updateTransition একটি ট্রানজিশন অবজেক্ট তৈরি করে যা একাধিক অ্যানিমেটিং ভ্যালু ম্যানেজ করতে পারে এবং কোনও স্টেটে পরিবর্তন হলে সেগুলি রান করে। rememberInfiniteTransition একই রকম, কিন্তু এটি একটি অসীম ট্রানজিশন তৈরি করে যা একাধিক অ্যানিমেশন ম্যানেজ করতে পারে যা অনির্দিষ্টকালের জন্য চলতে থাকে। Animatable ছাড়া বাকি সব API কম্পোজ করা যায়, যার অর্থ হল আপনি কম্পোজিশনের বাইরে এইসব অ্যানিমেশন তৈরি করতে পারবেন।

এইসব API আরও মৌলিক Animation API-এর উপর ভিত্তি করে তৈরি হয়েছে। যদিও বেশিরভাগ অ্যাপ সরাসরি Animation-এর সাথে ইন্টার‍্যাক্ট করে না, তবে আপনি হাই-লেভেল API-এর মাধ্যমে এর কিছু কাস্টমাইজেশন ক্ষমতা অ্যাক্সেস করতে পারবেন। AnimationVector ও AnimationSpec সম্পর্কে আরও জানতে অ্যানিমেশন কাস্টমাইজ করা দেখুন।

লো-লেভেল অ্যানিমেশন API-এর মধ্যে সম্পর্ক
ছবি ১. লো-লেভেল অ্যানিমেশন API-এর মধ্যে সম্পর্ক।

Animatable: কোরাউটিন-ভিত্তিক সিঙ্গেল ভ্যালু অ্যানিমেশন

Animatable হল একটি ভ্যালু হোল্ডার যা animateTo ব্যবহার করে ভ্যালু পরিবর্তন করার সাথে সাথে তা অ্যানিমেট করতে পারে। এটি হল সেই API যা 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)
)

আগের উদাহরণে, আপনি Color.Gray-এর প্রাথমিক মান সহ Animatable-এর একটি ইনস্ট্যান্স তৈরি ও মনে রাখেন। বুলিয়ান ফ্ল্যাগ ok-এর ভ্যালুর উপর নির্ভর করে, রঙটি Color.Green বা Color.Red-এ অ্যানিমেট করে। বুলিয়ান ভ্যালুতে পরবর্তী কোনও পরিবর্তন হলে, অন্য রঙে অ্যানিমেশন শুরু হয়। মান পরিবর্তন হওয়ার সময় কোনও অ্যানিমেশন চলতে থাকলে, Compose অ্যানিমেশনটি বাতিল করে দেয় এবং নতুন অ্যানিমেশনটি বর্তমান স্ন্যাপশট ভ্যালু থেকে বর্তমান ভেলোসিটি সহ শুরু হয়।

এই Animatable API হল animate*AsState আগের বিভাগে উল্লেখ করা হয়েছে। Animatable সরাসরি ব্যবহার করলে বিভিন্ন উপায়ে আরও সূক্ষ্ম কন্ট্রোল পাওয়া যায়:

  • প্রথমত, Animatable-এর প্রাথমিক মান, প্রথম টার্গেট মানের থেকে আলাদা হতে পারে। যেমন, আগের কোডের উদাহরণে প্রথমে একটি ধূসর বক্স দেখানো হয়েছে, যা সঙ্গে সঙ্গে অ্যানিমেট হয়ে সবুজ বা লাল হয়ে যায়।
  • দ্বিতীয়ত, Animatable কন্টেন্ট ভ্যালুর উপর আরও অপারেশন প্রদান করে, বিশেষ করে snapTo এবং animateDecay।
    • snapTo বর্তমান ভ্যালুকে অবিলম্বে টার্গেট ভ্যালুতে সেট করে। অ্যানিমেশনই একমাত্র সত্যের উৎস না হলে এবং টাচ ইভেন্টের মতো অন্যান্য স্টেটের সাথে সিঙ্ক্রোনাইজ করতে হলে এটি উপযোগী।
    • animateDecay প্রদত্ত ভেলোসিটি থেকে ধীরে ধীরে কমে যাওয়া অ্যানিমেশন শুরু করে। ফ্লিং আচরণ প্রয়োগ করার জন্য এটি সহায়ক।

আরও তথ্যের জন্য জেসচার ও অ্যানিমেশন দেখুন।

সাধারণত, Animatable-এ Float ও Color কাজ করে, তবে আপনি TwoWayConverter প্রদান করে যেকোনও ডেটা টাইপ ব্যবহার করতে পারবেন। আরও তথ্যের জন্য AnimationVector দেখুন।

আপনি AnimationSpec প্রদান করে অ্যানিমেশন স্পেসিফিকেশন কাস্টমাইজ করতে পারবেন। আরও তথ্যের জন্য AnimationSpec দেখুন।

Animation: ম্যানুয়ালি কন্ট্রোল করা অ্যানিমেশন

Animation হল উপলভ্য সবচেয়ে লো-লেভেল অ্যানিমেশন API। আমরা এতক্ষণ যতগুলি অ্যানিমেশন দেখেছি, তার বেশিরভাগই Animation-এর উপর ভিত্তি করে তৈরি করা হয়েছে। দুটি Animation সাবটাইপ আছে: TargetBasedAnimation এবং DecayAnimation.

অ্যানিমেশনের সময় ম্যানুয়ালি কন্ট্রোল করতে শুধুমাত্র Animation ব্যবহার করুন। Animation হল স্টেটলেস এবং এর কোনও লাইফসাইকেল কনসেপ্ট নেই। এটি উচ্চ-স্তরের API-এর জন্য অ্যানিমেশন গণনা ইঞ্জিন হিসেবে কাজ করে।

TargetBasedAnimation

অন্যান্য API বেশিরভাগ ব্যবহারিক প্রয়োগ কভার করে, কিন্তু TargetBasedAnimation সরাসরি ব্যবহার করলে আপনি অ্যানিমেশনের প্লে টাইম নিয়ন্ত্রণ করতে পারবেন। নিচের উদাহরণে, আপনি ম্যানুয়ালি withFrameNanos-এর দেওয়া ফ্রেম টাইম অনুযায়ী TargetAnimation-এর প্লে টাইম কন্ট্রোল করছেন।

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 সেট করা ভ্যালু থেকে অ্যানিমেশন ভেলোসিটি শুরু হয় এবং সময়ের সাথে সাথে তা কমে যায়।