জেসচার বোঝা

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

সংজ্ঞা

এই পৃষ্ঠায় বিভিন্ন ধারণা বুঝতে, আপনাকে কিছু পরিভাষা বুঝতে হবে:

  • পয়েন্টার: আপনার অ্যাপ্লিকেশনের সাথে ইন্টার‍্যাক্ট করার জন্য ব্যবহার করতে পারেন এমন কোনও ফিজিক্যাল অবজেক্ট। মোবাইল ডিভাইসের ক্ষেত্রে, সবচেয়ে সাধারণ পয়েন্টার হল আপনার আঙুল যা টাচস্ক্রিনের সাথে ইন্টার‍্যাক্ট করে। অথবা, আঙুলের পরিবর্তে স্টাইলাস ব্যবহার করতে পারেন। বড় স্ক্রিনের ক্ষেত্রে, ডিসপ্লের সাথে পরোক্ষভাবে ইন্টার‍্যাক্ট করতে আপনি মাউস বা ট্র্যাকপ্যাড ব্যবহার করতে পারবেন। ইনপুট ডিভাইসকে অবশ্যই কোনও কোঅর্ডিনেটে "পয়েন্ট" করতে পারতে হবে, তবেই সেটিকে পয়েন্টার হিসেবে বিবেচনা করা হবে। যেমন, কীবোর্ডকে পয়েন্টার হিসেবে বিবেচনা করা যায় না। Compose-এ, PointerType ব্যবহার করে পয়েন্টার পরিবর্তনের মধ্যে পয়েন্টারের ধরন অন্তর্ভুক্ত করা হয়।
  • পয়েন্টার ইভেন্ট: কোনও নির্দিষ্ট সময়ে অ্যাপ্লিকেশন সহ এক বা একাধিক পয়েন্টারের লো-লেভেল ইন্টার‍্যাকশন বর্ণনা করে। স্ক্রিনে আঙ্গুল রাখা বা মাউস টেনে আনার মতো যেকোনও পয়েন্টার ইন্টার‍্যাকশন একটি ইভেন্ট ট্রিগার করবে। Compose-এ, এই ধরনের ইভেন্টের জন্য প্রাসঙ্গিক সব তথ্য PointerEvent ক্লাসে থাকে।
  • জেসচার: পয়েন্টার ইভেন্টের একটি ক্রম যা একটি অ্যাকশন হিসেবে ব্যাখ্যা করা যায়। যেমন, ট্যাপ জেসচারকে down ইভেন্টের একটি সিকোয়েন্স হিসেবে বিবেচনা করা যেতে পারে, যার পরে একটি up ইভেন্ট হয়। অনেক অ্যাপে সাধারণ জেসচার ব্যবহার করা হয়, যেমন ট্যাপ, ড্র্যাগ বা ট্রান্সফর্ম করা। তবে, প্রয়োজন হলে আপনি নিজের কাস্টম জেসচারও তৈরি করতে পারেন।

বিমূর্ততার বিভিন্ন লেভেল

জেসচার ম্যানেজ করার জন্য Jetpack Compose বিভিন্ন লেভেলের অ্যাবস্ট্রাকশন প্রদান করে। সবচেয়ে উপরের লেভেলে কম্পোনেন্ট সাপোর্ট আছে। Button-এর মতো কম্পোজ করার উপযুক্ত আইটেম অটোমেটিক জেসচার সাপোর্ট অন্তর্ভুক্ত করে। কাস্টম কম্পোনেন্টে জেসচার সাপোর্ট যোগ করতে, আপনি যেকোনও কম্পোজেবল ফাংশনে জেসচার মডিফায়ার যেমন clickable যোগ করতে পারেন। সবশেষে, আপনার কাস্টম জেসচার প্রয়োজন হলে, আপনি pointerInput মডিফায়ার ব্যবহার করতে পারবেন।

সাধারণত, সবচেয়ে বেশি অ্যাবস্ট্রাকশন লেভেলে তৈরি করুন যা আপনার প্রয়োজনীয় কার্যকারিতা প্রদান করে। এইভাবে, লেয়ারে অন্তর্ভুক্ত পেশাদার পদ্ধতি থেকে আপনি উপকৃত হন। যেমন, Button-এ clickable-এর তুলনায় অ্যাক্সেসিবিলিটির জন্য ব্যবহৃত অনেক বেশি শব্দার্থ সংক্রান্ত তথ্য রয়েছে, যা একটি কাঁচা pointerInput প্রয়োগের তুলনায় অনেক বেশি তথ্য ধারণ করে।

কম্পোনেন্ট সহায়তা

Compose-এ অনেক রেডিমেড কম্পোনেন্ট থাকে যেগুলিতে ইন্টার্নাল জেসচার হ্যান্ডেল করার সুবিধা থাকে। যেমন, LazyColumn ড্র্যাগ জেসচারের উত্তরে এর কন্টেন্ট স্ক্রল করে, Button-এ প্রেস করলে একটি রিপেল দেখা যায় এবং SwipeToDismiss কম্পোনেন্টে কোনও এলিমেন্ট বাতিল করার জন্য সোয়াইপ করার লজিক থাকে। এই ধরনের জেসচার হ্যান্ডেলিং অটোমেটিক কাজ করে।

ইন্টার্নাল জেসচার হ্যান্ডেলিংয়ের পাশাপাশি, অনেক কম্পোনেন্টকেও জেসচার হ্যান্ডেল করতে হয়। যেমন, Button অটোমেটিক ট্যাপ শনাক্ত করে এবং ক্লিক ইভেন্ট ট্রিগার করে। আপনি জেসচার অনুযায়ী প্রতিক্রিয়া জানাতে Button-এ onClick ল্যাম্বডা পাস করেন। একইভাবে, ব্যবহারকারী স্লাইডার হ্যান্ডেল ড্র্যাগ করলে প্রতিক্রিয়া জানাতে আপনি onValueChange lambda-কে Slider-এর সাথে যোগ করেন।

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

// Talkback: "Click me!, Button, double tap to activate"
Button(onClick = { /* TODO */ }) { Text("Click me!") }
// Talkback: "Click me!, double tap to activate"
Box(Modifier.clickable { /* TODO */ }) { Text("Click me!") }

Compose-এ অ্যাক্সেসিবিলিটি সম্পর্কে আরও জানতে, Compose-এ অ্যাক্সেসিবিলিটি দেখুন।

মডিফায়ার সহ যেকোনও কম্পোজেবল এলিমেন্টে নির্দিষ্ট জেসচার যোগ করা

যেকোনও কম্পোজ করার উপযুক্ত আইটেমে জেসচার মডিফায়ার প্রয়োগ করে কম্পোজ করার উপযুক্ত আইটেমকে জেসচার শোনার উপযুক্ত করে তুলতে পারবেন। যেমন, আপনি কোনও জেনেরিক Box ট্যাপ জেসচারকে clickable করে হ্যান্ডেল করতে দিতে পারেন অথবা Column ভার্টিক্যাল স্ক্রলকে verticalScroll প্রয়োগ করে হ্যান্ডেল করতে দিতে পারেন।

বিভিন্ন ধরনের জেসচার হ্যান্ডেল করার জন্য অনেক মডিফায়ার আছে:

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

pointerInput মডিফায়ার সহ যেকোনও কম্পোজেবল ফাংশনে কাস্টম জেসচার যোগ করুন

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

নিম্নলিখিত কোড র পয়েন্টার ইভেন্ট শোনে:

@Composable
private fun LogPointerEvents(filter: PointerEventType? = null) {
    var log by remember { mutableStateOf("") }
    Column {
        Text(log)
        Box(
            Modifier
                .size(100.dp)
                .background(Color.Red)
                .pointerInput(filter) {
                    awaitPointerEventScope {
                        while (true) {
                            val event = awaitPointerEvent()
                            // handle pointer event
                            if (filter == null || event.type == filter) {
                                log = "${event.type}, ${event.changes.first().position}"
                            }
                        }
                    }
                }
        )
    }
}

আপনি এই স্নিপেটটি ভেঙে দেখলে, মূল কম্পোনেন্টগুলি হল:

  • pointerInput মডিফায়ার। আপনি এতে এক বা একাধিক কী পাস করেন। সেইসব কী-এর মধ্যে কোনওটির ভ্যালু পরিবর্তিত হলে, মডিফায়ার কন্টেন্ট ল্যাম্বডাকে আবার এক্সিকিউট করা হয়। কম্পোজ করা যায় এমন আইটেমের জন্য নমুনাটি একটি ঐচ্ছিক ফিল্টার পাস করে। যদি সেই ফিল্টারের ভ্যালু পরিবর্তিত হয়, তাহলে সঠিক ইভেন্ট লগ করা হয়েছে কিনা তা নিশ্চিত করতে, পয়েন্টার ইভেন্ট হ্যান্ডলারকে আবার এক্সিকিউট করতে হবে।
  • awaitPointerEventScope একটি কোরাউটিন স্কোপ তৈরি করে যা পয়েন্টার ইভেন্টের জন্য অপেক্ষা করতে ব্যবহার করা যেতে পারে।
  • পরবর্তী পয়েন্টার ইভেন্ট না হওয়া পর্যন্ত awaitPointerEvent কোরাউটিন সাসপেন্ড করে।

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

সম্পূর্ণ জেসচার শনাক্ত করা

র পointers ইভেন্ট ম্যানেজ করার পরিবর্তে, আপনি নির্দিষ্ট জেসচার ঘটতে পারে কিনা তা শুনতে এবং যথাযথভাবে উত্তর দিতে পারেন। AwaitPointerEventScope নিম্নলিখিত বিষয় শোনার জন্য পদ্ধতি প্রদান করে:

এগুলি হল টপ-লেভেল ডিটেক্টর, তাই আপনি একটি pointerInput মডিফায়ারের মধ্যে একাধিক ডিটেক্টর যোগ করতে পারবেন না। নিচের স্নিপেটটি শুধুমাত্র ট্যাপ শনাক্ত করে, ড্র্যাগ শনাক্ত করে না:

var log by remember { mutableStateOf("") }
Column {
    Text(log)
    Box(
        Modifier
            .size(100.dp)
            .background(Color.Red)
            .pointerInput(Unit) {
                detectTapGestures { log = "Tap!" }
                // Never reached
                detectDragGestures { _, _ -> log = "Dragging" }
            }
    )
}

অভ্যন্তরীণভাবে, detectTapGestures পদ্ধতি কোরাউটিন ব্লক করে এবং দ্বিতীয় ডিটেক্টর পর্যন্ত কখনও পৌঁছানো যায় না। আপনি যদি কোনও কম্পোজ করার উপযুক্ত আইটেমে একাধিক জেসচার লিসনার যোগ করতে চান, তাহলে এর পরিবর্তে আলাদা pointerInput মডিফায়ার ইনস্ট্যান্স ব্যবহার করুন:

var log by remember { mutableStateOf("") }
Column {
    Text(log)
    Box(
        Modifier
            .size(100.dp)
            .background(Color.Red)
            .pointerInput(Unit) {
                detectTapGestures { log = "Tap!" }
            }
            .pointerInput(Unit) {
                // These drag events will correctly be triggered
                detectDragGestures { _, _ -> log = "Dragging" }
            }
    )
}

জেসচার পিছু ইভেন্ট ম্যানেজ করা

সংজ্ঞা অনুযায়ী, জেসচার একটি পয়েন্টার ডাউন ইভেন্ট দিয়ে শুরু হয়। আপনি while(true) লুপের পরিবর্তে awaitEachGesture সহায়ক পদ্ধতি ব্যবহার করতে পারেন যা প্রতিটি কাঁচা ইভেন্টের মধ্য দিয়ে যায়। সবকটি পয়েন্টার তুলে নেওয়া হলে, awaitEachGesture পদ্ধতিটি ধারণকারী ব্লকটি রিস্টার্ট করে, এর অর্থ হল জেসচারটি সম্পূর্ণ হয়েছে:

@Composable
private fun SimpleClickable(onClick: () -> Unit) {
    Box(
        Modifier
            .size(100.dp)
            .pointerInput(onClick) {
                awaitEachGesture {
                    awaitFirstDown().also { it.consume() }
                    val up = waitForUpOrCancellation()
                    if (up != null) {
                        up.consume()
                        onClick()
                    }
                }
            }
    )
}

বাস্তবে, আপনি প্রায় সবসময়ই awaitEachGesture ব্যবহার করতে চাইবেন, যদি না আপনি জেসচার শনাক্ত না করেই পয়েন্টার ইভেন্টে উত্তর দেন। এর একটি উদাহরণ হল hoverable, যা পয়েন্টার ডাউন বা আপ ইভেন্টে কোনও প্রতিক্রিয়া জানায় না— এটি শুধু জানতে চায় যে কোনও পয়েন্টার কখন এর বাউন্ডে প্রবেশ করে বা বেরিয়ে যায়।

নির্দিষ্ট ইভেন্ট বা সাব-জেসচারের জন্য অপেক্ষা করা

জেসচারের সাধারণ অংশ শনাক্ত করতে সাহায্য করে এমন কিছু পদ্ধতি আছে:

  • awaitFirstDown দিয়ে কোনও পয়েন্টার নিচে না নামা পর্যন্ত সাসপেন্ড করুন অথবা waitForUpOrCancellation দিয়ে সব পয়েন্টার উপরে না ওঠা পর্যন্ত অপেক্ষা করুন।
  • awaitTouchSlopOrCancellation ও awaitDragOrCancellation ব্যবহার করে লো-লেভেল ড্র্যাগ লিসনার তৈরি করুন। পয়েন্টার টাচ স্লপে না পৌঁছানো পর্যন্ত জেসচার হ্যান্ডলার প্রথমে সাসপেন্ড হয় এবং তারপরে প্রথম ড্র্যাগ ইভেন্ট না আসা পর্যন্ত সাসপেন্ড হয়। আপনি যদি শুধুমাত্র একটি অক্ষ বরাবর ড্র্যাগ করতে চান, তাহলে awaitHorizontalTouchSlopOrCancellation প্লাস awaitHorizontalDragOrCancellation অথবা awaitVerticalTouchSlopOrCancellation প্লাস awaitVerticalDragOrCancellation ব্যবহার করুন।
  • awaitLongPressOrCancellation বোতামটি বেশিক্ষণ প্রেস না করা পর্যন্ত সাসপেন্ড করে রাখা হবে।
  • ড্র্যাগ ইভেন্ট ক্রমাগত শোনার জন্য drag পদ্ধতি ব্যবহার করুন অথবা horizontalDrag বা verticalDrag ব্যবহার করে একটি অক্ষে ড্র্যাগ ইভেন্ট শুনুন।

মাল্টি-টাচ ইভেন্টের জন্য গণনা প্রয়োগ করা

কোনও ব্যবহারকারী যখন একাধিক পয়েন্টার ব্যবহার করে মাল্টি-টাচ জেসচার পারফর্ম করছেন, তখন র ভ্যালুর উপর ভিত্তি করে প্রয়োজনীয় ট্রান্সফর্মেশন বোঝা কঠিন। transformable মডিফায়ার বা detectTransformGestures মেথড আপনার ব্যবহারের ক্ষেত্রে যথেষ্ট ফাইন-গ্রেন কন্ট্রোল না দিলে, আপনি র ইভেন্ট শুনতে এবং সেগুলিতে গণনা প্রয়োগ করতে পারবেন। এইসব হেল্পার পদ্ধতি হল calculateCentroid, calculateCentroidSize, calculatePan, calculateRotation এবং calculateZoom.

ইভেন্ট ডিসপ্যাচ করা ও হিট-টেস্টিং

প্রতিটি পয়েন্টার ইভেন্ট প্রতিটি pointerInput মডিফায়ারে পাঠানো হয় না। ইভেন্ট ডিসপ্যাচিং নিম্নলিখিতভাবে কাজ করে:

  • পয়েন্টার ইভেন্ট কম্পোজ করার উপযুক্ত হায়ারার্কিতে ডিসপ্যাচ করা হয়। নতুন পয়েন্টার যখন প্রথম পয়েন্টার ইভেন্ট ট্রিগার করে, তখন সিস্টেম "উপযুক্ত" কম্পোজ করার যোগ্য আইটেম হিট-টেস্ট করা শুরু করে। কোনও কম্পোজেবলকে উপযুক্ত হিসেবে বিবেচনা করা হয় যখন সেটির মধ্যে পয়েন্টার ইনপুট ম্যানেজ করার ক্ষমতা থাকে। UI ট্রির উপর থেকে নিচে পর্যন্ত হিট-টেস্টিং ফ্লো হয়। কোনও কম্পোজ করার উপযুক্ত আইটেমের মধ্যে পয়েন্টার ইভেন্ট ঘটলে, কম্পোজ করার উপযুক্ত আইটেমটি "হিট" হয়। এই প্রসেসের ফলে কম্পোজেবল আইটেমের চেন তৈরি হয় যা পজিটিভ হিট-টেস্ট করে।
  • ডিফল্ট হিসেবে, ট্রি-এর একই লেভেলে একাধিক উপযুক্ত কম্পোজ করার মতো আইটেম থাকলে, শুধুমাত্র সবচেয়ে বেশি z-ইনডেক্স থাকা কম্পোজ করার মতো আইটেমটিই "হিট" হয়। যেমন, আপনি Box-এ দুটি ওভারল্যাপ করা Button কম্পোজ করার মতো আইটেম যোগ করলে, শুধুমাত্র উপরে আঁকা আইটেমটিই যেকোনও পয়েন্টার ইভেন্ট পায়। আপনি তাত্ত্বিকভাবে নিজের PointerInputModifierNode ইমপ্লিমেন্টেশন তৈরি করে এবং sharePointerInputWithSiblings-এর মান true সেট করে এই আচরণ ওভাররাইড করতে পারবেন।
  • একই পয়েন্টারের জন্য আরও ইভেন্ট কম্পোজ করার উপযুক্ত একই চেনে ডিসপ্যাচ করা হয় এবং ইভেন্ট প্রোপাগেশন লজিক অনুযায়ী ফ্লো হয়। সিস্টেম এই পয়েন্টারের জন্য আর কোনও হিট-টেস্টিং পারফর্ম করে না। এর অর্থ হল, চেনের প্রতিটি কম্পোজ করার উপযুক্ত আইটেম সেই পয়েন্টারের জন্য সব ইভেন্ট পায়, এমনকি সেগুলি সেই কম্পোজ করার উপযুক্ত আইটেমের সীমার বাইরে ঘটলেও। চেনে না থাকা কম্পোজ করার উপযুক্ত আইটেম কখনও পয়েন্টার ইভেন্ট পায় না, এমনকি পয়েন্টারটি সেটির বাউন্ডের মধ্যে থাকলেও পায় না।

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

ইভেন্ট কনজাম্পশন

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

ছবি সহ তালিকা আইটেম, দুটি টেক্সট সহ কলাম ও একটি বোতাম।

ব্যবহারকারী বুকমার্ক বোতামে ট্যাপ করলে, বোতামের onClick ল্যাম্বডা সেই জেসচার হ্যান্ডেল করে। ব্যবহারকারী তালিকার আইটেমের অন্য কোনও অংশে ট্যাপ করলে, ListItem সেই জেসচার হ্যান্ডেল করে নিবন্ধে নেভিগেট করে। পয়েন্টার ইনপুট সংক্রান্ত, বোতামটিকে অবশ্যই এই ইভেন্টটি কনজিউম করতে হবে, যাতে এর পেরেন্ট বুঝতে পারে যে সেটি আর এই ইভেন্টে প্রতিক্রিয়া জানাবে না। আউট-অফ-দ্য-বক্স কম্পোনেন্ট ও সাধারণ জেসচার মডিফায়ারে অন্তর্ভুক্ত জেসচার এই কনজাম্পশন বিহেভিয়ার অন্তর্ভুক্ত করে, কিন্তু আপনি নিজের কাস্টম জেসচার লিখলে, আপনাকে অবশ্যই ম্যানুয়ালি ইভেন্ট কনজিউ করতে হবে। আপনি এটি PointerInputChange.consume পদ্ধতিতে করতে পারেন:

Modifier.pointerInput(Unit) {

    awaitEachGesture {
        while (true) {
            val event = awaitPointerEvent()
            // consume all changes
            event.changes.forEach { it.consume() }
        }
    }
}

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

Modifier.pointerInput(Unit) {
    awaitEachGesture {
        while (true) {
            val event = awaitPointerEvent()
            if (event.changes.any { it.isConsumed }) {
                // A pointer is consumed by another gesture handler
            } else {
                // Handle unconsumed event
            }
        }
    }
}

ইভেন্ট প্রোপাগেশন

আগে উল্লেখ করা হয়েছে, পয়েন্টার পরিবর্তন প্রতিটি কম্পোজ করার উপযুক্ত আইটেমে পাস করা হয়। কিন্তু, যদি এই ধরনের একাধিক কম্পোজ করার উপযুক্ত আইটেম থাকে, তাহলে কোন ক্রমে ইভেন্ট প্রোপাগেট হবে? আপনি যদি আগের বিভাগের উদাহরণটি নেন, তাহলে এই UI নিম্নলিখিত UI ট্রি-তে অনুবাদ করে, যেখানে শুধুমাত্র ListItem এবং Button পয়েন্টার ইভেন্টে প্রতিক্রিয়া জানায়:

ট্রি স্ট্রাকচার। সবচেয়ে উপরের লেয়ার হল ListItem, দ্বিতীয় লেয়ারে আছে Image, Column ও Button এবং Column দু'টি Text-এ বিভক্ত। ListItem ও Button হাইলাইট করা হয়েছে।

তিনটি "পাস" চলাকালীন, এই কম্পোজ করার উপযুক্ত প্রতিটি আইটেমের মধ্যে দিয়ে তিনবার করে পয়েন্টার ইভেন্ট ফ্লো করে:

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

ভিজ্যুয়ালি, ইভেন্ট ফ্লোকে নিম্নলিখিতভাবে উপস্থাপন করা যেতে পারে:

ইনপুট পরিবর্তন ব্যবহার করা হয়ে গেলে, ফ্লোয়ের সেই পয়েন্ট থেকে এই তথ্য পাস করা হয়:

কোডে, আপনি যে পাসে আগ্রহী সেটি নির্দিষ্ট করতে পারবেন:

Modifier.pointerInput(Unit) {
    awaitPointerEventScope {
        val eventOnInitialPass = awaitPointerEvent(PointerEventPass.Initial)
        val eventOnMainPass = awaitPointerEvent(PointerEventPass.Main) // default
        val eventOnFinalPass = awaitPointerEvent(PointerEventPass.Final)
    }
}

এই কোড স্নিপেটে, প্রতিটি await মেথড কল একই অভিন্ন ইভেন্ট রিটার্ন করে, যদিও কনজাম্পশন সংক্রান্ত ডেটা পরিবর্তিত হতে পারে।

জেসচার পরীক্ষা করা

আপনার পরীক্ষার পদ্ধতিতে, আপনি performTouchInput পদ্ধতি ব্যবহার করে ম্যানুয়ালি পয়েন্টার ইভেন্ট পাঠাতে পারবেন। এর ফলে আপনি হয় হাই-লেভেল সম্পূর্ণ জেসচার (যেমন, পিঞ্চ করা বা অনেকক্ষণ ক্লিক করে রাখা) অথবা লো-লেভেল জেসচার (যেমন, নির্দিষ্ট সংখ্যক পিক্সেল দিয়ে কার্সর সরানো) পারফর্ম করতে পারবেন:

composeTestRule.onNodeWithTag("MyList").performTouchInput {
    swipeUp()
    swipeDown()
    click()
}

আরও উদাহরণের জন্য performTouchInput ডকুমেন্টেশন দেখুন।

আরও জানুন

আপনি Jetpack Compose-এ জেসচার সম্পর্কে নিম্নলিখিত সোর্স থেকে আরও জানতে পারবেন: