مؤلفههای میانای کاربر ازطریق نحوه پاسخ دادن به تعاملهای کاربر، به کاربر دستگاه بازخورد میدهند. هر مؤلفه روش خاص خود را برای پاسخ دادن به تعاملات دارد، که به کاربر کمک میکند بداند تعاملاتش چه کاری انجام میدهند. برای مثال، اگر کاربر دکمهای را در صفحه لمسی دستگاه لمس کند، احتمالاً دکمه به نحوی تغییر میکند، مثلاً با اضافه شدن رنگ برجسته. این تغییر به کاربر اطلاع میدهد که دکمه را لمس کرده است. اگر کاربر نمیخواست این کار را انجام دهد، متوجه میشود که باید انگشتش را قبلاز رها کردن از روی دکمه بکشد، درغیراینصورت دکمه فعال میشود.
سند اشارهها در Compose نحوه مدیریت رویداد اشارهگر سطح پایین، مانند حرکتهای اشارهگر و کلیکها، توسط مؤلفههای 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 ساخته شده است، بنابراین میتوانید تعاملات را از آن به همان روشی که با هر جریان دیگری کار میکنید جمعآوری کنید. برای اطلاعات بیشتر درباره این تصمیم طراحی،
پست وبلاگ Illuminating Interactions را ببینید.
وضعیت تعامل
شاید بخواهید عملکرد داخلی مؤلفههایتان را با ردیابی تعاملات توسط خودتان گسترش دهید. برای مثال، شاید بخواهید دکمهای هنگام فشار دادن تغییر رنگ دهد. سادهترین راه برای ردیابی تعاملها،
مشاهده وضعیت تعامل مناسب است. 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()، Compose این موارد را نیز ارائه میدهد:
collectIsFocusedAsState()، collectIsDraggedAsState()، و
collectIsHoveredAsState(). این روشها درواقع روشهای راحتی هستند
که روی میاناهای برنامهسازی کاربردی InteractionSource سطح پایینتر ساخته شدهاند. در برخی موارد، ممکن است بخواهید مستقیماً از آن عملکردهای سطح پایینتر استفاده کنید.
برای مثال، فرض کنید باید بدانید که آیا دکمهای فشار داده میشود و
همچنین آیا کشیده میشود یا نه. اگر از هر دو collectIsPressedAsState()
و collectIsDraggedAsState() استفاده کنید، Compose کارهای تکراری زیادی انجام میدهد و
هیچ تضمینی وجود ندارد که همه تعاملها را بهترتیب درست دریافت کنید. برای
موقعیتهایی مثل این، شاید بخواهید مستقیماً با
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 سطح بالا روی بلوکهای ساختمانی پایهای ساخته میشوند که Interactions موردنیاز برای کنترل موجها و سایر جلوههای بصری را تولید میکنند. کتابخانه پایه اصلاحکنندههای تعامل سطح بالا را ارائه میدهد
مانند 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 است — این کار مشاهده مقادیر بهروزشده را آسان میکند، زیرا خواندن مقدار وضعیت بهطور خودکار باعث بازترکیب میشود. بااینحال،
ترکیببندی بهصورت پیشفریم دستهبندی میشود. This means that if the state changes, and
then changes back within the same frame, components observing the state won't
see the change.
این امر برای تعاملات مهم است، زیرا تعاملات میتوانند بهطور منظم در یک قاب شروع و پایان یابند. برای مثال، بااستفاده از مثال قبلی با 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 جدید روی Material Button موجود ساخته شده است، به تعاملات کاربر به همه روشهای معمول واکنش نشان میدهد. وقتی کاربر
دکمه را فشار میدهد، کدری آن کمی تغییر میکند، درست مثل Material Button
معمولی.
ایجاد و اعمال جلوه سفارشی بااستفاده از 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()را لغو کند و جلوه مقیاس را بااستفاده از همان فرمانهای رسم مانند هر میانای برنامهسازی کاربردی گرافیکی دیگر در 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را ایجاد کنید. تنها مسئولیت آن ایجاد یک نمونه گره جدید برای منبع تعامل ارائهشده است. As there are no parameters to configure the indication, the factory can be an object: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 در اینجا بسیار شبیه به مثال قبلی است —
فقط یک گره با چند پارامتر ایجاد میکند. Since the animated border depends
on the shape and the border of the component the Indication is used for, the
Indication implementation also requires shape and border width to be provided
as parameters:
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
- مؤلفهها و چیدمانهای Material