ژست ها را درک کنید

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

تعاریف

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

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

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

منتظر یک رویداد یا حرکت فرعی خاص باشید

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

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

هنگامی که کاربر در حال انجام یک حرکت چند لمسی با استفاده از بیش از یک اشاره گر است، درک تغییر شکل مورد نیاز بر اساس مقادیر خام پیچیده است. اگر اصلاح‌کننده 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 به رویدادهای اشاره گر پاسخ می دهند:

ساختار درختی لایه بالایی ListItem است، لایه دوم دارای Image، Column و Button است و ستون به دو Text تقسیم می شود. 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 اطلاعات بیشتری کسب کنید:

{% کلمه به کلمه %} {% آخر کلمه %} {% کلمه به کلمه %} {% آخر کلمه %}