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

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

تعاریف

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

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

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

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

به‌عنوان یک قانون، روی بالاترین سطح انتزاعی که عملکرد موردنیازتان را ارائه می‌دهد بسازید. به‌این‌ترتیب، از روال‌های مطلوب موجود در لایه بهره‌مند می‌شوید. برای مثال، Button اطلاعات معنایی بیشتری نسبت‌به clickable دارد که برای دسترس‌پذیری استفاده می‌شود، و 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!") }

برای کسب اطلاعات بیشتر درباره دسترس‌پذیری در Compose، به دسترس‌پذیری در Compose مراجعه کنید.

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

می‌توانید اصلاح‌کننده‌های اشاره را روی هر ترکیب‌پذیر دلخواهی اعمال کنید تا ترکیب‌پذیر به اشاره‌ها گوش دهد. برای مثال، می‌توانید با 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. یک یا چند کلید را به آن ارسال می‌کنید. When the value of one of those keys changes, the modifier content lambda is re-executed. نمونه فیلتری اختیاری را به composable ارسال می‌کند. اگر مقدار آن فیلتر تغییر کند، گرداننده رویداد اشاره‌گر باید دوباره اجرا شود تا مطمئن شوید رویدادهای درست ثبت می‌شوند.
  • 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 کوروتین را مسدود می‌کند و آشکارساز دوم هرگز به آن نمی‌رسد. اگر لازم است بیش‌از یک شنونده اشاره به یک composable اضافه کنید، به‌جای آن از نمونه‌های اصلاح‌کننده 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 ارسال نمی‌شوند. ارسال رویداد به‌صورت زیر عمل می‌کند:

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

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

مصرف رویداد

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

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

وقتی کاربر روی دکمه نشانک تک‌ضرب می‌زند، onClick lambda دکمه آن اشاره را مدیریت می‌کند. وقتی کاربر روی هر بخش دیگری از مورد فهرست تک‌ضرب می‌زند، ListItem آن اشاره را مدیریت می‌کند و به مقاله می‌رود. ازنظر ورودی اشاره‌گر، «دکمه» باید این رویداد را مصرف کند تا والد آن بداند که دیگر نباید به آن واکنش نشان دهد. اشاره‌های موجود در مؤلفه‌های آماده و اصلاح‌کننده‌های اشاره رایج شامل این رفتار مصرف هستند، اما اگر اشاره سفارشی خود را می‌نویسید، باید رویدادها را به‌صورت دستی مصرف کنید. این کار را با روش PointerInputChange.consume انجام می‌دهید:

Modifier.pointerInput(Unit) {

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

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

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 برجسته شده‌اند.

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

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

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

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

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

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 از منابع زیر بیشتر بدانید: