مؤلفههای میانای کاربر ازطریق نحوه پاسخ دادن به تعاملات کاربر به کاربر دستگاه بازخورد میدهند. هر عنصر روش خاص خود را برای پاسخ دادن به تعاملات دارد که به کاربر کمک میکند بداند تعاملاتش چه کاری انجام میدهند. برای مثال، اگر کاربری دکمهای را در صفحه لمسی دستگاه لمس کند، احتمالاً دکمه به نحوی تغییر میکند، شاید با افزودن رنگ برجسته. این تغییر به کاربر اطلاع میدهد که دکمه را لمس کرده است. اگر کاربر نمیخواهد این کار را انجام دهد، میداند که قبلاز رها کردن انگشتش باید آن را از روی دکمه بردارد، درغیراینصورت دکمه فعال خواهد شد.
مستندات Compose اشارهها نحوه مدیریت عناصر Compose برای رویدادهای اشارهگر سطح پایین، مانند حرکتهای اشارهگر و کلیکها را پوشش میدهد. «نوشتن» بهطور پیشفرض آن رویدادهای سطح پایین را به تعاملات سطح بالاتر تبدیل میکند، برای مثال، مجموعهای از رویدادهای اشارهگر ممکن است به فشار دادن و رها کردن دکمه تبدیل شود. درک این انتزاعهای سطح بالاتر میتواند به شما کمک کند نحوه پاسخ واسط کاربر به کاربر را سفارشیسازی کنید. برای مثال، ممکن است بخواهید نحوه تغییر ظاهر یک عنصر را هنگام تعامل کاربر با آن سفارشیسازی کنید، یا شاید فقط بخواهید گزارشی از این کنشهای کاربر را حفظ کنید. این سند اطلاعات لازم برای تغییر عناصر استاندارد واسط کاربر یا طراحی عناصر خودتان را دراختیارتان قرار میدهد.
تعاملات
در بسیاری از موارد، نیازی نیست بدانید که مؤلفه Compose چگونه
تعاملات کاربر را تفسیر میکند. برای مثال، Button برای اینکه بفهمد کاربر روی دکمه کلیک کرده است یا نه، به
Modifier.clickable
متکی است. اگر دکمهای معمولی به برنامهتان اضافه میکنید، میتوانید کد onClick دکمه را تعریف کنید و
Modifier.clickable آن کد را در زمان مناسب اجرا میکند. یعنی لازم نیست بدانید کاربر روی صفحهنمایش تکضرب زده است یا دکمه را با صفحهکلید انتخاب کرده است؛ Modifier.clickable متوجه میشود که کاربر کلیک کرده است و با اجرای کد onClick شما پاسخ میدهد.
بااینحال، اگر میخواهید پاسخ عنصر میانای کاربری خود را به رفتار کاربر سفارشیسازی کنید، ممکن است لازم باشد درباره آنچه در پسزمینه اتفاق میافتد بیشتر بدانید. این بخش برخیاز این اطلاعات را دراختیارتان قرار میدهد.
وقتی کاربر با عنصر میانای کاربری تعامل برقرار میکند، سیستم رفتار او را با تولید تعدادی رویداد
Interaction
نشان میدهد. برای مثال، اگر کاربری دکمهای را لمس کند، دکمه
PressInteraction.Press تولید میکند.
اگر کاربر انگشت خود را داخل دکمه بردارد،
PressInteraction.Release تولید میشود و دکمه متوجه میشود که کلیک تمام شده است. از طرف دیگر، اگر کاربر انگشت خود را به خارج از دکمه بکشد و سپس انگشت خود را بردارد، دکمه
PressInteraction.Cancel
تولید میکند تا نشان دهد فشار روی دکمه لغو شده است، نه تکمیل شده است.
این تعاملات بیطرفانه هستند. یعنی این رویدادهای تعامل سطح پایین قصد ندارند معنای کنشهای کاربر یا توالی آنها را تفسیر کنند. همچنین تفسیر نمیکنند که کدام کنشهای کاربر ممکن است نسبت به کنشهای دیگر اولویت داشته باشند.
این تعاملات معمولاً بهصورت جفت، با شروع و پایان، انجام میشوند. تعامل دوم
حاوی ارجاع به تعامل اول است. برای مثال، اگر کاربری
دکمهای را لمس کند و سپس انگشتش را بردارد، لمس باعث ایجاد
PressInteraction.Press
تعامل میشود و برداشتن انگشت باعث ایجاد
PressInteraction.Release میشود؛
Release دارای ویژگی press است که PressInteraction.Press اولیه را
شناسایی میکند.
با مشاهده InteractionSource میتوانید تعاملات مربوط به یک مؤلفه خاص را ببینید. InteractionSource روی جریانهای Kotlin ساخته شده است، بنابراین میتوانید تعاملات آن را به همان روشی که با هر جریان دیگری کار میکنید جمعآوری کنید. برای کسب اطلاعات بیشتر درباره این تصمیم طراحی،
پست وبلاگ تعاملات روشنگر را ببینید.
وضعیت تعامل
ممکن است بخواهید با ردیابی تعاملات خودتان، عملکرد داخلی عناصرتان را گسترش دهید. برای مثال، شاید بخواهید دکمهای داشته باشید که
وقتی فشار داده میشود رنگش تغییر کند. سادهترین راه برای ردیابی تعاملها این است که
وضعیت تعامل مناسب را مشاهده کنید. InteractionSource روشهای متعددی ارائه میدهد که وضعیتهای مختلف تعامل را بهعنوان وضعیت نشان میدهد. برای مثال، اگر میخواهید ببینید دکمه خاصی فشار داده شده است یا نه، میتوانید روش
InteractionSource.collectIsPressedAsState()
آن را فراخوانی کنید:
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() Button( onClick = { /* do something */ }, interactionSource = interactionSource ) { Text(if (isPressed) "Pressed!" else "Not pressed") }
علاوهبر collectIsPressedAsState()، «نوشتن» همچنین
collectIsFocusedAsState()، collectIsDraggedAsState()، و
collectIsHoveredAsState() را ارائه میدهد. این روشها درواقع روشهای راحتی هستند که
روی میاناهای برنامهسازی کاربردی InteractionSource سطح پایینتر ساخته شدهاند. در برخی موارد، ممکن است بخواهید مستقیماً از آن توابع سطح پایینتر استفاده کنید.
برای مثال، فرض کنید باید بدانید که آیا دکمهای فشار داده میشود یا خیر، و
همچنین آیا کشیده میشود یا خیر. اگر از هر دو collectIsPressedAsState()
و collectIsDraggedAsState() استفاده کنید، «نوشتن» کارهای تکراری زیادی انجام میدهد و
هیچ تضمینی وجود ندارد که همه تعاملات را بهترتیب درست دریافت کنید. برای
موقعیتهایی مانند این، ممکن است بخواهید مستقیماً با
InteractionSource کار کنید. برای اطلاعات بیشتر درباره پیگیری تعاملات خودتان با InteractionSource، به کار با InteractionSource مراجعه کنید.
بخش زیر نحوه مصرف و انتشار تعاملات با
InteractionSource و MutableInteractionSource را بهترتیب شرح میدهد.
مصرف و انتشار Interaction
InteractionSource نشاندهنده جاریسازی فقط خواندنی Interactions است — نمیتوان Interaction را به InteractionSource ارسال کرد. برای انتشار
Interaction، باید از MutableInteractionSource استفاده کنید که از
InteractionSource گسترش مییابد.
اصلاحکنندهها و عناصر میتوانند Interactions را مصرف کنند، منتشر کنند، یا مصرف و منتشر کنند.
بخشهای زیر نحوه مصرف و انتشار تعاملات از هر دو
اصلاحگر و مؤلفه را شرح میدهد.
نمونه مصرفکننده اصلاحگر
برای اصلاحگری که حاشیهای برای وضعیت تمرکز میکشد، فقط باید
Interactions را مشاهده کنید، بنابراین میتوانید InteractionSource را بپذیرید:
fun Modifier.focusBorder(interactionSource: InteractionSource): Modifier { // ... }
از امضای تابع مشخص است که این اصلاحگر مصرفکننده است — میتواند Interaction را مصرف کند، اما نمیتواند آنها را منتشر کند.
تولید مثال اصلاحگر
برای اصلاحگری که رویدادهای شناور را مدیریت میکند، مثل Modifier.hoverable، باید Interactions را منتشر کنید و MutableInteractionSource را بهعنوان پارامتر بپذیرید:
fun Modifier.hover(interactionSource: MutableInteractionSource, enabled: Boolean): Modifier { // ... }
این اصلاحگر تولیدکننده است — میتواند از MutableInteractionSource ارائهشده برای انتشار HoverInteractions هنگام قرار گرفتن یا برداشته شدن اشارهگر استفاده کند.
ساختن عناصری که مصرف و تولید میکنند
عناصر سطح بالا مثل Material Button هم بهعنوان تولیدکننده و هم بهعنوان مصرفکننده عمل میکنند. این دکمهها رویدادهای ورودی و تمرکز را مدیریت میکنند و همچنین ظاهرشان را
در پاسخ به این رویدادها تغییر میدهند، مثلاً موجی نشان میدهند یا ارتفاعشان را
پویانمایی میکنند. درنتیجه، آنها مستقیماً MutableInteractionSource را بهعنوان
پارامتر آشکار میکنند، تا بتوانید نمونه بهیادمانده خودتان را ارائه دهید:
@Composable fun Button( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, // exposes MutableInteractionSource as a parameter interactionSource: MutableInteractionSource? = null, elevation: ButtonElevation? = ButtonDefaults.elevatedButtonElevation(), shape: Shape = MaterialTheme.shapes.small, border: BorderStroke? = null, colors: ButtonColors = ButtonDefaults.buttonColors(), contentPadding: PaddingValues = ButtonDefaults.ContentPadding, content: @Composable RowScope.() -> Unit ) { /* content() */ }
این کار امکان بالا بردن
MutableInteractionSource از جزء و مشاهده همه
Interactionهای تولیدشده توسط جزء را فراهم میکند. میتوانید از این برای کنترل
ظاهر آن عنصر یا هر عنصر دیگری در واسط کاربرتان استفاده کنید.
اگر درحال ساختن عناصر تعاملی سطح بالای خودتان هستید، توصیه میکنیم
که MutableInteractionSource را به این روش بهعنوان پارامتر نمایان کنید. علاوهبر
دنبال کردن روالهای مطلوب بالابردن وضعیت، این کار خواندن و
کنترل وضعیت دیداری یک عنصر را به همان روشی که هر نوع وضعیت دیگری (مثل وضعیت فعال) میتواند خوانده و کنترل شود آسان میکند.
Compose از رویکرد معماری لایهای پیروی میکند،
بنابراین عناصر سطح بالای Material روی واحدهای سازنده
پایهای ساخته میشوند که Interactionهای موردنیاز برای کنترل موجها و دیگر
جلوههای بصری را تولید میکنند. کتابخانه پایه اصلاحکنندههای تعامل سطح بالا
مثل Modifier.hoverable، Modifier.focusable، و
Modifier.draggable را ارائه میدهد.
برای ساختن مؤلفهای که به رویدادهای نگه داشتن پاسخ میدهد، میتوانید بهسادگی از
Modifier.hoverable استفاده کنید و MutableInteractionSource را بهعنوان پارامتر ارسال کنید.
هرگاه نشانگر روی عنصر قرار گیرد، HoverInteractions منتشر میکند و میتوانید از این برای تغییر نحوه نمایش عنصر استفاده کنید.
// This InteractionSource will emit hover interactions val interactionSource = remember { MutableInteractionSource() } Box( Modifier .size(100.dp) .hoverable(interactionSource = interactionSource), contentAlignment = Alignment.Center ) { Text("Hello!") }
برای اینکه این عنصر هم قابلتمرکز باشد، میتوانید Modifier.focusable را اضافه کنید و همان MutableInteractionSource را بهعنوان پارامتر ارسال کنید. اکنون، هم HoverInteraction.Enter/Exit و هم FocusInteraction.Focus/Unfocus ازطریق MutableInteractionSource یکسانی منتشر میشوند و میتوانید ظاهر هر دو نوع تعامل را در یک مکان سفارشیسازی کنید:
// This InteractionSource will emit hover and focus interactions val interactionSource = remember { MutableInteractionSource() } Box( Modifier .size(100.dp) .hoverable(interactionSource = interactionSource) .focusable(interactionSource = interactionSource), contentAlignment = Alignment.Center ) { Text("Hello!") }
Modifier.clickable انتزاعیتر از hoverable و focusable است — برای اینکه عنصری کلیککردنی باشد، بهطور ضمنی قابلنگهداشتن است، و عناصری که میتوانند کلیک شوند باید قابلتمرکز هم باشند. میتوانید از Modifier.clickable برای ایجاد مؤلفهای استفاده کنید که
تعاملات شناور شدن، تمرکز، و فشار را مدیریت میکند، بدون اینکه نیاز باشد
میاناهای برنامهسازی کاربردی سطح پایینتر را ترکیب کنید. اگر میخواهید عنصرتان کلیککردنی هم باشد، میتوانید
hoverable و focusable را با clickable جایگزین کنید:
// This InteractionSource will emit hover, focus, and press interactions val interactionSource = remember { MutableInteractionSource() } Box( Modifier .size(100.dp) .clickable( onClick = {}, interactionSource = interactionSource, // Also show a ripple effect indication = ripple() ), contentAlignment = Alignment.Center ) { Text("Hello!") }
کار با InteractionSource
اگر به اطلاعات سطح پایین درباره تعامل با یک عنصر نیاز دارید، میتوانید از میاناهای برنامهسازی کاربردی جریان استاندارد برای InteractionSource آن عنصر استفاده کنید.
برای مثال، فرض کنید میخواهید فهرستی از تعاملات فشار دادن و کشیدن برای InteractionSource حفظ کنید. این کد نیمی از کار را انجام میدهد و
فشار جدید را به فهرست اضافه میکند:
val interactionSource = remember { MutableInteractionSource() } val interactions = remember { mutableStateListOf<Interaction>() } LaunchedEffect(interactionSource) { interactionSource.interactions.collect { interaction -> when (interaction) { is PressInteraction.Press -> { interactions.add(interaction) } is DragInteraction.Start -> { interactions.add(interaction) } } } }
اما علاوهبر افزودن تعاملهای جدید، باید تعاملها را نیز وقتی تمام میشوند بردارید (برای مثال، وقتی کاربر انگشتش را از روی عنصر برمیدارد). این کار آسان است، زیرا تعاملهای پایانی همیشه به تعامل شروع مرتبط ارجاع میدهند. این کد نشان میدهد چگونه تعاملاتی را که پایان یافتهاند بردارید:
val interactionSource = remember { MutableInteractionSource() } val interactions = remember { mutableStateListOf<Interaction>() } LaunchedEffect(interactionSource) { interactionSource.interactions.collect { interaction -> when (interaction) { is PressInteraction.Press -> { interactions.add(interaction) } is PressInteraction.Release -> { interactions.remove(interaction.press) } is PressInteraction.Cancel -> { interactions.remove(interaction.press) } is DragInteraction.Start -> { interactions.add(interaction) } is DragInteraction.Stop -> { interactions.remove(interaction.start) } is DragInteraction.Cancel -> { interactions.remove(interaction.start) } } } }
اکنون، اگر میخواهید بدانید که آیا مؤلفه درحالحاضر فشرده یا کشیده میشود یا نه،
تنها کاری که باید انجام دهید این است که بررسی کنید آیا interactions خالی است یا نه:
val isPressedOrDragged = interactions.isNotEmpty()
اگر میخواهید بدانید آخرین تعامل چه بوده است، کافی است به آخرین مورد در فهرست نگاه کنید. برای مثال، پیادهسازی موج Compose به این صورت پوشش وضعیت مناسب برای استفاده در جدیدترین تعامل را تشخیص میدهد:
val lastInteraction = when (interactions.lastOrNull()) { is DragInteraction.Start -> "Dragged" is PressInteraction.Press -> "Pressed" else -> "No state" }
ازآنجاییکه همه Interactionها از ساختار یکسانی پیروی میکنند، هنگام کار با انواع مختلف تعاملات کاربر، تفاوت زیادی در کد وجود ندارد — الگوی کلی یکسان است.
توجه داشته باشید که مثالهای قبلی در این بخش نشاندهنده Flow تعاملها بااستفاده از State است
— این کار مشاهده مقادیر بهروزرسانیشده را آسان میکند،
زیرا خواندن مقدار وضعیت بهطور خودکار باعث بازآفرینی میشود. بااینحال،
ترکیب دستهای پیشاز قاب است. این یعنی اگر وضعیت تغییر کند و سپس در همان قاب به وضعیت قبلی برگردد، عناصر مشاهدهکننده وضعیت تغییر را نمیبینند.
این برای تعاملات مهم است، زیرا تعاملات میتوانند بهطور منظم در همان قاب شروع و پایان یابند. برای مثال، بااستفاده از مثال قبلی با Button:
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() Button(onClick = { /* do something */ }, interactionSource = interactionSource) { Text(if (isPressed) "Pressed!" else "Not pressed") }
اگر فشاری در همان قاب شروع و تمام شود، نوشتار هرگز بهصورت
"فشار داده شد!" نمایش داده نخواهد شد. در اکثر موارد، این یک مشکل نیست — نمایش یک جلوه بصری برای چنین مدت زمان کوتاهی منجر به سوسو زدن میشود و برای کاربر بسیار قابل توجه نخواهد بود. برای برخی موارد، مانند نمایش اثر موج یا پویانمایی مشابه، ممکن است بخواهید اثر را حداقل برای مدت زمان معینی نمایش دهید، بهجای اینکه اگر دکمه دیگر فشار داده نشد، بلافاصله متوقف شود. برای انجام این کار، میتوانید بهجای نوشتن در وضعیت، مستقیماً پویانماییها را از داخل لامبدای جمعآوری شروع و متوقف کنید. نمونهای از این الگو در بخش
ساختن Indication پیشرفته با قاب پویانماییشده وجود دارد.
مثال: ساختن عنصر با مدیریت تعامل سفارشی
برای دیدن اینکه چگونه میتوانید مؤلفهها را با پاسخ سفارشی به ورودی بسازید، در اینجا نمونهای از دکمه اصلاحشده ارائه شده است. در این مورد، فرض کنید دکمهای میخواهید که با تغییر ظاهرش به فشارها پاسخ دهد:
برای انجام این کار، براساس Button، عنصر ترکیبی سفارشی بسازید و آن را طوری تنظیم کنید که
پارامتر icon اضافی را برای رسم نماد (در این مورد، چرخ خرید) بگیرد. برای پیگیری اینکه آیا کاربر موشواره را روی دکمه نگه داشته است یا نه، collectIsPressedAsState() را فراخوانی میکنید؛ اگر نگه داشته باشد، نماد را اضافه میکنید. کد به این شکل است:
@Composable fun PressIconButton( onClick: () -> Unit, icon: @Composable () -> Unit, text: @Composable () -> Unit, modifier: Modifier = Modifier, interactionSource: MutableInteractionSource? = null ) { val isPressed = interactionSource?.collectIsPressedAsState()?.value ?: false Button( onClick = onClick, modifier = modifier, interactionSource = interactionSource ) { AnimatedVisibility(visible = isPressed) { if (isPressed) { Row { icon() Spacer(Modifier.size(ButtonDefaults.IconSpacing)) } } } text() } }
و در اینجا نحوه استفاده از آن عنصر ترکیبی جدید را میبینید:
PressIconButton( onClick = {}, icon = { Icon(Icons.Filled.ShoppingCart, contentDescription = null) }, text = { Text("Add to cart") } )
چون این PressIconButton جدید روی Button Material موجود ساخته شده است، به تعاملات کاربر به همه روشهای معمول واکنش نشان میدهد. وقتی کاربر دکمه را فشار میدهد، کدورت آن کمی تغییر میکند، درست مثل یک
Button Material معمولی.
ایجاد و اعمال جلوه سفارشی قابلاستفاده مجدد با Indication
در بخشهای قبلی، یاد گرفتید چگونه بخشی از یک عنصر را در پاسخ به Interactionهای مختلف تغییر دهید،
مثلاً وقتی دکمهای فشار داده میشود نمادی نشان داده شود. همین رویکرد را میتوان برای تغییر مقدار پارامترهایی که به یک عنصر ارائه میدهید، یا تغییر محتوای نمایشدادهشده در داخل یک عنصر استفاده کرد، اما این فقط در سطح هر عنصر قابلاعمال است. اغلب، یک برنامه یا سیستم طراحی
دارای یک سیستم عمومی برای جلوههای دیداری حالتدار است — جلوهای که باید
بهطور یکنواخت روی همه عناصر اعمال شود.
اگر درحال ساختن این نوع سیستم طراحی هستید، سفارشیسازی یک عنصر و استفاده مجدد از این سفارشیسازی برای عناصر دیگر میتواند به دلایل زیر دشوار باشد:
- هر عنصر در سیستم طراحی به کلیشه یکسانی نیاز دارد
- بهراحتی میتوان فراموش کرد که این جلوه را روی عناصر تازه ساختهشده و عناصر کلیککردنی سفارشی اعمال کنید
- ترکیب کردن جلوه سفارشی با جلوههای دیگر ممکن است دشوار باشد
برای جلوگیری از این مشکلات و مقیاسبندی آسان عنصر سفارشی در سراسر سیستم،
میتوانید از Indication استفاده کنید.
Indication نشاندهنده یک جلوه دیداری قابلاستفاده مجدد است که میتواند در سراسر
عناصر در یک برنامه یا سیستم طراحی اعمال شود. Indication به دو بخش تقسیم شده است:
IndicationNodeFactory: کارخانهای که نمونههایModifier.Nodeرا ایجاد میکند که جلوههای دیداری را برای یک عنصر پردازش میکنند. برای پیادهسازیهای سادهتری که در سراسر عناصر تغییر نمیکنند، این میتواند یک تکعنصری (شیء) باشد و در سراسر برنامه دوباره استفاده شود.این نمونهها میتوانند حالتدار یا بدون حالت باشند. ازآنجاییکه این متغیرها برای هر عنصر ایجاد میشوند، میتوانند مقادیر را از
CompositionLocalبازیابی کنند تا نحوه نمایش یا عملکرد آنها را در یک عنصر خاص تغییر دهند، همانند هرModifier.Nodeدیگر.Modifier.indication: اصلاحگری کهIndicationرا برای عنصر رسم میکند.Modifier.clickableو دیگر اصلاحکنندههای تعامل سطح بالا پارامتر نشانگر را مستقیماً میپذیرند، بنابراین نه تنهاInteractionرا منتشر میکنند، بلکه میتوانند جلوههای دیداری برایInteractionهایی که منتشر میکنند نیز رسم کنند. بنابراین، برای موارد ساده، میتوانید فقط ازModifier.clickableبدون نیاز بهModifier.indicationاستفاده کنید.
جایگزین کردن جلوه با Indication
این بخش نحوه جایگزین کردن جلوه مقیاس دستی اعمالشده روی یک دکمه خاص با یک نشانگر معادل را که میتواند در چندین عنصر استفاده مجدد شود شرح میدهد.
کد زیر دکمهای ایجاد میکند که با فشار دادن، بهسمت پایین مقیاسبندی میشود:
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() val scale by animateFloatAsState(targetValue = if (isPressed) 0.9f else 1f, label = "scale") Button( modifier = Modifier.scale(scale), onClick = { }, interactionSource = interactionSource ) { Text(if (isPressed) "Pressed!" else "Not pressed") }
برای تبدیل کردن جلوه مقیاس در گزیده بالا به Indication، این مراحل را دنبال کنید:
Modifier.Nodeمسئول اعمال جلوه مقیاس را ایجاد کنید. وقتی پیوست میشود، گره منبع تعامل را مشاهده میکند، مشابه مثالهای قبلی. تنها تفاوت در اینجا این است که بهجای تبدیل «تعاملهای» ورودی به وضعیت، مستقیماً پویانماییها را راهاندازی میکند.گره باید
DrawModifierNodeرا پیادهسازی کند تا بتواندContentDrawScope#draw()را ملغی کند و بااستفاده از همان دستورات رسم که در هر API گرافیکی دیگری در Compose استفاده میشود، جلوه مقیاس را پرداز کند.فراخوانی
drawContent()دردسترس از گیرندهContentDrawScopeعنصر واقعی را کهIndicationباید روی آن اعمال شود رسم میکند، بنابراین فقط باید این تابع را در یک تبدیل مقیاس فراخوانی کنید. مطمئن شوید پیادهسازیهایIndicationشما همیشه در نقطهایdrawContent()را فرا میخوانند؛ درغیراینصورت، عنصری کهIndicationرا روی آن اعمال میکنید رسم نخواهد شد.private class ScaleNode(private val interactionSource: InteractionSource) : Modifier.Node(), DrawModifierNode { var currentPressPosition: Offset = Offset.Zero val animatedScalePercent = Animatable(1f) private suspend fun animateToPressed(pressPosition: Offset) { currentPressPosition = pressPosition animatedScalePercent.animateTo(0.9f, spring()) } private suspend fun animateToResting() { animatedScalePercent.animateTo(1f, spring()) } override fun onAttach() { coroutineScope.launch { interactionSource.interactions.collectLatest { interaction -> when (interaction) { is PressInteraction.Press -> animateToPressed(interaction.pressPosition) is PressInteraction.Release -> animateToResting() is PressInteraction.Cancel -> animateToResting() } } } } override fun ContentDrawScope.draw() { scale( scale = animatedScalePercent.value, pivot = currentPressPosition ) { this@draw.drawContent() } } }
ایجاد
IndicationNodeFactory. تنها مسئولیت آن ایجاد یک نمونه گره جدید برای منبع تعامل ارائهشده است. ازآنجاییکه هیچ پارامتری برای پیکربندی نشانگر وجود ندارد، کارخانه میتواند یک شیء باشد:object ScaleIndication : IndicationNodeFactory { override fun create(interactionSource: InteractionSource): DelegatableNode { return ScaleNode(interactionSource) } override fun equals(other: Any?): Boolean = other === ScaleIndication override fun hashCode() = 100 }
Modifier.clickableبهصورت داخلی ازModifier.indicationاستفاده میکند، بنابراین برای ایجاد یک عنصر کلیککردنی باScaleIndication، تنها کاری که باید انجام دهید این است کهIndicationرا بهعنوان پارامتر بهclickableارائه دهید:Box( modifier = Modifier .size(100.dp) .clickable( onClick = {}, indication = ScaleIndication, interactionSource = null ) .background(Color.Blue), contentAlignment = Alignment.Center ) { Text("Hello!", color = Color.White) }
این کار همچنین ساختن مؤلفههای سطح بالا و قابلاستفاده مجدد را بااستفاده از سفارشی
Indicationآسان میکند — دکمه میتواند بهصورت زیر باشد:@Composable fun ScaleButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource? = null, shape: Shape = CircleShape, content: @Composable RowScope.() -> Unit ) { Row( modifier = modifier .defaultMinSize(minWidth = 76.dp, minHeight = 48.dp) .clickable( enabled = enabled, indication = ScaleIndication, interactionSource = interactionSource, onClick = onClick ) .border(width = 2.dp, color = Color.Blue, shape = shape) .padding(horizontal = 16.dp, vertical = 8.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, content = content ) }
سپس میتوانید از دکمه به روش زیر استفاده کنید:
ScaleButton(onClick = {}) { Icon(Icons.Filled.ShoppingCart, "") Spacer(Modifier.padding(10.dp)) Text(text = "Add to cart!") }
Indication سفارشی ساخته شده است.ساختن Indication پیشرفته با حاشیه پویانماییشده
Indication فقط به جلوههای تبدیل، مثل مقیاسبندی یک
عنصر، محدود نمیشود. چون IndicationNodeFactory یک Modifier.Node برمیگرداند، میتوانید
هر نوع جلوهای را بالای محتوا یا پایین آن رسم کنید، همانطور که با دیگر میاناهای برنامهسازی کاربردی رسم انجام میدهید. برای مثال، میتوانید وقتی عنصر فشرده میشود، یک قاب متحرک دور عنصر و یک رونهاد روی عنصر بکشید:
Indication کشیده شده است.پیادهسازی Indication در اینجا بسیار شبیه به مثال قبلی است —
فقط گرهی با چند پارامتر ایجاد میکند. ازآنجاییکه حاشیه پویانماییشده به شکل و حاشیه عنصری که Indication برای آن استفاده میشود بستگی دارد، پیادهسازی Indication نیز نیازمند ارائه شکل و پهنای حاشیه بهعنوان پارامتر است:
data class NeonIndication(private val shape: Shape, private val borderWidth: Dp) : IndicationNodeFactory { override fun create(interactionSource: InteractionSource): DelegatableNode { return NeonNode( shape, // Double the border size for a stronger press effect borderWidth * 2, interactionSource ) } }
پیادهسازی Modifier.Node نیز ازنظر مفهومی یکسان است، حتی اگر کد طراحی پیچیدهتر باشد. مانند قبل، InteractionSource
وقتی پیوست میشود، پویانماییها را راهاندازی میکند و DrawModifierNode را برای رسم
جلوه روی محتوا پیادهسازی میکند:
private class NeonNode( private val shape: Shape, private val borderWidth: Dp, private val interactionSource: InteractionSource ) : Modifier.Node(), DrawModifierNode { var currentPressPosition: Offset = Offset.Zero val animatedProgress = Animatable(0f) val animatedPressAlpha = Animatable(1f) var pressedAnimation: Job? = null var restingAnimation: Job? = null private suspend fun animateToPressed(pressPosition: Offset) { // Finish any existing animations, in case of a new press while we are still showing // an animation for a previous one restingAnimation?.cancel() pressedAnimation?.cancel() pressedAnimation = coroutineScope.launch { currentPressPosition = pressPosition animatedPressAlpha.snapTo(1f) animatedProgress.snapTo(0f) animatedProgress.animateTo(1f, tween(450)) } } private fun animateToResting() { restingAnimation = coroutineScope.launch { // Wait for the existing press animation to finish if it is still ongoing pressedAnimation?.join() animatedPressAlpha.animateTo(0f, tween(250)) animatedProgress.snapTo(0f) } } override fun onAttach() { coroutineScope.launch { interactionSource.interactions.collect { interaction -> when (interaction) { is PressInteraction.Press -> animateToPressed(interaction.pressPosition) is PressInteraction.Release -> animateToResting() is PressInteraction.Cancel -> animateToResting() } } } } override fun ContentDrawScope.draw() { val (startPosition, endPosition) = calculateGradientStartAndEndFromPressPosition( currentPressPosition, size ) val brush = animateBrush( startPosition = startPosition, endPosition = endPosition, progress = animatedProgress.value ) val alpha = animatedPressAlpha.value drawContent() val outline = shape.createOutline(size, layoutDirection, this) // Draw overlay on top of content drawOutline( outline = outline, brush = brush, alpha = alpha * 0.1f ) // Draw border on top of overlay drawOutline( outline = outline, brush = brush, alpha = alpha, style = Stroke(width = borderWidth.toPx()) ) } /** * Calculates a gradient start / end where start is the point on the bounding rectangle of * size [size] that intercepts with the line drawn from the center to [pressPosition], * and end is the intercept on the opposite end of that line. */ private fun calculateGradientStartAndEndFromPressPosition( pressPosition: Offset, size: Size ): Pair<Offset, Offset> { // Convert to offset from the center val offset = pressPosition - size.center // y = mx + c, c is 0, so just test for x and y to see where the intercept is val gradient = offset.y / offset.x // We are starting from the center, so halve the width and height - convert the sign // to match the offset val width = (size.width / 2f) * sign(offset.x) val height = (size.height / 2f) * sign(offset.y) val x = height / gradient val y = gradient * width // Figure out which intercept lies within bounds val intercept = if (abs(y) <= abs(height)) { Offset(width, y) } else { Offset(x, height) } // Convert back to offsets from 0,0 val start = intercept + size.center val end = Offset(size.width - start.x, size.height - start.y) return start to end } private fun animateBrush( startPosition: Offset, endPosition: Offset, progress: Float ): Brush { if (progress == 0f) return TransparentBrush // This is *expensive* - we are doing a lot of allocations on each animation frame. To // recreate a similar effect in a performant way, it would be better to create one large // gradient and translate it on each frame, instead of creating a whole new gradient // and shader. The current approach will be janky! val colorStops = buildList { when { progress < 1 / 6f -> { val adjustedProgress = progress * 6f add(0f to Blue) add(adjustedProgress to Color.Transparent) } progress < 2 / 6f -> { val adjustedProgress = (progress - 1 / 6f) * 6f add(0f to Purple) add(adjustedProgress * MaxBlueStop to Blue) add(adjustedProgress to Blue) add(1f to Color.Transparent) } progress < 3 / 6f -> { val adjustedProgress = (progress - 2 / 6f) * 6f add(0f to Pink) add(adjustedProgress * MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } progress < 4 / 6f -> { val adjustedProgress = (progress - 3 / 6f) * 6f add(0f to Orange) add(adjustedProgress * MaxPinkStop to Pink) add(MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } progress < 5 / 6f -> { val adjustedProgress = (progress - 4 / 6f) * 6f add(0f to Yellow) add(adjustedProgress * MaxOrangeStop to Orange) add(MaxPinkStop to Pink) add(MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } else -> { val adjustedProgress = (progress - 5 / 6f) * 6f add(0f to Yellow) add(adjustedProgress * MaxYellowStop to Yellow) add(MaxOrangeStop to Orange) add(MaxPinkStop to Pink) add(MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } } } return linearGradient( colorStops = colorStops.toTypedArray(), start = startPosition, end = endPosition ) } companion object { val TransparentBrush = SolidColor(Color.Transparent) val Blue = Color(0xFF30C0D8) val Purple = Color(0xFF7848A8) val Pink = Color(0xFFF03078) val Orange = Color(0xFFF07800) val Yellow = Color(0xFFF0D800) const val MaxYellowStop = 0.16f const val MaxOrangeStop = 0.33f const val MaxPinkStop = 0.5f const val MaxPurpleStop = 0.67f const val MaxBlueStop = 0.83f } }
تفاوت اصلی در اینجا این است که اکنون حداقل مدتی برای
پویانمایی با تابع animateToResting() وجود دارد، بنابراین حتی اگر فشار
بلافاصله رها شود، پویانمایی فشار ادامه خواهد یافت. همچنین در ابتدای animateToPressed، برای چند فشار سریع، مدیریت وجود دارد — اگر فشاری درطول فشار موجود یا پویانمایی استراحت رخ دهد، پویانمایی قبلی لغو میشود و پویانمایی فشار از ابتدا شروع میشود. برای پشتیبانی از چندین جلوه همزمان (مانند موجها، که در آن پویانمایی موج جدیدی روی موجهای دیگر رسم میشود)، میتوانید پویانماییها را در فهرستی پیگیری کنید، بهجای اینکه پویانماییهای موجود را لغو کنید و پویانماییهای جدیدی را شروع کنید.
توصیهشده برای شما
- توجه: نوشتار پیوند وقتی جاوا اسکریپت خاموش است نمایش داده میشود
- آشنایی با اشارهها
- Kotlin برای Jetpack Compose
- چیدمانها و عناصر «طراحی مواد»