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

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

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

animate*AsState ফাংশন হল Compose-এ সিঙ্গল ভ্যালু অ্যানিমেট করার জন্য সহজ অ্যানিমেশন API। আপনি শুধু টার্গেট ভ্যালু (বা শেষ ভ্যালু) প্রদান করেন এবং 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 ফাংশন প্রদান করে। আপনি 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-এর সাথে ট্রানজিশন ব্যবহার করুন

Transition-এর এক্সটেনশন ফাংশন হিসেবে AnimatedVisibility এবং AnimatedContent উপলভ্য। 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")
            }
        }
    }
}

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

সহজ ব্যবহারের ক্ষেত্রে, আপনার 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: Coroutine-ভিত্তিক একক ভ্যালু অ্যানিমেশন

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

আগের উদাহরণে, আপনি Animatable-এর একটি ইনস্ট্যান্স তৈরি করেন এবং মনে রাখেন যার প্রাথমিক মান হল Color.Gray। বুলিয়ান ফ্ল্যাগ 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 প্রদান করার প্রয়োজন নেই। পরিবর্তে, এটি targetValue গণনা করে শুরুর শর্তের উপর ভিত্তি করে, যা initialVelocity এবং initialValue এবং প্রদত্ত DecayAnimationSpec দ্বারা সেট করা হয়।

কোনও এলিমেন্টকে ধীরে ধীরে থামানোর জন্য ফ্লিং জেসচারের পরে প্রায়শই ডিকে অ্যানিমেশন ব্যবহার করা হয়। অ্যানিমেশনের গতি initialVelocityVector-এর সেট করা মান থেকে শুরু হয় এবং সময়ের সাথে সাথে কমে যায়।