স্টাইলে অবস্থা এবং অ্যানিমেশন

স্টাইলস এপিআই (Styles API) hovered , focused , এবং pressed -এর মতো ইন্টারঅ্যাকশন স্টেট চলাকালীন ইউআই (UI) পরিবর্তনগুলি পরিচালনা করার জন্য একটি ডিক্লারেটিভ এবং সুবিন্যস্ত পদ্ধতি প্রদান করে। এই এপিআই-এর সাহায্যে, মডিফায়ার ব্যবহার করার সময় সাধারণত প্রয়োজনীয় বয়লারপ্লেট কোড আপনি উল্লেখযোগ্যভাবে কমাতে পারেন।

রিঅ্যাক্টিভ স্টাইলিং সহজ করার জন্য, StyleState একটি স্থিতিশীল, রিড-অনলি ইন্টারফেস হিসেবে কাজ করে যা কোনো এলিমেন্টের সক্রিয় অবস্থা (যেমন এর এনাবলড, প্রেসড বা ফোকাসড স্ট্যাটাস) ট্র্যাক করে। একটি StyleScope এর মধ্যে, আপনি আপনার স্টাইল ডেফিনিশনে সরাসরি কন্ডিশনাল লজিক প্রয়োগ করতে state প্রপার্টির মাধ্যমে এটি অ্যাক্সেস করতে পারেন।

অবস্থা-ভিত্তিক মিথস্ক্রিয়া: হোভার করা, ফোকাস করা, চাপ দেওয়া, নির্বাচিত, সক্রিয়, টগল করা

স্টাইলগুলোতে সাধারণ ইন্টারঅ্যাকশনের জন্য অন্তর্নির্মিত সমর্থন রয়েছে:

  • চাপ দেওয়া
  • হোভার করা
  • নির্বাচিত
  • সক্ষম করা হয়েছে
  • টগল করা হয়েছে

কাস্টম স্টেট সমর্থন করাও সম্ভব। আরও তথ্যের জন্য ‘StyleState দিয়ে কাস্টম স্টেট স্টাইলিং’ বিভাগটি দেখুন।

স্টাইল প্যারামিটার ব্যবহার করে ইন্টারঅ্যাকশন স্টেটগুলো পরিচালনা করুন

নিম্নলিখিত উদাহরণটি ইন্টারঅ্যাকশন অবস্থার প্রতিক্রিয়ায় background এবং borderColor পরিবর্তন করা দেখায়, বিশেষত হোভার করলে বেগুনি এবং ফোকাস করলে নীল রঙে পরিবর্তিত হয়:

@Preview
@Composable
private fun OpenButton() {
    BaseButton(
        style = outlinedButtonStyle then {
            background(Color.White)
            hovered {
                background(lightPurple)
                border(2.dp, lightPurple)
            }
            focused {
                background(lightBlue)
            }
        },
        onClick = {  },
        content = {
            BaseText("Open in Studio", style = {
                contentColor(Color.Black)
                fontSize(26.sp)
                textAlign(TextAlign.Center)
            })
        }
    )
}

চিত্র ১. হোভার এবং ফোকাস অবস্থার উপর ভিত্তি করে পটভূমির রঙ পরিবর্তন।

আপনি নেস্টেড স্টেট ডেফিনিশনও তৈরি করতে পারেন। উদাহরণস্বরূপ, যখন একটি বাটন একই সাথে চাপা এবং হোভার করা হয়, তখন তার জন্য আপনি একটি নির্দিষ্ট স্টাইল নির্ধারণ করতে পারেন:

@Composable
private fun OpenButton_CombinedStates() {
    BaseButton(
        style = outlinedButtonStyle then {
            background(Color.White)
            hovered {
                // light purple
                background(lightPurple)
                pressed {
                    // When running on a device that can hover, whilst hovering and then pressing the button this would be invoked
                    background(lightOrange)
                }
            }
            pressed {
                // when running on a device without a mouse attached, this would be invoked as you wouldn't be in a hovered state only
                background(lightRed)
            }
            focused {
                background(lightBlue)
            }
        },
        onClick = {  },
        content = {
            BaseText("Open in Studio", style = {
                contentColor(Color.Black)
                fontSize(26.sp)
                textAlign(TextAlign.Center)
            })
        }
    )
}

চিত্র ২। একটি বাটনের উপর মাউস হোভার এবং চাপ দেওয়া অবস্থা একসাথে।

Modifier.styleable ব্যবহার করে কাস্টম কম্পোজেবল

নিজের styleable কম্পোনেন্ট তৈরি করার সময়, আপনাকে অবশ্যই একটি interactionSource একটি styleState এর সাথে সংযুক্ত করতে হবে। তারপর, এটিকে ব্যবহার করার জন্য এই স্টেটটি Modifier.styleable এর মধ্যে পাস করুন।

এমন একটি পরিস্থিতির কথা ভাবুন যেখানে আপনার ডিজাইন সিস্টেমে একটি GradientButton অন্তর্ভুক্ত আছে। আপনি হয়তো এমন একটি LoginButton তৈরি করতে চাইতে পারেন যা GradientButton থেকে ইনহেরিট করবে, কিন্তু বাটনটি চাপার মতো ইন্টারঅ্যাকশনের সময় এর রং পরিবর্তন করবে।

  • interactionSource স্টাইলের আপডেটগুলি সক্ষম করতে, আপনার composable-এর মধ্যে একটি প্যারামিটার হিসেবে interactionSource অন্তর্ভুক্ত করুন। প্রদত্ত প্যারামিটারটি ব্যবহার করুন অথবা, যদি কোনোটি সরবরাহ করা না থাকে, তাহলে একটি নতুন MutableInteractionSource ইনিশিয়ালাইজ করুন।
  • interactionSource প্রদান করে styleState কে ইনিশিয়ালাইজ করুন। নিশ্চিত করুন যে styleState এর enabled স্ট্যাটাসটি প্রদত্ত enabled প্যারামিটারের মানকে প্রতিফলিত করে।
  • focusable এবং clickable মডিফায়ারগুলোতে interactionSource নির্ধারণ করুন। সবশেষে, মডিফায়ারের styleable প্যারামিটারে styleState প্রয়োগ করুন।

@Composable
private fun GradientButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    style: Style = Style,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource? = null,
    content: @Composable RowScope.() -> Unit,
) {
    val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
    val styleState = rememberUpdatedStyleState(interactionSource) {
        it.isEnabled = enabled
    }
    Row(
        modifier =
            modifier
                .clickable(
                    onClick = onClick,
                    enabled = enabled,
                    interactionSource = interactionSource,
                    indication = null,
                )
                .styleable(styleState, baseGradientButtonStyle then style),
        content = content,
    )
}

এখন আপনি স্টাইল ব্লকের ভিতরে pressed, focused, এবং hovered অপশনগুলির মাধ্যমে স্টাইল পরিবর্তন করতে interactionSource স্টেটটি ব্যবহার করতে পারেন:

@Preview
@Composable
fun LoginButton() {
    val loginButtonStyle = Style {
        pressed {
            background(
                Brush.linearGradient(
                    listOf(Color.Magenta, Color.Red)
                )
            )
        }
    }
    GradientButton(onClick = {
        // Login logic
    }, style = loginButtonStyle) {
        BaseText("Login")
    }
}

চিত্র ৩. interactionSource উপর ভিত্তি করে একটি কাস্টম কম্পোজেবল স্টেট পরিবর্তন করা।

শৈলী পরিবর্তনগুলি অ্যানিমেট করুন

স্টাইলের স্টেট পরিবর্তনের সাথে বিল্ট-ইন অ্যানিমেশন সাপোর্ট রয়েছে। বিভিন্ন স্টেটের মধ্যে স্বয়ংক্রিয়ভাবে অ্যানিমেশন যোগ করার জন্য আপনি যেকোনো স্টেট চেঞ্জ ব্লকের মধ্যে 'new' প্রপার্টিটিকে animate দিয়ে র‍্যাপ করতে পারেন। এটি animate*AsState API-গুলোর মতোই। নিচের উদাহরণটিতে, স্টেট 'focused'-এ পরিবর্তিত হলে borderColor কে কালো থেকে নীল রঙে অ্যানিমেট করা হয়েছে:

val animatingStyle = Style {
    externalPadding(48.dp)
    border(3.dp, Color.Black)
    background(Color.White)
    size(100.dp)

    pressed {
        animate {
            borderColor(Color.Magenta)
            background(Color(0xFFB39DDB))
        }
    }
}

@Preview
@Composable
private fun AnimatingStyleChanges() {
    val interactionSource = remember { MutableInteractionSource() }
    val styleState = remember(interactionSource) { MutableStyleState(interactionSource) }
    Box(modifier = Modifier
        .clickable(
            interactionSource,
            enabled = true,
            indication = null,
            onClick = {

            }
        )
        .styleable(styleState, animatingStyle)) {

    }
}

চিত্র ৪। প্রেসে রঙের পরিবর্তন অ্যানিমেট করা।

animate API-টি অ্যানিমেশন কার্ভের সময়কাল বা আকৃতি পরিবর্তন করার জন্য একটি animationSpec গ্রহণ করে। নিচের উদাহরণটি একটি spring spec ব্যবহার করে বক্সটির আকার অ্যানিমেট করে:

val animatingStyleSpec = Style {
    externalPadding(48.dp)
    border(3.dp, Color.Black)
    background(Color.White)
    size(100.dp)
    transformOrigin(TransformOrigin.Center)
    pressed {
        animate {
            borderColor(Color.Magenta)
            background(Color(0xFFB39DDB))
        }
        animate(spring(dampingRatio = Spring.DampingRatioMediumBouncy)) {
            scale(1.2f)
        }
    }
}

@Preview(showBackground = true)
@Composable
fun AnimatingStyleChangesSpec() {
    val interactionSource = remember { MutableInteractionSource() }
    val styleState = remember(interactionSource) { MutableStyleState(interactionSource) }
    Box(modifier = Modifier
        .clickable(
            interactionSource,
            enabled = true,
            indication = null,
            onClick = {

            }
        )
        .styleable(styleState, animatingStyleSpec))
}

চিত্র ৫. প্রেসে আকার ও রঙের পরিবর্তন অ্যানিমেট করা।

StyleState দিয়ে কাস্টম স্টেট স্টাইলিং

আপনার কম্পোজেবল ব্যবহারের ধরনের ওপর নির্ভর করে, আপনার বিভিন্ন স্টাইল থাকতে পারে যা কাস্টম স্টেট দ্বারা সমর্থিত। উদাহরণস্বরূপ, যদি আপনার একটি মিডিয়া অ্যাপ থাকে, তাহলে আপনি প্লেয়ারের প্লেব্যাক স্টেটের ওপর নির্ভর করে আপনার MediaPlayer কম্পোজেবলের বাটনগুলোর জন্য ভিন্ন স্টাইলিং রাখতে চাইতে পারেন। আপনার নিজস্ব কাস্টম স্টেট তৈরি এবং ব্যবহার করতে এই ধাপগুলো অনুসরণ করুন:

  1. কাস্টম কী সংজ্ঞায়িত করুন
  2. StyleState এক্সটেনশন তৈরি করুন
  3. কাস্টম স্টেটের লিঙ্ক

কাস্টম কী সংজ্ঞায়িত করুন

একটি কাস্টম স্টেট-ভিত্তিক স্টাইল তৈরি করতে, প্রথমে একটি StyleStateKey তৈরি করুন এবং ডিফল্ট স্টেট ভ্যালুটি পাস করুন। যখন অ্যাপটি চালু হয়, মিডিয়া প্লেয়ারটি Stopped স্টেটে থাকে, তাই এটি এইভাবে ইনিশিয়ালাইজ করা হয়:

enum class PlayerState {
    Stopped,
    Playing,
    Paused
}

val playerStateKey = StyleStateKey(PlayerState.Stopped)

স্টাইলস্টেট এক্সটেনশন ফাংশন তৈরি করুন

বর্তমান playState জানতে StyleState উপর একটি এক্সটেনশন ফাংশন সংজ্ঞায়িত করুন। তারপর, playStateKey , নির্দিষ্ট স্টেট সহ একটি ল্যাম্বডা এবং স্টাইল পাস করে আপনার কাস্টম স্টেটগুলো দিয়ে StyleScope এর উপর এক্সটেনশন ফাংশন তৈরি করুন।

// Extension Function on MutableStyleState to query and set the current playState
var MutableStyleState.playerState
    get() = this[playerStateKey]
    set(value) { this[playerStateKey] = value }

fun StyleScope.playerPlaying(value: Style) {
    state(playerStateKey, value, { key, state -> state[key] == PlayerState.Playing })
}
fun StyleScope.playerPaused(value: Style) {
    state(playerStateKey, value, { key, state -> state[key] == PlayerState.Paused })
}

আপনার কম্পোজেবলে styleState সংজ্ঞায়িত করুন এবং styleState.playState কে ইনকামিং স্টেটের সমান সেট করুন। মডিফায়ারের styleable ফাংশনে styleState পাস করুন।

style ল্যাম্বডার মধ্যে, আপনি পূর্বে সংজ্ঞায়িত এক্সটেনশন ফাংশনগুলো ব্যবহার করে কাস্টম স্টেটগুলোর জন্য স্টেট-ভিত্তিক স্টাইলিং প্রয়োগ করতে পারেন।

নিম্নলিখিত কোডটি এই উদাহরণের সম্পূর্ণ কোড স্নিপেট:

enum class PlayerState {
    Stopped,
    Playing,
    Paused
}
val playerStateKey = StyleStateKey<PlayerState>(PlayerState.Stopped)
var MutableStyleState.playerState
    get() = this[playerStateKey]
    set(value) { this[playerStateKey] = value }

fun StyleScope.playerPlaying(value: Style) {
    state(playerStateKey, value, { key, state -> state[key] == PlayerState.Playing })
}
fun StyleScope.playerPaused(value: Style) {
    state(playerStateKey, value, { key, state -> state[key] == PlayerState.Paused })

}

@Composable
fun MediaPlayer(
    url: String,
    modifier: Modifier = Modifier,
    style: Style = Style,
    state: PlayerState = remember { PlayerState.Paused }
) {
    // Hoist style state, set playstate as a parameter,
    val styleState = remember { MutableStyleState(null) }
    // Set equal to incoming state to link the two together
    styleState.playerState = state
    Box(
        modifier = modifier.styleable(styleState, Style {
            size(100.dp)
            border(2.dp, Color.Red)

        }, style, )) {

        ///..
    }
}
@Composable
fun StyleStateKeySample() {
    // Using the extension function to change the border color to green while playing
    val style = Style {
        borderColor(Color.Gray)
        playerPlaying {
            animate {
                borderColor(Color.Green)
            }
        }
        playerPaused {
            animate {
                borderColor(Color.Blue)
            }
        }
    }
    val styleState = remember { MutableStyleState(null) }
    styleState[playerStateKey] = PlayerState.Playing

    // Using the style in a composable that sets the state -> notice if you change the state parameter, the style changes. You can link this up to an ViewModel and change the state from there too.
    MediaPlayer(url = "https://example.com/media/video",
        style = style,
        state = PlayerState.Stopped)
}