چندین اصطلاح و مفهوم وجود دارد که درک آنها هنگام کار بر روی مدیریت حرکت در یک برنامه مهم است. این صفحه اصطلاحات اشاره گر، رویدادهای اشاره گر، و ژست ها را توضیح می دهد و سطوح مختلف انتزاع برای ژست ها را معرفی می کند. همچنین عمیقتر به مصرف و انتشار رویداد میپردازد.
تعاریف
برای درک مفاهیم مختلف در این صفحه، باید برخی از اصطلاحات استفاده شده را درک کنید:
- اشاره گر : یک شی فیزیکی که می توانید از آن برای تعامل با برنامه خود استفاده کنید. برای دستگاه های تلفن همراه، رایج ترین اشاره گر تعامل انگشت شما با صفحه لمسی است. از طرف دیگر، می توانید از یک قلم برای جایگزینی انگشت خود استفاده کنید. برای صفحه نمایش های بزرگ، می توانید از ماوس یا پد لمسی برای تعامل غیرمستقیم با نمایشگر استفاده کنید. یک دستگاه ورودی باید بتواند به یک مختصات اشاره کند تا به عنوان یک اشاره گر در نظر گرفته شود، بنابراین برای مثال، یک صفحه کلید را نمی توان یک اشاره گر در نظر گرفت. در Compose، نوع اشاره گر با استفاده از
PointerType
در تغییرات نشانگر گنجانده شده است. - رویداد اشاره گر : تعامل سطح پایین یک یا چند اشاره گر با برنامه را در یک زمان معین توصیف می کند. هر گونه فعل و انفعال نشانگر، مانند قرار دادن انگشت روی صفحه یا کشیدن ماوس، یک رویداد را آغاز می کند. در Compose، تمام اطلاعات مربوط به چنین رویدادی در کلاس
PointerEvent
موجود است. - ژست : دنباله ای از رویدادهای اشاره گر که می تواند به عنوان یک عمل واحد تفسیر شود. به عنوان مثال، یک حرکت ضربه زدن را می توان دنباله ای از یک رویداد پایین و به دنبال آن یک رویداد بالا در نظر گرفت. حرکات معمولی وجود دارد که توسط بسیاری از برنامه ها استفاده می شود، مانند ضربه زدن، کشیدن، یا تبدیل، اما شما همچنین می توانید در صورت نیاز ژست سفارشی خود را ایجاد کنید.
سطوح مختلف انتزاع
Jetpack Compose سطوح مختلفی از انتزاع را برای مدیریت ژست ها فراهم می کند. در سطح بالا پشتیبانی کامپوننت است. قابلیت های Composable مانند Button
به طور خودکار شامل پشتیبانی ژست می شوند. برای افزودن پشتیبانی ژست به اجزای سفارشی، میتوانید اصلاحکنندههای حرکتی مانند clickable
را به اجزای دلخواه اضافه کنید. در نهایت، اگر به یک ژست سفارشی نیاز دارید، می توانید از اصلاح کننده pointerInput
استفاده کنید.
به عنوان یک قاعده، بر روی بالاترین سطح انتزاعی که عملکرد مورد نیاز شما را ارائه می دهد، بسازید. به این ترتیب، از بهترین شیوه های موجود در لایه بهره مند می شوید. برای مثال، Button
حاوی اطلاعات معنایی بیشتری است که برای دسترسی استفاده میشود تا clickable
، که حاوی اطلاعات بیشتری نسبت به اجرای pointerInput
خام است.
پشتیبانی از کامپوننت
بسیاری از اجزای خارج از جعبه در Compose شامل نوعی مدیریت حرکت داخلی هستند. به عنوان مثال، یک LazyColumn
با پیمایش محتوای خود به حرکات درگ پاسخ می دهد، یک Button
هنگامی که آن را فشار می دهید، موجی را نشان می دهد، و مؤلفه SwipeToDismiss
حاوی منطق کشیدن برای رد کردن یک عنصر است. این نوع مدیریت ژست به طور خودکار کار می کند.
در کنار مدیریت ژست های داخلی، بسیاری از مؤلفه ها نیز به تماس گیرنده نیاز دارند تا ژست را مدیریت کند. به عنوان مثال، یک Button
به طور خودکار ضربه ها را تشخیص می دهد و یک رویداد کلیک را راه اندازی می کند. شما یک onClick
lambda را به Button
ارسال می کنید تا به ژست واکنش نشان دهید. به طور مشابه، یک 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!") }
برای کسب اطلاعات بیشتر در مورد قابلیت دسترسی در نوشتن، دسترسی در نوشتن را ببینید.
ژستهای حرکتی خاص را با اصلاحکنندهها به ترکیبهای دلخواه اضافه کنید
میتوانید اصلاحکنندههای ژستها را برای هر آهنگسازی دلخواه اعمال کنید تا آن را به ژستها گوش دهید. بهعنوان مثال، میتوانید به یک Box
عمومی اجازه دهید تا با استفاده از clickable
روی ژستها ضربه بزند، یا به یک Column
اجازه دهید با اعمال verticalScroll
، اسکرول عمودی را انجام دهد.
اصلاحکنندههای زیادی برای مدیریت انواع ژستها وجود دارد:
- ضربهها و فشارها را با اصلاحکنندههای
clickable
،combinedClickable
،selectable
،toggleable
، وtriStateToggleable
مدیریت کنید. - اسکرول را با
horizontalScroll
،verticalScroll
، و اصلاح کننده های عمومی ترscrollable
انجام دهید. - کشیدن را با اصلاح کننده
draggable
وswipeable
کنترل کنید. - با اصلاح کننده
transformable
، حرکات چند لمسی مانند حرکت، چرخش، و بزرگنمایی را مدیریت کنید .
به عنوان یک قاعده، اصلاح کننده های حرکتی خارج از جعبه را به مدیریت ژست های سفارشی ترجیح دهید. اصلاحکنندهها قابلیتهای بیشتری را به مدیریت رویداد نشانگر خالص اضافه میکنند. برای مثال، اصلاحکننده clickable
نه تنها تشخیص فشار و ضربه را اضافه میکند، بلکه اطلاعات معنایی، نشانههای بصری در مورد تعاملات، شناور کردن، فوکوس و پشتیبانی از صفحه کلید را نیز اضافه میکند. می توانید کد منبع clickable
را بررسی کنید تا ببینید عملکرد چگونه اضافه می شود.
با تغییر دهنده pointerInput
اشاره سفارشی را به composable های دلخواه اضافه کنید
هر حرکتی با یک اصلاح کننده حرکتی خارج از جعبه پیاده سازی نمی شود. به عنوان مثال، نمی توانید از یک اصلاح کننده برای واکنش به کشیدن پس از فشار طولانی، یک کلیک کنترلی، یا یک ضربه سه انگشتی استفاده کنید. درعوض، میتوانید برای شناسایی این حرکات سفارشی، کنترلکننده ژستهای خود را بنویسید. میتوانید با مدیفایر 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
. شما آن را یک یا چند کلید پاس می دهید. هنگامی که مقدار یکی از آن کلیدها تغییر می کند، محتوای اصلاح کننده لامبدا دوباره اجرا می شود. نمونه یک فیلتر اختیاری را به composable منتقل می کند. اگر مقدار آن فیلتر تغییر کند، کنترل کننده رویداد اشاره گر باید دوباره اجرا شود تا مطمئن شوید که رویدادهای مناسب ثبت شده اند. -
awaitPointerEventScope
یک محدوده کاری ایجاد می کند که می تواند برای انتظار رویدادهای اشاره گر استفاده شود. -
awaitPointerEvent
تا زمانی که یک رویداد اشاره گر بعدی اتفاق بیفتد، برنامه را به حالت تعلیق در می آورد.
اگرچه گوش دادن به رویدادهای ورودی خام قدرتمند است، اما نوشتن یک حرکت سفارشی بر اساس این داده های خام نیز پیچیده است. برای ساده کردن ایجاد حرکات سفارشی، بسیاری از روشهای کاربردی در دسترس هستند.
ژست های کامل را تشخیص دهید
به جای مدیریت رویدادهای اشاره گر خام، می توانید به ژست های خاص گوش دهید و به درستی پاسخ دهید. AwaitPointerEventScope
روش هایی را برای گوش دادن به موارد زیر ارائه می دهد:
- فشار دهید، ضربه بزنید، دو بار ضربه بزنید، و طولانی فشار دهید:
detectTapGestures
- Drags:
detectHorizontalDragGestures
،detectVerticalDragGestures
،detectDragGestures
وdetectDragGesturesAfterLongPress
- Transforms:
detectTransformGestures
اینها آشکارسازهای سطح بالایی هستند، بنابراین نمی توانید چندین آشکارساز را در یک اصلاح کننده 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" } } ) }
رویدادها را در هر ژست مدیریت کنید
طبق تعریف، ژستها با یک رویداد اشاره گر پایین شروع میشوند. میتوانید به جای حلقه 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
ارسال نمی شود. عملیات اعزام رویداد به شرح زیر است:
- رویدادهای اشاره گر به یک سلسله مراتب قابل ترکیب ارسال می شوند. لحظه ای که یک اشاره گر جدید اولین رویداد اشاره گر خود را راه اندازی می کند، سیستم شروع به تست ضربه زدن به ترکیب های "واجد شرایط" می کند. یک Composable زمانی واجد شرایط در نظر گرفته می شود که دارای قابلیت مدیریت ورودی اشاره گر باشد. تست ضربه از بالای درخت UI به پایین جریان دارد. زمانی که رویداد اشارهگر در محدودههای آن composable رخ داده باشد، یک composable ضربه میزند. این فرآیند منجر به زنجیرهای از مواد ترکیبپذیر میشود که تست مثبت را نشان میدهند.
- بهطور پیشفرض، زمانی که چندین ترکیب واجد شرایط در یک سطح درخت وجود داشته باشد، تنها قابل ترکیب با بالاترین شاخص z «هیت» میشود. به عنوان مثال، وقتی دو
Button
ترکیبپذیر همپوشانی را به یکBox
اضافه میکنید، تنها موردی که در بالا کشیده شده است، رویدادهای اشارهگر را دریافت میکند. از نظر تئوری میتوانید با ایجاد پیادهسازیPointerInputModifierNode
خودتان و تنظیمsharePointerInputWithSiblings
روی true، این رفتار را نادیده بگیرید. - رویدادهای بیشتر برای همان اشاره گر به همان زنجیره از اجزای سازنده ارسال می شوند و بر اساس منطق انتشار رویداد جریان می یابند. سیستم دیگر تست ضربه ای را برای این اشاره گر انجام نمی دهد. این به این معنی است که هر ترکیبسازی در زنجیره، همه رویدادهای آن اشارهگر را دریافت میکند، حتی زمانی که این رویدادها خارج از محدوده آن ترکیبپذیر رخ میدهند. ترکیبپذیرهایی که در زنجیره نیستند، هرگز رویدادهای اشارهگر را دریافت نمیکنند، حتی زمانی که اشارهگر در داخل کرانهای آنها باشد.
رویدادهای شناور، که توسط ماوس یا قلم فعال می شوند، استثنایی از قوانین تعریف شده در اینجا هستند. رویدادهای شناور به هر کامپوزیتی که برخورد می کند ارسال می شود. بنابراین هنگامی که یک کاربر یک اشاره گر را از مرزهای یک composable به بعدی نشان می دهد، به جای ارسال رویدادها به اولین composable، رویدادها به composable جدید ارسال می شوند.
مصرف رویداد
هنگامی که بیش از یک قابل ترکیب یک کنترل کننده اشاره به آن اختصاص داده شده است، این کنترل کننده ها نباید تداخل داشته باشند. به عنوان مثال، به این UI نگاهی بیندازید:
وقتی کاربر روی دکمه نشانک ضربه میزند، دکمه 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 } } } }
انتشار رویداد
همانطور که قبلا ذکر شد، تغییرات اشاره گر به هر ترکیبی که ضربه می زند ارسال می شود. اما اگر بیش از یک چنین ترکیبسازی وجود داشته باشد، رویدادها به چه ترتیبی منتشر میشوند؟ اگر مثال را از بخش آخر بگیرید، این UI به درخت UI زیر ترجمه می شود، جایی که فقط ListItem
و Button
به رویدادهای اشاره گر پاسخ می دهند:
رویدادهای اشاره گر از طریق هر یک از این ترکیبپذیرها سه بار طی سه «گذر» جریان مییابند:
- در پاس اولیه ، رویداد از بالای درخت UI به پایین جریان می یابد. این جریان به والدین اجازه می دهد تا قبل از اینکه کودک آن را مصرف کند، رویدادی را رهگیری کند. به عنوان مثال، راهنمای ابزار باید به جای انتقال آن به فرزندان خود ، فشار طولانی را قطع کند . در مثال ما،
ListItem
رویداد را قبل ازButton
دریافت می کند. - در Main pass ، رویداد از گره های برگ درخت UI تا ریشه درخت UI جریان می یابد. این مرحله جایی است که شما معمولاً از ژستها استفاده میکنید و هنگام گوش دادن به رویدادها، پاس پیشفرض است. مدیریت ژست ها در این پاس به این معنی است که گره های برگ بر والدین خود اولویت دارند، که منطقی ترین رفتار برای اکثر حرکات است. در مثال ما،
Button
رویداد قبل ازListItem
را دریافت می کند. - در Final pass ، رویداد یک بار دیگر از بالای درخت UI به گرههای برگ جریان مییابد. این جریان به عناصر بالاتر در پشته اجازه می دهد تا به مصرف رویداد توسط والد خود پاسخ دهند. به عنوان مثال، هنگامی که یک فشار به درگ والد قابل پیمایش تبدیل میشود، یک دکمه نشاندهنده امواج خود را حذف میکند.
به صورت بصری، جریان رویداد را می توان به صورت زیر نشان داد:
هنگامی که یک تغییر ورودی مصرف می شود، این اطلاعات از آن نقطه در جریان به بعد منتقل می شود:
در کد، می توانید پاس مورد علاقه خود را مشخص کنید:
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 اطلاعات بیشتری کسب کنید:
{% کلمه به کلمه %}برای شما توصیه می شود
- توجه: وقتی جاوا اسکریپت خاموش است، متن پیوند نمایش داده می شود
- قابلیت دسترسی در نوشتن
- اسکرول کنید
- ضربه بزنید و فشار دهید