آشنایی با اشاره‌ها

چندین اصطلاح و مفهوم وجود دارد که درک آن‌ها هنگام کار با مدیریت اشاره در یک برنامه مهم است. این صفحه اصطلاحات اشاره‌گرها، رویدادهای اشاره‌گر، و اشاره‌ها را توضیح می‌دهد و سطوح مختلف انتزاع برای اشاره‌ها را معرفی می‌کند. همچنین به مصرف و انتشار رویدادها عمیق‌تر می‌پردازد.

تعاریف

برای درک مفاهیم مختلف در این صفحه، باید با برخی‌از اصطلاحات استفاده‌شده آشنا باشید:

  • اشاره‌گر: شیء فیزیکی که می‌توانید از آن برای تعامل با برنامه‌تان استفاده کنید. برای دستگاه‌های همراه، رایج‌ترین اشاره‌گر انگشت شما است که با صفحه لمسی تعامل می‌کند. یا می‌توانید از قلم به‌جای انگشتتان استفاده کنید. برای صفحه‌نمایش‌های بزرگ، می‌توانید از موشواره یا پد لمسی برای تعامل غیرمستقیم با نمایشگر استفاده کنید. دستگاه ورودی باید بتواند به مختصاتی «اشاره» کند تا به‌عنوان اشاره‌گر درنظر گرفته شود، بنابراین، برای مثال، صفحه‌کلید نمی‌تواند به‌عنوان اشاره‌گر درنظر گرفته شود. در «نوشتن»، نوع اشاره‌گر بااستفاده از PointerType در تغییرات اشاره‌گر گنجانده می‌شود.
  • رویداد اشاره‌گر: تعامل سطح پایین یک یا چند اشاره‌گر با برنامه در زمانی معین را توصیف می‌کند. هرگونه تعامل با اشاره‌گر، مانند قرار دادن انگشت روی صفحه یا کشیدن موشواره، باعث راه‌اندازی رویداد می‌شود. در «نوشتن»، همه اطلاعات مربوط به چنین رویدادی در کلاس PointerEvent گنجانده شده است.
  • اشاره: توالی رویدادهای اشاره‌گر که می‌تواند به‌عنوان یک کنش واحد تفسیر شود. برای مثال، اشاره تک‌ضرب می‌تواند به‌عنوان توالی رویداد پایین و سپس رویداد بالا درنظر گرفته شود. اشاره‌های رایجی وجود دارد که در بسیاری از برنامه‌ها استفاده می‌شود، مثل تک‌ضرب، کشیدن، یا تبدیل، اما درصورت نیاز می‌توانید اشاره سفارشی خودتان را نیز ایجاد کنید.

سطوح مختلف انتزاع

‫Jetpack Compose سطوح مختلفی از انتزاع برای مدیریت اشاره‌ها ارائه می‌دهد. در سطح بالا پشتیبانی از مؤلفه قرار دارد. عناصر ترکیبی مثل Button به‌طور خودکار از پشتیبانی اشاره‌ها برخوردارند. برای افزودن پشتیبانی از اشاره به عناصر سفارشی، می‌توانید اصلاح‌کننده‌های اشاره مانند clickable را به عناصر ترکیب‌پذیر دلخواه اضافه کنید. درنهایت، اگر به اشاره سفارشی نیاز دارید، می‌توانید از اصلاح‌گر pointerInput استفاده کنید.

به‌طورکلی، براساس بالاترین سطح انتزاعی که عملکرد موردنیاز شما را ارائه می‌دهد بسازید. به‌این‌ترتیب، از روال‌های مطلوب موجود در لایه بهره‌مند می‌شوید. برای مثال، Button نسبت‌به clickable اطلاعات معنایی بیشتری دارد که برای دسترس‌پذیری استفاده می‌شود، و نسبت‌به پیاده‌سازی pointerInput خام اطلاعات بیشتری دارد.

پشتیبانی از مؤلفه

بسیاری از عناصر آماده در Compose شامل نوعی مدیریت اشاره داخلی هستند. برای مثال، LazyColumn با پیمایش محتوای خود به اشاره‌های کشیدن پاسخ می‌دهد، Button وقتی آن را فشار می‌دهید موجی نشان می‌دهد، و عنصر SwipeToDismiss حاوی منطق تند کشیدن برای بستن عنصر است. این نوع مدیریت اشاره به‌طور خودکار کار می‌کند.

علاوه‌بر مدیریت اشاره داخلی، بسیاری از عناصر نیز از تماس‌گیرنده می‌خواهند که اشاره را مدیریت کند. برای مثال، Button به‌طور خودکار ضربه‌ها را شناسایی می‌کند و رویداد کلیک را راه‌اندازی می‌کند. یک لامبدای onClick را به Button ارسال می‌کنید تا به اشاره واکنش نشان دهد. به‌همین ترتیب، یک onValueChange لامبدا به 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!") }

برای کسب اطلاعات بیشتر درباره دسترس‌پذیری در «نوشتن»، دسترس‌پذیری در نوشتن را ببینید.

افزودن اشاره‌های خاص به عناصر ترکیبی اختیاری با اصلاح‌کننده‌ها

می‌توانید اصلاح‌کننده‌های اشاره را روی هر عنصر ترکیبی دلخواهی اعمال کنید تا عنصر ترکیبی به اشاره‌ها گوش دهد. برای مثال، می‌توانید با clickable کردن یک Box عمومی، به آن اجازه دهید اشاره‌های تک‌ضرب را مدیریت کند، یا با اعمال کردن verticalScroll، به یک Column اجازه دهید پیمایش عمودی را مدیریت کند.

تعداد زیادی اصلاح‌گر برای مدیریت انواع مختلف اشاره‌ها وجود دارد:

به‌طورکلی، اصلاح‌کننده‌های اشاره آماده را به مدیریت اشاره سفارشی ترجیح دهید. اصلاح‌گرها قابلیت‌های بیشتری به مدیریت رویداد اشاره‌گر خالص اضافه می‌کنند. برای مثال، اصلاح‌گر 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 روال هم‌زمان را تا زمانی‌که رویداد اشاره‌گر بعدی رخ دهد معلقه می‌کند.

اگرچه گوش دادن به رویدادهای ورودی خام قدرتمند است، اما نوشتن اشاره سفارشی براساس این داده‌های خام نیز پیچیده است. برای ساده کردن ایجاد اشاره‌های سفارشی، روش‌های ابزاری زیادی دردسترس است.

تشخیص اشاره‌های کامل

به‌جای مدیریت رویدادهای اشاره‌گر خام، می‌توانید به وقوع ژست‌های خاص گوش دهید و به‌طور مناسب پاسخ دهید. 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" }
            }
    )
}

پرداختن به رویدادها در هر اشاره

طبق تعریف، اشاره‌ها با رویداد اشاره‌گر پایین شروع می‌شوند. می‌توانید از روش کمکی awaitEachGesture به‌جای حلقه while(true) که از هر رویداد خام عبور می‌کند استفاده کنید. وقتی همه اشاره‌گرها برداشته می‌شوند، روش 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 است که به رویدادهای پایین یا بالای اشاره‌گر پاسخ نمی‌دهد— فقط باید بداند که اشاره‌گر چه زمانی وارد محدوده آن می‌شود یا از آن خارج می‌شود.

انتظار برای رویداد یا اشاره فرعی خاص

مجموعه‌ای از روش‌ها وجود دارد که به شناسایی بخش‌های مشترک اشاره‌ها کمک می‌کند:

اعمال محاسبات برای رویدادهای چندلمسی

وقتی کاربر بااستفاده از بیش‌از یک اشاره‌گر، اشاره چندلمسی انجام می‌دهد، درک تبدیل موردنیاز براساس مقادیر خام پیچیده است. اگر اصلاح‌کننده transformable یا روش‌های detectTransformGestures کنترل دقیق کافی برای مورد استفاده شما ارائه نمی‌دهند، می‌توانید به رویدادهای خام گوش دهید و محاسبات را روی آن‌ها اعمال کنید. این روش‌های کمکی عبارت‌اند از: calculateCentroid، calculateCentroidSize، calculatePan، calculateRotation، و calculateZoom.

اعزام رویداد و آزمایش ضربه

همه رویدادهای اشاره‌گر به همه اصلاح‌کننده‌های pointerInput ارسال نمی‌شود. ارسال رویداد به‌صورت زیر عمل می‌کند:

  • رویدادهای اشاره‌گر به سلسله مراتب ترکیب‌پذیر ارسال می‌شوند. لحظه‌ای که یک اشاره‌گر جدید اولین رویداد اشاره‌گر خود را راه‌اندازی می‌کند، سیستم شروع به آزمایش ضربه ترکیب‌های «واجدشرایط» می‌کند. ترکیب‌پذیر زمانی واجدشرایط درنظر گرفته می‌شود که قابلیت‌های مدیریت ورودی اشاره‌گر داشته باشد. جریان‌های آزمایش ضربه از بالای درخت واسط کاربر به پایین می‌رود. وقتی رویداد اشاره‌گر در محدوده آن عنصر ترکیبی رخ می‌دهد، عنصر ترکیبی «لمس» می‌شود. این فرایند منجر به زنجیره‌ای از عناصر ترکیب‌پذیر می‌شود که آزمون ضربه را با موفقیت پشت سر می‌گذارند.
  • به‌طور پیش‌فرض، وقتی چندین عنصر ترکیبی واجدشرایط در یک سطح از درخت وجود داشته باشد، فقط عنصر ترکیبی با بالاترین شاخص z «برخورد» می‌کند. برای مثال، وقتی دو عنصر ترکیبی Button هم‌پوشان را به Box اضافه می‌کنید، فقط عنصر ترکیبی که در بالا رسم شده است رویدادهای اشاره‌گر را دریافت می‌کند. ازنظر تئوری می‌توانید با ایجاد پیاده‌سازی PointerInputModifierNode خودتان و تنظیم sharePointerInputWithSiblings روی درست، این رفتار را ملغی کنید.
  • رویدادهای بعدی برای همان اشاره‌گر به همان زنجیره عناصر ترکیب‌پذیر ارسال می‌شوند و طبق منطق انتشار رویداد جریان می‌یابند. سیستم دیگر برای این اشاره‌گر آزمایش ضربه انجام نمی‌دهد. این یعنی هر ترکیب‌پذیر در زنجیره همه رویدادهای آن اشاره‌گر را دریافت می‌کند، حتی اگر این رویدادها خارج از محدوده آن ترکیب‌پذیر رخ دهند. عناصر ترکیبی که در زنجیره نباشند هرگز رویدادهای اشاره‌گر را دریافت نمی‌کنند، حتی اگر اشاره‌گر در محدوده آن‌ها باشد.

رویدادهای شناور، که با شناور کردن موشواره یا قلم ایجاد می‌شوند، استثنایی بر قوانین تعریف‌شده در اینجا هستند. رویدادهای شناور به هر عنصر ترکیبی که به آن برخورد کنند ارسال می‌شوند. بنابراین، وقتی کاربر اشاره‌گری را از محدوده یک عنصر ترکیبی به عنصر ترکیبی بعدی می‌برد، به‌جای اینکه رویدادها به آن عنصر ترکیبی اول ارسال شود، رویدادها به عنصر ترکیبی جدید ارسال می‌شود.

مصرف رویداد

وقتی بیش‌از یک عنصر ترکیبی دارای کنترل‌کننده اشاره اختصاص‌داده‌شده باشد، این کنترل‌کننده‌ها نباید با هم تداخل داشته باشند. برای مثال، به این واسط کاربر نگاه کنید:

مورد فهرست با «تصویر»، «ستون» با دو نوشتار، و «دکمه».

وقتی کاربری روی دکمه نشانک تک‌ضرب می‌زند، تابع لامبدای 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
            }
        }
    }
}

انتشار رویداد

همان‌طور که قبلاً ذکر شد، تغییرات اشاره‌گر به هر عنصر ترکیبی که با آن برخورد می‌کند منتقل می‌شود. اما اگر بیش‌از یک عنصر ترکیبی وجود داشته باشد، رویدادها به چه ترتیبی منتشر می‌شوند؟ اگر مثال بخش قبلی را درنظر بگیرید، این واسط کاربر به درخت واسط کاربر زیر ترجمه می‌شود، که در آن فقط ListItem و Button به رویدادهای اشاره‌گر پاسخ می‌دهند:

ساختار درختی. لایه بالایی ListItem است، لایه دوم شامل Image،‏ Column، و Button است، و Column به دو Text تقسیم می‌شود. «فهرست‌مورد» و «دکمه» برجسته می‌شوند.

رویدادهای اشاره‌گر درطول هریک از این عناصر ترکیبی سه بار و درطول سه «گذر» جریان می‌یابند:

  • در گذر اولیه، جریان رویداد از بالای درخت میانای کاربری به پایین می‌رود. این جریان به ولی اجازه می‌دهد رویدادی را قبل‌از اینکه کودک بتواند آن را مصرف کند رهگیری کند. برای مثال، راهنمای ابزار باید یک فشار طولانی را جایگزین کند به‌جای اینکه آن را به فرزندانش منتقل کند. در مثال ما، ListItem رویداد را قبل‌از Button دریافت می‌کند.
  • در گذر اصلی، رویداد از گره‌های برگ درخت میانای کاربر به ریشه درخت میانای کاربر جریان می‌یابد. در این مرحله است که معمولاً از اشاره‌ها استفاده می‌کنید و این مرحله گذر پیش‌فرض هنگام گوش دادن به رویدادها است. مدیریت اشاره‌ها در این گذر به‌معنای این است که گره‌های برگ بر والدین خود اولویت دارند، که منطقی‌ترین رفتار برای اکثر اشاره‌ها است. در مثال ما، Button رویداد را قبل‌از ListItem دریافت می‌کند.
  • در گذر نهایی، جریان رویداد یک بار دیگر از بالای درخت واسط کاربر به گره‌های برگ می‌رسد. این جریان به عناصر بالاتر در پشته اجازه می‌دهد به مصرف رویداد توسط والدینشان پاسخ دهند. برای مثال، وقتی فشار دادن دکمه به کشیدن والد پیمایش‌پذیر آن تبدیل می‌شود، دکمه نشانه موج‌دار خود را برمی‌دارد.

جریان رویداد را می‌توان به‌صورت زیر نشان داد:

وقتی تغییر ورودی مصرف شد، این اطلاعات از آن نقطه در جریان به بعد منتقل می‌شود:

در کد، می‌توانید مجوز موردنظرتان را مشخص کنید:

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

در این تکه‌کد، رویداد یکسان و مشابهی توسط هریک از این فراخوانی‌های روش انتظار برگردانده می‌شود، هرچند داده‌های مربوط به مصرف ممکن است تغییر کرده باشد.

امتحان کردن اشاره‌ها

در روش‌های آزمایشی‌تان، می‌توانید بااستفاده از روش performTouchInput رویدادهای اشاره‌گر را به‌صورت دستی ارسال کنید. این کار به شما امکان می‌دهد اشاره‌های کامل سطح بالاتر (مثل نیشگون گرفتن یا کلیک طولانی) یا اشاره‌های سطح پایین (مثل حرکت دادن مکان‌نما به میزان مشخصی از پیکسل‌ها) را انجام دهید:

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

برای مثال‌های بیشتر، به مستندات performTouchInput مراجعه کنید.

بیشتر بدانید

می‌توانید از منابع زیر درباره اشاره‌ها در Jetpack Compose بیشتر بدانید: