ব্যবহারকারীর মিথস্ক্রিয়া পরিচালনা করা

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

চিত্র 1. বোতামগুলি যেগুলি সর্বদা সক্রিয় দেখায়, কোন প্রেস রিপল ছাড়াই।
চিত্র 2. প্রেস রিপল সহ বোতাম যা তাদের সক্রিয় অবস্থাকে সেই অনুযায়ী প্রতিফলিত করে।

কম্পোজ অঙ্গভঙ্গি ডকুমেন্টেশন কভার করে যে কীভাবে কম্পোজ উপাদান নিম্ন-স্তরের পয়েন্টার ইভেন্ট পরিচালনা করে, যেমন পয়েন্টার মুভ এবং ক্লিক। বাক্সের বাইরে, কম্পোজ সেই নিম্ন-স্তরের ইভেন্টগুলিকে উচ্চ-স্তরের মিথস্ক্রিয়াতে বিমূর্ত করে-উদাহরণস্বরূপ, পয়েন্টার ইভেন্টগুলির একটি সিরিজ একটি বোতাম প্রেস-এন্ড-রিলিজ পর্যন্ত যোগ করতে পারে। এই উচ্চ-স্তরের বিমূর্ততাগুলি বোঝা আপনাকে আপনার UI ব্যবহারকারীকে কীভাবে প্রতিক্রিয়া জানায় তা কাস্টমাইজ করতে সহায়তা করতে পারে। উদাহরণস্বরূপ, ব্যবহারকারী যখন এটির সাথে ইন্টারঅ্যাক্ট করে তখন আপনি একটি উপাদানের চেহারা কীভাবে পরিবর্তিত হয় তা আপনি কাস্টমাইজ করতে চাইতে পারেন, বা আপনি কেবল সেই ব্যবহারকারীর ক্রিয়াগুলির একটি লগ বজায় রাখতে চান৷ এই নথিটি আপনাকে স্ট্যান্ডার্ড UI উপাদানগুলি পরিবর্তন করতে বা আপনার নিজস্ব ডিজাইন করার জন্য প্রয়োজনীয় তথ্য দেয়৷

মিথস্ক্রিয়া

অনেক ক্ষেত্রে, আপনার কম্পোজ কম্পোনেন্ট ব্যবহারকারীর মিথস্ক্রিয়াকে কীভাবে ব্যাখ্যা করছে তা আপনার জানার দরকার নেই। উদাহরণস্বরূপ, ব্যবহারকারী বোতামটি ক্লিক করেছেন কিনা তা বোঝার জন্য Button Modifier.clickable এর উপর নির্ভর করে। আপনি যদি আপনার অ্যাপে একটি সাধারণ বোতাম যোগ করেন, তাহলে আপনি বোতামের onClick কোডটি সংজ্ঞায়িত করতে পারেন, এবং Modifier.clickable সেই কোডটি যথাযথ হলে চালায়৷ এর মানে আপনার জানার দরকার নেই যে ব্যবহারকারী স্ক্রীন ট্যাপ করেছেন নাকি কীবোর্ড দিয়ে বোতামটি নির্বাচন করেছেন; Modifier.clickable পরিসংখ্যান বের করে যে ব্যবহারকারী একটি ক্লিক করেছে এবং আপনার onClick কোড চালিয়ে প্রতিক্রিয়া জানায়।

যাইহোক, আপনি যদি ব্যবহারকারীর আচরণে আপনার UI উপাদানের প্রতিক্রিয়া কাস্টমাইজ করতে চান, তাহলে আপনাকে হুডের নীচে কী ঘটছে তা আরও জানতে হবে। এই বিভাগটি আপনাকে সেই তথ্যের কিছু দেয়।

যখন একজন ব্যবহারকারী একটি UI উপাদানের সাথে ইন্টারঅ্যাক্ট করে, তখন সিস্টেমটি বেশ কয়েকটি Interaction ইভেন্ট তৈরি করে তাদের আচরণের প্রতিনিধিত্ব করে। উদাহরণস্বরূপ, যদি একজন ব্যবহারকারী একটি বোতাম স্পর্শ করে, বোতামটি PressInteraction.Press তৈরি করে। ব্যবহারকারী যদি বোতামের ভিতরে তাদের আঙুল তোলে, তাহলে এটি একটি PressInteraction.Release তৈরি করে। রিলিজ, বোতামটিকে জানিয়ে দেয় যে ক্লিকটি শেষ হয়েছে। অন্যদিকে, ব্যবহারকারী যদি বোতামের বাইরে তাদের আঙুল টেনে আনে, তারপর তাদের আঙুল তুলে নেয়, বোতামটি PressInteraction.Cancel তৈরি করে। ক্যানসেল , বোঝাতে যে বোতামের চাপ বাতিল করা হয়েছে, সম্পূর্ণ হয়নি।

এই মিথস্ক্রিয়াগুলি অকল্পনীয় । অর্থাৎ, এই নিম্ন-স্তরের মিথস্ক্রিয়া ইভেন্টগুলি ব্যবহারকারীর ক্রিয়াকলাপের অর্থ, বা তাদের ক্রম ব্যাখ্যা করতে চায় না। এছাড়াও তারা ব্যাখ্যা করে না কোন ব্যবহারকারীর ক্রিয়াগুলি অন্যান্য ক্রিয়াকলাপের চেয়ে অগ্রাধিকার নিতে পারে৷

এই মিথস্ক্রিয়াগুলি সাধারণত জোড়ায় আসে, শুরু এবং শেষ সহ। দ্বিতীয় মিথস্ক্রিয়ায় প্রথমটির একটি রেফারেন্স রয়েছে। উদাহরণস্বরূপ, যদি একজন ব্যবহারকারী একটি বোতাম স্পর্শ করে এবং তার আঙুল তুলে নেয়, স্পর্শটি একটি PressInteraction.Press তৈরি করে PressInteraction.Release Release একটি press সম্পত্তি রয়েছে যা প্রাথমিক PressInteraction.Press চিহ্নিত করে।

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

মিথস্ক্রিয়া অবস্থা

আপনি নিজেই মিথস্ক্রিয়াগুলি ট্র্যাক করে আপনার উপাদানগুলির অন্তর্নির্মিত কার্যকারিতা প্রসারিত করতে চাইতে পারেন। উদাহরণস্বরূপ, সম্ভবত আপনি একটি বোতাম টিপলে রঙ পরিবর্তন করতে চান৷ মিথস্ক্রিয়াগুলি ট্র্যাক করার সবচেয়ে সহজ উপায় হল উপযুক্ত মিথস্ক্রিয়া অবস্থা পর্যবেক্ষণ করা। InteractionSource বেশ কয়েকটি পদ্ধতি অফার করে যা রাষ্ট্র হিসাবে বিভিন্ন মিথস্ক্রিয়া অবস্থা প্রকাশ করে। উদাহরণস্বরূপ, যদি আপনি দেখতে চান যে একটি নির্দিষ্ট বোতাম টিপছে কিনা, আপনি এটির InteractionSource.collectIsPressedAsState() পদ্ধতিতে কল করতে পারেন:

val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()

Button(
    onClick = { /* do something */ },
    interactionSource = interactionSource
) {
    Text(if (isPressed) "Pressed!" else "Not pressed")
}

collectIsPressedAsState() ছাড়াও, Compose এছাড়াও collectIsFocusedAsState() , collectIsDraggedAsState() , এবং collectIsHoveredAsState() প্রদান করে। এই পদ্ধতিগুলি আসলে নিম্ন-স্তরের InteractionSource API-এর উপরে তৈরি সুবিধার পদ্ধতি। কিছু ক্ষেত্রে, আপনি সেই নিম্ন-স্তরের ফাংশনগুলি সরাসরি ব্যবহার করতে চাইতে পারেন।

উদাহরণস্বরূপ, ধরুন আপনাকে একটি বোতাম টিপছে কিনা এবং এটি টেনে আনা হচ্ছে কিনা তাও জানতে হবে। আপনি যদি collectIsPressedAsState() এবং collectIsDraggedAsState() উভয়ই ব্যবহার করেন, তাহলে রচনা অনেক সদৃশ কাজ করে, এবং আপনি সঠিক ক্রমে সমস্ত মিথস্ক্রিয়া পাবেন এমন কোন গ্যারান্টি নেই। এই ধরনের পরিস্থিতিতে, আপনি InteractionSource এর সাথে সরাসরি কাজ করতে চাইতে পারেন। InteractionSource সাথে মিথস্ক্রিয়াগুলি ট্র্যাক করার বিষয়ে আরও তথ্যের জন্য, InteractionSource সাথে কাজ দেখুন।

নিম্নলিখিত বিভাগটি বর্ণনা করে কিভাবে যথাক্রমে InteractionSource এবং MutableInteractionSource এর সাথে মিথস্ক্রিয়া ব্যবহার এবং নির্গত করা যায়।

ব্যবহার এবং Interaction নির্গত

InteractionSource Interactions একটি রিড-ওনলি স্ট্রীম উপস্থাপন করে — এটি একটি InteractionSource Interaction নির্গত করা সম্ভব নয়। Interaction s নির্গত করতে, আপনাকে একটি MutableInteractionSource ব্যবহার করতে হবে, যা InteractionSource থেকে প্রসারিত হয়।

সংশোধক এবং উপাদানগুলি Interactions গ্রাস করতে, নির্গত করতে বা গ্রাস করতে এবং নির্গত করতে পারে। নিম্নলিখিত বিভাগগুলি বর্ণনা করে যে কীভাবে সংশোধক এবং উপাদান উভয় থেকে মিথস্ক্রিয়া গ্রহণ এবং নির্গত করা যায়।

কনজিউমিং মডিফায়ার উদাহরণ

একটি সংশোধকের জন্য যা ফোকাসড স্টেটের জন্য একটি সীমানা আঁকে, আপনাকে শুধুমাত্র Interactions পর্যবেক্ষণ করতে হবে, যাতে আপনি একটি InteractionSource গ্রহণ করতে পারেন:

fun Modifier.focusBorder(interactionSource: InteractionSource): Modifier {
    // ...
}

ফাংশন সিগনেচার থেকে এটা স্পষ্ট যে এই মডিফায়ারটি একজন ভোক্তা — এটি Interaction গ্রাস করতে পারে, কিন্তু সেগুলি নির্গত করতে পারে না।

সংশোধক উদাহরণ উত্পাদন

Modifier.hoverable এর মতো হোভার ইভেন্টগুলি পরিচালনা করে এমন একটি সংশোধকের জন্য, আপনাকে Interactions নির্গত করতে হবে এবং পরিবর্তে একটি পরামিতি হিসাবে একটি MutableInteractionSource গ্রহণ করতে হবে:

fun Modifier.hover(interactionSource: MutableInteractionSource, enabled: Boolean): Modifier {
    // ...
}

এই সংশোধকটি একটি প্রযোজক — এটি HoverInteractions নির্গত করার জন্য প্রদত্ত MutableInteractionSource ব্যবহার করতে পারে যখন এটি হোভার বা আনহোভার থাকে।

উপাদানগুলি তৈরি করুন যা ব্যবহার করে এবং উত্পাদন করে

উচ্চ-স্তরের উপাদান যেমন একটি উপাদান Button প্রযোজক এবং ভোক্তা উভয় হিসাবে কাজ করে। তারা ইনপুট এবং ফোকাস ইভেন্টগুলি পরিচালনা করে এবং এই ইভেন্টগুলির প্রতিক্রিয়া হিসাবে তাদের চেহারা পরিবর্তন করে, যেমন একটি লহর দেখানো বা তাদের উচ্চতা অ্যানিমেট করা। ফলস্বরূপ, তারা একটি পরামিতি হিসাবে সরাসরি MutableInteractionSource প্রকাশ করে, যাতে আপনি আপনার নিজের মনে রাখা উদাহরণ প্রদান করতে পারেন:

@Composable
fun Button(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,

    // exposes MutableInteractionSource as a parameter
    interactionSource: MutableInteractionSource? = null,

    elevation: ButtonElevation? = ButtonDefaults.elevatedButtonElevation(),
    shape: Shape = MaterialTheme.shapes.small,
    border: BorderStroke? = null,
    colors: ButtonColors = ButtonDefaults.buttonColors(),
    contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
    content: @Composable RowScope.() -> Unit
) { /* content() */ }

এটি কম্পোনেন্ট থেকে MutableInteractionSource উত্তোলন করতে এবং কম্পোনেন্ট দ্বারা উত্পাদিত সমস্ত Interaction পর্যবেক্ষণ করতে দেয়। আপনি এই উপাদানটির চেহারা বা আপনার UI-তে অন্য কোনো উপাদান নিয়ন্ত্রণ করতে এটি ব্যবহার করতে পারেন।

আপনি যদি আপনার নিজস্ব ইন্টারেক্টিভ উচ্চ স্তরের উপাদানগুলি তৈরি করেন, তাহলে আমরা সুপারিশ করব যে আপনি এইভাবে একটি পরামিতি হিসাবে MutableInteractionSource প্রকাশ করুন ৷ রাষ্ট্র উত্তোলনের সর্বোত্তম অনুশীলনগুলি অনুসরণ করার পাশাপাশি, এটি একটি উপাদানের চাক্ষুষ অবস্থাকে পড়া এবং নিয়ন্ত্রণ করা সহজ করে তোলে যেভাবে অন্য কোনও ধরণের রাষ্ট্র (যেমন সক্ষম রাষ্ট্র) পড়া এবং নিয়ন্ত্রণ করা যায়।

কম্পোজ একটি স্তরযুক্ত স্থাপত্য পদ্ধতি অনুসরণ করে, তাই উচ্চ-স্তরের উপাদান উপাদানগুলি ভিত্তিগত বিল্ডিং ব্লকগুলির উপরে তৈরি করা হয় যা তরঙ্গ এবং অন্যান্য ভিজ্যুয়াল প্রভাবগুলি নিয়ন্ত্রণ করতে প্রয়োজনীয় Interaction তৈরি করে। ফাউন্ডেশন লাইব্রেরি উচ্চ-স্তরের ইন্টারঅ্যাকশন মডিফায়ার প্রদান করে যেমন Modifier.hoverable , Modifier.focusable এবং Modifier.draggable

হোভার ইভেন্টগুলিতে সাড়া দেয় এমন একটি উপাদান তৈরি করতে, আপনি কেবল Modifier.hoverable ব্যবহার করতে পারেন এবং একটি পরামিতি হিসাবে একটি MutableInteractionSource পাস করতে পারেন। যখনই কম্পোনেন্টটি হোভার করা হয়, তখন এটি HoverInteraction s নির্গত করে এবং আপনি উপাদানটি কীভাবে প্রদর্শিত হবে তা পরিবর্তন করতে এটি ব্যবহার করতে পারেন।

// This InteractionSource will emit hover interactions
val interactionSource = remember { MutableInteractionSource() }

Box(
    Modifier
        .size(100.dp)
        .hoverable(interactionSource = interactionSource),
    contentAlignment = Alignment.Center
) {
    Text("Hello!")
}

এই উপাদানটিকে ফোকাসযোগ্য করার জন্য, আপনি Modifier.focusable যোগ করতে পারেন এবং একই MutableInteractionSource একটি প্যারামিটার হিসাবে পাস করতে পারেন। এখন, HoverInteraction.Enter/Exit এবং FocusInteraction.Focus/Unfocus উভয়ই একই MutableInteractionSource এর মাধ্যমে নির্গত হয়, এবং আপনি একই জায়গায় উভয় ধরনের ইন্টারঅ্যাকশনের জন্য চেহারা কাস্টমাইজ করতে পারেন:

// This InteractionSource will emit hover and focus interactions
val interactionSource = remember { MutableInteractionSource() }

Box(
    Modifier
        .size(100.dp)
        .hoverable(interactionSource = interactionSource)
        .focusable(interactionSource = interactionSource),
    contentAlignment = Alignment.Center
) {
    Text("Hello!")
}

Modifier.clickable হল hoverable এবং focusable চেয়েও উচ্চ স্তরের বিমূর্ততা — একটি কম্পোনেন্টকে ক্লিক করার জন্য, এটি অন্তর্নিহিতভাবে ঘোরানো যায়, এবং যে উপাদানগুলিতে ক্লিক করা যায় সেগুলিও ফোকাসযোগ্য হওয়া উচিত। নিম্ন স্তরের APIগুলিকে একত্রিত করার প্রয়োজন ছাড়াই আপনি একটি উপাদান তৈরি করতে Modifier.clickable ব্যবহার করতে পারেন যা হোভার, ফোকাস এবং প্রেস ইন্টারঅ্যাকশন পরিচালনা করে। আপনি যদি আপনার উপাদানটিকেও ক্লিকযোগ্য করে তুলতে চান তবে আপনি একটি clickable দিয়ে hoverable এবং focusable প্রতিস্থাপন করতে পারেন:

// This InteractionSource will emit hover, focus, and press interactions
val interactionSource = remember { MutableInteractionSource() }
Box(
    Modifier
        .size(100.dp)
        .clickable(
            onClick = {},
            interactionSource = interactionSource,

            // Also show a ripple effect
            indication = ripple()
        ),
    contentAlignment = Alignment.Center
) {
    Text("Hello!")
}

InteractionSource সাথে কাজ করুন

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

val interactionSource = remember { MutableInteractionSource() }
val interactions = remember { mutableStateListOf<Interaction>() }

LaunchedEffect(interactionSource) {
    interactionSource.interactions.collect { interaction ->
        when (interaction) {
            is PressInteraction.Press -> {
                interactions.add(interaction)
            }
            is DragInteraction.Start -> {
                interactions.add(interaction)
            }
        }
    }
}

কিন্তু নতুন ইন্টারঅ্যাকশনগুলি যোগ করার পাশাপাশি, আপনাকে মিথস্ক্রিয়াগুলি শেষ হয়ে গেলে (উদাহরণস্বরূপ, যখন ব্যবহারকারী তাদের আঙুলটি কম্পোনেন্ট থেকে ফিরিয়ে দেয়) সরিয়ে ফেলতে হবে। এটি করা সহজ, যেহেতু শেষ মিথস্ক্রিয়া সবসময় সংশ্লিষ্ট শুরু ইন্টারঅ্যাকশনের একটি রেফারেন্স বহন করে। এই কোডটি দেখায় যে আপনি কীভাবে শেষ হওয়া ইন্টারঅ্যাকশনগুলি সরিয়ে ফেলবেন:

val interactionSource = remember { MutableInteractionSource() }
val interactions = remember { mutableStateListOf<Interaction>() }

LaunchedEffect(interactionSource) {
    interactionSource.interactions.collect { interaction ->
        when (interaction) {
            is PressInteraction.Press -> {
                interactions.add(interaction)
            }
            is PressInteraction.Release -> {
                interactions.remove(interaction.press)
            }
            is PressInteraction.Cancel -> {
                interactions.remove(interaction.press)
            }
            is DragInteraction.Start -> {
                interactions.add(interaction)
            }
            is DragInteraction.Stop -> {
                interactions.remove(interaction.start)
            }
            is DragInteraction.Cancel -> {
                interactions.remove(interaction.start)
            }
        }
    }
}

এখন, যদি আপনি জানতে চান যে উপাদানটি বর্তমানে চাপা বা টেনে আনা হচ্ছে, আপনাকে যা করতে হবে তা হল interactions খালি কিনা তা পরীক্ষা করে দেখুন:

val isPressedOrDragged = interactions.isNotEmpty()

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

val lastInteraction = when (interactions.lastOrNull()) {
    is DragInteraction.Start -> "Dragged"
    is PressInteraction.Press -> "Pressed"
    else -> "No state"
}

যেহেতু সমস্ত Interaction একই কাঠামো অনুসরণ করে, বিভিন্ন ধরনের ব্যবহারকারীর মিথস্ক্রিয়াগুলির সাথে কাজ করার সময় কোডে খুব বেশি পার্থক্য নেই — সামগ্রিক প্যাটার্ন একই।

মনে রাখবেন যে এই বিভাগে পূর্ববর্তী উদাহরণগুলি State ব্যবহার করে মিথস্ক্রিয়াগুলির Flow উপস্থাপন করে — এটি আপডেট করা মানগুলি পর্যবেক্ষণ করা সহজ করে তোলে, কারণ স্টেট মান পড়ার ফলে স্বয়ংক্রিয়ভাবে পুনর্গঠন ঘটবে। যাইহোক, কম্পোজিশন ব্যাচড প্রি-ফ্রেম। এর মানে হল যে যদি রাষ্ট্র পরিবর্তিত হয়, এবং তারপরে একই ফ্রেমের মধ্যে পরিবর্তিত হয়, রাষ্ট্র পর্যবেক্ষণকারী উপাদানগুলি পরিবর্তন দেখতে পাবে না।

মিথস্ক্রিয়াগুলির জন্য এটি গুরুত্বপূর্ণ, কারণ মিথস্ক্রিয়াগুলি একই ফ্রেমের মধ্যে নিয়মিতভাবে শুরু এবং শেষ হতে পারে। উদাহরণস্বরূপ, Button সহ পূর্ববর্তী উদাহরণ ব্যবহার করে:

val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()

Button(onClick = { /* do something */ }, interactionSource = interactionSource) {
    Text(if (isPressed) "Pressed!" else "Not pressed")
}

যদি একটি প্রেস একই ফ্রেমের মধ্যে শুরু হয় এবং শেষ হয় তবে পাঠ্যটি কখনই "প্রেসড!" হিসাবে প্রদর্শিত হবে না। বেশিরভাগ ক্ষেত্রে, এটি একটি সমস্যা নয় — এত অল্প সময়ের জন্য একটি ভিজ্যুয়াল ইফেক্ট দেখানোর ফলে ঝিকিমিকি হবে, এবং ব্যবহারকারীর কাছে খুব বেশি লক্ষণীয় হবে না। কিছু ক্ষেত্রে, যেমন একটি রিপল ইফেক্ট বা অনুরূপ অ্যানিমেশন দেখানোর জন্য, আপনি বোতামটি আর না চাপলে অবিলম্বে বন্ধ করার পরিবর্তে কমপক্ষে একটি ন্যূনতম সময়ের জন্য প্রভাব দেখাতে চাইতে পারেন। এটি করার জন্য, আপনি একটি রাজ্যে লেখার পরিবর্তে সরাসরি সংগ্রহ ল্যাম্বডা থেকে অ্যানিমেশনগুলি শুরু এবং বন্ধ করতে পারেন। অ্যানিমেটেড বর্ডার বিভাগে একটি উন্নত Indication তৈরি করুন- এ এই প্যাটার্নের একটি উদাহরণ রয়েছে।

উদাহরণ: কাস্টম ইন্টারঅ্যাকশন হ্যান্ডলিং সহ উপাদান তৈরি করুন

ইনপুটের কাস্টম প্রতিক্রিয়া দিয়ে আপনি কীভাবে উপাদানগুলি তৈরি করতে পারেন তা দেখতে, এখানে একটি সংশোধিত বোতামের একটি উদাহরণ রয়েছে। এই ক্ষেত্রে, ধরুন আপনি একটি বোতাম চান যা তার চেহারা পরিবর্তন করে প্রেসে সাড়া দেয়:

একটি বোতামের অ্যানিমেশন যা ক্লিক করার সময় গতিশীলভাবে একটি মুদির কার্ট আইকন যোগ করে
চিত্র 3. একটি বোতাম যা ক্লিক করার সময় গতিশীলভাবে একটি আইকন যোগ করে।

এটি করার জন্য, Button উপর ভিত্তি করে একটি কাস্টম কম্পোজযোগ্য তৈরি করুন এবং আইকনটি আঁকতে একটি অতিরিক্ত icon প্যারামিটার নিতে হবে (এই ক্ষেত্রে, একটি শপিং কার্ট)। ব্যবহারকারী বোতামের উপর ঘোরাফেরা করছে কিনা তা ট্র্যাক করতে আপনি collectIsPressedAsState() কল করুন; তারা যখন, আপনি আইকন যোগ করুন. কোডটি দেখতে কেমন তা এখানে:

@Composable
fun PressIconButton(
    onClick: () -> Unit,
    icon: @Composable () -> Unit,
    text: @Composable () -> Unit,
    modifier: Modifier = Modifier,
    interactionSource: MutableInteractionSource? = null
) {
    val isPressed = interactionSource?.collectIsPressedAsState()?.value ?: false

    Button(
        onClick = onClick,
        modifier = modifier,
        interactionSource = interactionSource
    ) {
        AnimatedVisibility(visible = isPressed) {
            if (isPressed) {
                Row {
                    icon()
                    Spacer(Modifier.size(ButtonDefaults.IconSpacing))
                }
            }
        }
        text()
    }
}

এবং সেই নতুন কম্পোজেবল ব্যবহার করতে দেখতে কেমন লাগে তা এখানে:

PressIconButton(
    onClick = {},
    icon = { Icon(Icons.Filled.ShoppingCart, contentDescription = null) },
    text = { Text("Add to cart") }
)

যেহেতু এই নতুন PressIconButton বিদ্যমান উপাদান Button উপরে তৈরি করা হয়েছে, এটি সমস্ত সাধারণ উপায়ে ব্যবহারকারীর মিথস্ক্রিয়ায় প্রতিক্রিয়া দেখায়। ব্যবহারকারী যখন বোতামটি চাপেন, তখন এটি একটি সাধারণ উপাদান Button মতো তার অস্বচ্ছতা সামান্য পরিবর্তন করে।

Indication সহ একটি পুনরায় ব্যবহারযোগ্য কাস্টম প্রভাব তৈরি করুন এবং প্রয়োগ করুন

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

আপনি যদি এই ধরনের ডিজাইন সিস্টেম তৈরি করেন, একটি উপাদান কাস্টমাইজ করা এবং অন্যান্য উপাদানগুলির জন্য এই কাস্টমাইজেশন পুনরায় ব্যবহার করা নিম্নলিখিত কারণে কঠিন হতে পারে:

  • ডিজাইন সিস্টেমের প্রতিটি উপাদান একই বয়লারপ্লেট প্রয়োজন
  • নতুন নির্মিত উপাদান এবং কাস্টম ক্লিকযোগ্য উপাদানগুলিতে এই প্রভাব প্রয়োগ করতে ভুলে যাওয়া সহজ
  • অন্যান্য প্রভাবের সাথে কাস্টম প্রভাব একত্রিত করা কঠিন হতে পারে

এই সমস্যাগুলি এড়াতে এবং সহজেই আপনার সিস্টেম জুড়ে একটি কাস্টম উপাদান স্কেল করতে, আপনি Indication ব্যবহার করতে পারেন। Indication একটি পুনঃব্যবহারযোগ্য ভিজ্যুয়াল এফেক্টকে উপস্থাপন করে যা একটি অ্যাপ্লিকেশন বা ডিজাইন সিস্টেমের উপাদান জুড়ে প্রয়োগ করা যেতে পারে। Indication দুটি ভাগে বিভক্ত:

  • IndicationNodeFactory : একটি ফ্যাক্টরি যা Modifier.Node তৈরি করে। নোড ইনস্ট্যান্স যা একটি কম্পোনেন্টের জন্য ভিজ্যুয়াল ইফেক্ট রেন্ডার করে। সহজ বাস্তবায়নের জন্য যা উপাদান জুড়ে পরিবর্তন হয় না, এটি একটি সিঙ্গলটন (অবজেক্ট) হতে পারে এবং পুরো অ্যাপ্লিকেশন জুড়ে পুনরায় ব্যবহার করা যেতে পারে।

    এই উদাহরণ রাষ্ট্রীয় বা রাষ্ট্রহীন হতে পারে. যেহেতু এগুলি প্রতি কম্পোনেন্টে তৈরি করা হয়েছে, তাই অন্য যেকোন Modifier.Node এর মতো একটি নির্দিষ্ট কম্পোনেন্টের অভ্যন্তরে তারা কীভাবে উপস্থিত হয় বা আচরণ করে তা পরিবর্তন করতে তারা CompositionLocal থেকে মান পুনরুদ্ধার করতে পারে।

  • Modifier.indication : একটি সংশোধক যা একটি উপাদানের জন্য Indication আঁকে। Modifier.clickable এবং অন্যান্য উচ্চ স্তরের মিথস্ক্রিয়া সংশোধক সরাসরি একটি ইঙ্গিত পরামিতি গ্রহণ করে, তাই তারা শুধুমাত্র Interaction নির্গত করে না, তবে তারা যে Interaction নির্গত করে তার জন্য ভিজ্যুয়াল প্রভাবও আঁকতে পারে। সুতরাং, সাধারণ ক্ষেত্রে, আপনি Modifier.indication ছাড়াই শুধুমাত্র Modifier.clickable ব্যবহার করতে পারেন।

একটি Indication দিয়ে প্রভাব প্রতিস্থাপন

এই বিভাগটি বর্ণনা করে যে কীভাবে একটি নির্দিষ্ট বোতামে প্রয়োগ করা একটি ম্যানুয়াল স্কেল প্রভাবকে একটি ইঙ্গিত সমতুল্য দিয়ে প্রতিস্থাপন করতে হয় যা একাধিক উপাদান জুড়ে পুনরায় ব্যবহার করা যেতে পারে।

নিচের কোডটি একটি বোতাম তৈরি করে যা প্রেস করলে নিচের দিকে স্কেল হয়:

val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
val scale by animateFloatAsState(targetValue = if (isPressed) 0.9f else 1f, label = "scale")

Button(
    modifier = Modifier.scale(scale),
    onClick = { },
    interactionSource = interactionSource
) {
    Text(if (isPressed) "Pressed!" else "Not pressed")
}

উপরের স্নিপেটে থাকা স্কেল প্রভাবটিকে একটি Indication রূপান্তর করতে, এই পদক্ষেপগুলি অনুসরণ করুন:

  1. স্কেল প্রভাব প্রয়োগের জন্য দায়ী Modifier.Node তৈরি করুন । সংযুক্ত করা হলে, নোডটি পূর্ববর্তী উদাহরণগুলির মতো মিথস্ক্রিয়া উত্স পর্যবেক্ষণ করে। এখানে শুধুমাত্র পার্থক্য হল যে এটি ইনকামিং ইন্টারঅ্যাকশনকে স্টেটে রূপান্তর করার পরিবর্তে সরাসরি অ্যানিমেশন চালু করে।

    নোডটিকে DrawModifierNode প্রয়োগ করতে হবে যাতে এটি ContentDrawScope#draw() ওভাররাইড করতে পারে এবং কম্পোজের অন্য যেকোন গ্রাফিক্স API-এর মতো একই অঙ্কন কমান্ড ব্যবহার করে একটি স্কেল প্রভাব রেন্ডার করতে পারে।

    ContentDrawScope রিসিভার থেকে উপলব্ধ drawContent() কল করা প্রকৃত উপাদানটি আঁকবে যেটিতে Indication প্রয়োগ করা উচিত, তাই আপনাকে শুধুমাত্র একটি স্কেল রূপান্তরের মধ্যে এই ফাংশনটিকে কল করতে হবে। নিশ্চিত করুন যে আপনার Indication বাস্তবায়ন সর্বদা drawContent() কোনো সময়ে কল করে; অন্যথায়, আপনি যে উপাদানটিতে Indication প্রয়োগ করছেন তা আঁকা হবে না।

    private class ScaleNode(private val interactionSource: InteractionSource) :
        Modifier.Node(), DrawModifierNode {
    
        var currentPressPosition: Offset = Offset.Zero
        val animatedScalePercent = Animatable(1f)
    
        private suspend fun animateToPressed(pressPosition: Offset) {
            currentPressPosition = pressPosition
            animatedScalePercent.animateTo(0.9f, spring())
        }
    
        private suspend fun animateToResting() {
            animatedScalePercent.animateTo(1f, spring())
        }
    
        override fun onAttach() {
            coroutineScope.launch {
                interactionSource.interactions.collectLatest { interaction ->
                    when (interaction) {
                        is PressInteraction.Press -> animateToPressed(interaction.pressPosition)
                        is PressInteraction.Release -> animateToResting()
                        is PressInteraction.Cancel -> animateToResting()
                    }
                }
            }
        }
    
        override fun ContentDrawScope.draw() {
            scale(
                scale = animatedScalePercent.value,
                pivot = currentPressPosition
            ) {
                this@draw.drawContent()
            }
        }
    }

  2. IndicationNodeFactory তৈরি করুন । এটির একমাত্র দায়িত্ব একটি প্রদত্ত মিথস্ক্রিয়া উত্সের জন্য একটি নতুন নোড উদাহরণ তৈরি করা। যেহেতু ইঙ্গিত কনফিগার করার জন্য কোন পরামিতি নেই, কারখানাটি একটি বস্তু হতে পারে:

    object ScaleIndication : IndicationNodeFactory {
        override fun create(interactionSource: InteractionSource): DelegatableNode {
            return ScaleNode(interactionSource)
        }
    
        override fun equals(other: Any?): Boolean = other === ScaleIndication
        override fun hashCode() = 100
    }

  3. Modifier.clickable অভ্যন্তরীণভাবে Modifier.indication ব্যবহার করে, তাই ScaleIndication এর সাথে একটি ক্লিকযোগ্য উপাদান তৈরি করতে, আপনাকে যা করতে হবে তা হল clickable জন্য একটি প্যারামিটার হিসাবে Indication প্রদান করা :

    Box(
        modifier = Modifier
            .size(100.dp)
            .clickable(
                onClick = {},
                indication = ScaleIndication,
                interactionSource = null
            )
            .background(Color.Blue),
        contentAlignment = Alignment.Center
    ) {
        Text("Hello!", color = Color.White)
    }

    এটি একটি কাস্টম Indication ব্যবহার করে উচ্চ স্তরের, পুনঃব্যবহারযোগ্য উপাদানগুলি তৈরি করা সহজ করে তোলে — একটি বোতাম এর মতো দেখতে পারে:

    @Composable
    fun ScaleButton(
        onClick: () -> Unit,
        modifier: Modifier = Modifier,
        enabled: Boolean = true,
        interactionSource: MutableInteractionSource? = null,
        shape: Shape = CircleShape,
        content: @Composable RowScope.() -> Unit
    ) {
        Row(
            modifier = modifier
                .defaultMinSize(minWidth = 76.dp, minHeight = 48.dp)
                .clickable(
                    enabled = enabled,
                    indication = ScaleIndication,
                    interactionSource = interactionSource,
                    onClick = onClick
                )
                .border(width = 2.dp, color = Color.Blue, shape = shape)
                .padding(horizontal = 16.dp, vertical = 8.dp),
            horizontalArrangement = Arrangement.Center,
            verticalAlignment = Alignment.CenterVertically,
            content = content
        )
    }

তারপরে আপনি নিম্নলিখিত উপায়ে বোতামটি ব্যবহার করতে পারেন:

ScaleButton(onClick = {}) {
    Icon(Icons.Filled.ShoppingCart, "")
    Spacer(Modifier.padding(10.dp))
    Text(text = "Add to cart!")
}

একটি মুদির কার্ট আইকন সহ একটি বোতামের একটি অ্যানিমেশন যা চাপলে ছোট হয়ে যায়৷
চিত্র 4. একটি কাস্টম Indication সহ নির্মিত একটি বোতাম।

অ্যানিমেটেড সীমানা সহ একটি উন্নত Indication তৈরি করুন

Indication শুধুমাত্র রূপান্তর প্রভাবে সীমাবদ্ধ নয়, যেমন একটি উপাদান স্কেলিং। কারণ IndicationNodeFactory একটি Modifier.Node প্রদান করে, আপনি অন্যান্য ড্রয়িং API-এর মতো বিষয়বস্তুর উপরে বা নীচে যেকোনো ধরনের প্রভাব আঁকতে পারেন। উদাহরণস্বরূপ, আপনি উপাদানটির চারপাশে একটি অ্যানিমেটেড সীমানা আঁকতে পারেন এবং যখন এটি চাপানো হয় তখন উপাদানটির উপরে একটি ওভারলে আঁকতে পারেন:

প্রেসে অভিনব রংধনু প্রভাব সহ একটি বোতাম
চিত্র 5. Indication দিয়ে আঁকা একটি অ্যানিমেটেড বর্ডার ইফেক্ট।

এখানে Indication বাস্তবায়ন পূর্ববর্তী উদাহরণের অনুরূপ — এটি শুধুমাত্র কিছু পরামিতি সহ একটি নোড তৈরি করে। যেহেতু অ্যানিমেটেড সীমানাটি Indication ব্যবহৃত উপাদানটির আকৃতি এবং সীমানার উপর নির্ভর করে, তাই Indication বাস্তবায়নের জন্য আকৃতি এবং সীমানা প্রস্থকে প্যারামিটার হিসাবে প্রদান করা প্রয়োজন:

data class NeonIndication(private val shape: Shape, private val borderWidth: Dp) : IndicationNodeFactory {

    override fun create(interactionSource: InteractionSource): DelegatableNode {
        return NeonNode(
            shape,
            // Double the border size for a stronger press effect
            borderWidth * 2,
            interactionSource
        )
    }
}

Modifier.Node ইমপ্লিমেন্টেশনও ধারণাগতভাবে একই, এমনকি যদি অঙ্কন কোড আরও জটিল হয়। পূর্বের মত, এটি সংযুক্ত থাকাকালীন InteractionSource পর্যবেক্ষণ করে, অ্যানিমেশন চালু করে এবং বিষয়বস্তুর উপরে প্রভাব আঁকতে DrawModifierNode প্রয়োগ করে:

private class NeonNode(
    private val shape: Shape,
    private val borderWidth: Dp,
    private val interactionSource: InteractionSource
) : Modifier.Node(), DrawModifierNode {
    var currentPressPosition: Offset = Offset.Zero
    val animatedProgress = Animatable(0f)
    val animatedPressAlpha = Animatable(1f)

    var pressedAnimation: Job? = null
    var restingAnimation: Job? = null

    private suspend fun animateToPressed(pressPosition: Offset) {
        // Finish any existing animations, in case of a new press while we are still showing
        // an animation for a previous one
        restingAnimation?.cancel()
        pressedAnimation?.cancel()
        pressedAnimation = coroutineScope.launch {
            currentPressPosition = pressPosition
            animatedPressAlpha.snapTo(1f)
            animatedProgress.snapTo(0f)
            animatedProgress.animateTo(1f, tween(450))
        }
    }

    private fun animateToResting() {
        restingAnimation = coroutineScope.launch {
            // Wait for the existing press animation to finish if it is still ongoing
            pressedAnimation?.join()
            animatedPressAlpha.animateTo(0f, tween(250))
            animatedProgress.snapTo(0f)
        }
    }

    override fun onAttach() {
        coroutineScope.launch {
            interactionSource.interactions.collect { interaction ->
                when (interaction) {
                    is PressInteraction.Press -> animateToPressed(interaction.pressPosition)
                    is PressInteraction.Release -> animateToResting()
                    is PressInteraction.Cancel -> animateToResting()
                }
            }
        }
    }

    override fun ContentDrawScope.draw() {
        val (startPosition, endPosition) = calculateGradientStartAndEndFromPressPosition(
            currentPressPosition, size
        )
        val brush = animateBrush(
            startPosition = startPosition,
            endPosition = endPosition,
            progress = animatedProgress.value
        )
        val alpha = animatedPressAlpha.value

        drawContent()

        val outline = shape.createOutline(size, layoutDirection, this)
        // Draw overlay on top of content
        drawOutline(
            outline = outline,
            brush = brush,
            alpha = alpha * 0.1f
        )
        // Draw border on top of overlay
        drawOutline(
            outline = outline,
            brush = brush,
            alpha = alpha,
            style = Stroke(width = borderWidth.toPx())
        )
    }

    /**
     * Calculates a gradient start / end where start is the point on the bounding rectangle of
     * size [size] that intercepts with the line drawn from the center to [pressPosition],
     * and end is the intercept on the opposite end of that line.
     */
    private fun calculateGradientStartAndEndFromPressPosition(
        pressPosition: Offset,
        size: Size
    ): Pair<Offset, Offset> {
        // Convert to offset from the center
        val offset = pressPosition - size.center
        // y = mx + c, c is 0, so just test for x and y to see where the intercept is
        val gradient = offset.y / offset.x
        // We are starting from the center, so halve the width and height - convert the sign
        // to match the offset
        val width = (size.width / 2f) * sign(offset.x)
        val height = (size.height / 2f) * sign(offset.y)
        val x = height / gradient
        val y = gradient * width

        // Figure out which intercept lies within bounds
        val intercept = if (abs(y) <= abs(height)) {
            Offset(width, y)
        } else {
            Offset(x, height)
        }

        // Convert back to offsets from 0,0
        val start = intercept + size.center
        val end = Offset(size.width - start.x, size.height - start.y)
        return start to end
    }

    private fun animateBrush(
        startPosition: Offset,
        endPosition: Offset,
        progress: Float
    ): Brush {
        if (progress == 0f) return TransparentBrush

        // This is *expensive* - we are doing a lot of allocations on each animation frame. To
        // recreate a similar effect in a performant way, it would be better to create one large
        // gradient and translate it on each frame, instead of creating a whole new gradient
        // and shader. The current approach will be janky!
        val colorStops = buildList {
            when {
                progress < 1 / 6f -> {
                    val adjustedProgress = progress * 6f
                    add(0f to Blue)
                    add(adjustedProgress to Color.Transparent)
                }
                progress < 2 / 6f -> {
                    val adjustedProgress = (progress - 1 / 6f) * 6f
                    add(0f to Purple)
                    add(adjustedProgress * MaxBlueStop to Blue)
                    add(adjustedProgress to Blue)
                    add(1f to Color.Transparent)
                }
                progress < 3 / 6f -> {
                    val adjustedProgress = (progress - 2 / 6f) * 6f
                    add(0f to Pink)
                    add(adjustedProgress * MaxPurpleStop to Purple)
                    add(MaxBlueStop to Blue)
                    add(1f to Blue)
                }
                progress < 4 / 6f -> {
                    val adjustedProgress = (progress - 3 / 6f) * 6f
                    add(0f to Orange)
                    add(adjustedProgress * MaxPinkStop to Pink)
                    add(MaxPurpleStop to Purple)
                    add(MaxBlueStop to Blue)
                    add(1f to Blue)
                }
                progress < 5 / 6f -> {
                    val adjustedProgress = (progress - 4 / 6f) * 6f
                    add(0f to Yellow)
                    add(adjustedProgress * MaxOrangeStop to Orange)
                    add(MaxPinkStop to Pink)
                    add(MaxPurpleStop to Purple)
                    add(MaxBlueStop to Blue)
                    add(1f to Blue)
                }
                else -> {
                    val adjustedProgress = (progress - 5 / 6f) * 6f
                    add(0f to Yellow)
                    add(adjustedProgress * MaxYellowStop to Yellow)
                    add(MaxOrangeStop to Orange)
                    add(MaxPinkStop to Pink)
                    add(MaxPurpleStop to Purple)
                    add(MaxBlueStop to Blue)
                    add(1f to Blue)
                }
            }
        }

        return linearGradient(
            colorStops = colorStops.toTypedArray(),
            start = startPosition,
            end = endPosition
        )
    }

    companion object {
        val TransparentBrush = SolidColor(Color.Transparent)
        val Blue = Color(0xFF30C0D8)
        val Purple = Color(0xFF7848A8)
        val Pink = Color(0xFFF03078)
        val Orange = Color(0xFFF07800)
        val Yellow = Color(0xFFF0D800)
        const val MaxYellowStop = 0.16f
        const val MaxOrangeStop = 0.33f
        const val MaxPinkStop = 0.5f
        const val MaxPurpleStop = 0.67f
        const val MaxBlueStop = 0.83f
    }
}

এখানে প্রধান পার্থক্য হল animateToResting() ফাংশন সহ অ্যানিমেশনের জন্য এখন একটি ন্যূনতম সময়কাল রয়েছে, তাই প্রেস অবিলম্বে প্রকাশিত হলেও প্রেস অ্যানিমেশন চলতে থাকবে। animateToPressed এর শুরুতে একাধিক দ্রুত প্রেসের জন্য হ্যান্ডলিং রয়েছে — যদি একটি প্রেস বিদ্যমান প্রেস বা বিশ্রামের অ্যানিমেশনের সময় ঘটে, তবে পূর্ববর্তী অ্যানিমেশনটি বাতিল হয়ে যায় এবং প্রেস অ্যানিমেশন শুরু থেকে শুরু হয়। একাধিক সমসাময়িক প্রভাব সমর্থন করতে (যেমন লহরের সাথে, যেখানে একটি নতুন রিপল অ্যানিমেশন অন্যান্য লহরের উপরে আঁকবে), আপনি বিদ্যমান অ্যানিমেশনগুলি বাতিল করে নতুনগুলি শুরু করার পরিবর্তে একটি তালিকায় অ্যানিমেশনগুলি ট্র্যাক করতে পারেন।

{% শব্দার্থে %} {% endverbatim %} {% শব্দার্থে %} {% endverbatim %}