اجزای رابط کاربری با نحوه پاسخگویی به تعاملات کاربر به کاربر دستگاه بازخورد می دهند. هر جزء روش خاص خود را برای پاسخ به تعاملات دارد، که به کاربر کمک می کند تا بداند تعاملاتش چه می کنند. به عنوان مثال، اگر کاربر دکمه ای را روی صفحه لمسی دستگاه لمس کند، احتمالاً دکمه به نوعی تغییر می کند، شاید با افزودن یک رنگ برجسته. این تغییر به کاربر اطلاع می دهد که دکمه را لمس کرده است. اگر کاربر نمیخواست این کار را انجام دهد، میداند که قبل از رها کردن دکمه، انگشت خود را از آن دور کند - در غیر این صورت، دکمه فعال میشود.
مستندات Compose Gestures نحوه برخورد مؤلفههای Compose را با رویداد نشانگر سطح پایین، مانند حرکت اشارهگر و کلیکها، پوشش میدهد. خارج از جعبه، Compose آن رویدادهای سطح پایین را به تعاملات سطح بالاتر انتزاع می کند – برای مثال، یک سری رویدادهای اشاره گر ممکن است به یک دکمه فشار داده و رها شود. درک این انتزاعات سطح بالاتر می تواند به شما کمک کند نحوه پاسخ UI خود را به کاربر سفارشی کنید. به عنوان مثال، ممکن است بخواهید نحوه تغییر ظاهر یک مؤلفه را هنگام تعامل کاربر با آن سفارشی کنید، یا شاید فقط بخواهید گزارشی از آن اقدامات کاربر را حفظ کنید. این سند اطلاعاتی را در اختیار شما میگذارد که برای اصلاح عناصر رابط کاربری استاندارد یا طراحی خودتان نیاز دارید.
تعاملات
در بسیاری از موارد، لازم نیست بدانید که مؤلفه Compose چگونه تعاملات کاربر را تفسیر می کند. برای مثال، Button
به Modifier.clickable
متکی است تا بفهمد آیا کاربر روی دکمه کلیک کرده است یا خیر. اگر یک دکمه معمولی را به برنامه خود اضافه می کنید، می توانید کد onClick
دکمه را تعریف کنید و Modifier.clickable
آن کد را در صورت لزوم اجرا می کند. این بدان معنی است که شما نیازی به دانستن اینکه آیا کاربر روی صفحه ضربه زده است یا دکمه را با صفحه کلید انتخاب کرده است. Modifier.clickable
متوجه می شود که کاربر یک کلیک انجام داده است و با اجرای کد onClick
شما پاسخ می دهد.
با این حال، اگر میخواهید پاسخ مؤلفه رابط کاربری خود را به رفتار کاربر سفارشی کنید، ممکن است لازم باشد اطلاعات بیشتری در مورد آنچه در زیر کاپوت میگذرد بدانید. این بخش بخشی از این اطلاعات را در اختیار شما قرار می دهد.
هنگامی که یک کاربر با یک مؤلفه UI تعامل می کند، سیستم رفتار آنها را با ایجاد تعدادی رویداد Interaction
نشان می دهد. برای مثال، اگر کاربر دکمهای را لمس کند، دکمه PressInteraction.Press
را ایجاد میکند. اگر کاربر انگشت خود را در داخل دکمه بلند کند، یک PressInteraction.Release
ایجاد میکند که به دکمه اطلاع میدهد که کلیک تمام شده است. از سوی دیگر، اگر کاربر انگشت خود را به بیرون از دکمه بکشد، سپس انگشت خود را بلند کند، دکمه PressInteraction.Cancel
ایجاد می کند، تا نشان دهد که فشار روی دکمه لغو شده است، نه کامل شده است.
این تعاملات بدون نظر است. یعنی این رویدادهای تعامل سطح پایین قصد تفسیر معنای اقدامات کاربر یا توالی آنها را ندارند. آنها همچنین تفسیر نمی کنند که کدام اقدامات کاربر ممکن است بر سایر اقدامات اولویت داشته باشد.
این فعل و انفعالات عموماً به صورت جفت و با یک شروع و یک پایان انجام می شود. تعامل دوم شامل ارجاع به اولی است. برای مثال، اگر کاربر دکمهای را لمس کند و سپس انگشت خود را بلند کند، لمس یک PressInteraction.Release
PressInteraction.Press
ایجاد میکند. 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()
، Compose همچنین collectIsFocusedAsState()
، collectIsDraggedAsState()
و collectIsHoveredAsState()
را ارائه می دهد. این روش ها در واقع روش های راحتی هستند که بر روی API های سطح پایین تر InteractionSource
ساخته شده اند. در برخی موارد، ممکن است بخواهید مستقیماً از آن توابع سطح پایین استفاده کنید.
برای مثال، فرض کنید باید بدانید که آیا یک دکمه در حال فشار دادن است یا خیر، و همچنین اینکه آیا در حال کشیدن است یا خیر. اگر از هر دو collectIsPressedAsState()
و collectIsDraggedAsState()
استفاده می کنید، Compose کارهای تکراری زیادی انجام می دهد و هیچ تضمینی وجود ندارد که تمام تعاملات را به ترتیب درست انجام دهید. برای شرایطی مانند این، ممکن است بخواهید مستقیماً با InteractionSource
کار کنید. برای اطلاعات بیشتر در مورد ردیابی تعاملات خود با InteractionSource
، به کار با InteractionSource
مراجعه کنید.
بخش زیر نحوه مصرف و انتشار تعاملات را به ترتیب با InteractionSource
و MutableInteractionSource
شرح می دهد.
Consume and Emit 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
در هنگام شناور یا بازکردن آن استفاده کند.
اجزایی بسازید که مصرف و تولید کنند
اجزای سطح بالا مانند 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 از رویکرد معماری لایهای پیروی میکند، بنابراین اجزای متریال سطح بالا بر روی بلوکهای ساختمانی پایه ساخته میشوند که Interaction
را که برای کنترل موجها و سایر جلوههای بصری نیاز دارند، ایجاد میکنند. کتابخانه پایه اصلاح کننده های تعامل سطح بالایی مانند Modifier.hoverable
، Modifier.focusable
و Modifier.draggable
را ارائه می دهد.
برای ساختن کامپوننتی که به رویدادهای شناور پاسخ می دهد، می توانید به سادگی از Modifier.hoverable
استفاده کنید و یک MutableInteractionSource
را به عنوان پارامتر ارسال کنید. هر زمان که کامپوننت شناور می شود، HoverInteraction
s را منتشر می کند، و می توانید از آن برای تغییر نحوه ظاهر شدن کامپوننت استفاده کنید.
// 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
برای ایجاد مؤلفهای استفاده کنید که تعاملات شناور، فوکوس و فشار را بدون نیاز به ترکیب APIهای سطح پایینتر انجام میدهد. اگر میخواهید کامپوننت خود را نیز قابل کلیک کنید، میتوانید 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
کار کنید
اگر به اطلاعات سطح پایینی درباره تعامل با یک مؤلفه نیاز دارید، میتوانید از APIهای جریان استاندارد برای 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") }
اگر فشاری در همان قاب شروع و به پایان برسد، متن هرگز به صورت "فشرده شده!" نمایش داده نمی شود. در بیشتر موارد، این یک مشکل نیست - نشان دادن یک جلوه بصری برای چنین مدت زمان کمی منجر به سوسو زدن می شود و برای کاربر چندان قابل توجه نخواهد بود. برای برخی موارد، مانند نمایش یک افکت موج دار یا انیمیشن مشابه، ممکن است بخواهید جلوه را حداقل برای مدت زمان کمتری نشان دهید، به جای اینکه اگر دکمه دیگر فشار داده نشد، فوراً متوقف شود. برای انجام این کار، میتوانید بهجای نوشتن در حالت، مستقیماً انیمیشنها را از داخل مجموعه لامبدا شروع و متوقف کنید. نمونه ای از این الگو در قسمت Build an advanced Indication
with animated border وجود دارد.
مثال: ساخت مؤلفه با مدیریت تعامل سفارشی
برای اینکه ببینید چگونه می توانید کامپوننت ها را با یک پاسخ سفارشی به ورودی بسازید، در اینجا نمونه ای از یک دکمه تغییر یافته آورده شده است. در این حالت، فرض کنید دکمهای را میخواهید که با تغییر ظاهر به فشارها پاسخ دهد:
برای انجام این کار، یک ترکیب سفارشی بر اساس 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() } }
و در اینجا به نظر می رسد که استفاده از آن composable جدید چگونه است:
PressIconButton( onClick = {}, icon = { Icon(Icons.Filled.ShoppingCart, contentDescription = null) }, text = { Text("Add to cart") } )
از آنجایی که این PressIconButton
جدید بر روی Button
مواد موجود ساخته شده است، به تمام روش های معمول به تعاملات کاربر واکنش نشان می دهد. هنگامی که کاربر دکمه را فشار می دهد، کمی شفافیت آن را تغییر می دهد، درست مانند یک Button
معمولی Material.
یک افکت سفارشی قابل استفاده مجدد با Indication
ایجاد و اعمال کنید
در بخشهای قبلی، یاد گرفتید که چگونه بخشی از یک مؤلفه را در پاسخ به Interaction
مختلف تغییر دهید، مانند نشان دادن یک نماد هنگام فشار دادن. همین رویکرد را می توان برای تغییر مقدار پارامترهایی که به یک مؤلفه ارائه می دهید یا تغییر محتوای نمایش داده شده در یک مؤلفه استفاده کرد، اما این فقط بر اساس هر مؤلفه قابل اعمال است. اغلب، یک برنامه کاربردی یا سیستم طراحی دارای یک سیستم عمومی برای جلوههای بصری حالت است - افکتی که باید روی همه اجزا به شیوهای ثابت اعمال شود.
اگر در حال ساخت این نوع سیستم طراحی هستید، سفارشی کردن یک جزء و استفاده مجدد از این سفارشی سازی برای سایر اجزا به دلایل زیر می تواند دشوار باشد:
- هر جزء در سیستم طراحی نیاز به یک دیگ بخار دارد
- به راحتی می توان فراموش کرد که این افکت را روی اجزای تازه ساخته شده و اجزای قابل کلیک سفارشی اعمال کنید
- ممکن است ترکیب افکت سفارشی با جلوه های دیگر دشوار باشد
برای جلوگیری از این مشکلات و مقیاس بندی آسان یک مؤلفه سفارشی در سراسر سیستم خود، می توانید از Indication
استفاده کنید. Indication
یک جلوه بصری قابل استفاده مجدد را نشان می دهد که می تواند در اجزای یک برنامه کاربردی یا سیستم طراحی اعمال شود. Indication
به دو بخش تقسیم می شود:
IndicationNodeFactory
: کارخانه ای که نمونه هایModifier.Node
را ایجاد می کند که جلوه های بصری را برای یک جزء ارائه می دهد. برای پیادهسازیهای سادهتر که در اجزای مختلف تغییر نمیکنند، این میتواند یک تک تن (شیء) باشد و در کل برنامه مجددا استفاده شود.این موارد می توانند حالت دار یا بدون تابعیت باشند. از آنجایی که آنها برای هر کامپوننت ایجاد می شوند، می توانند مقادیر را از
CompositionLocal
بازیابی کنند تا نحوه ظاهر یا رفتار آنها در داخل یک جزء خاص را تغییر دهند، مانند هرModifier.Node
دیگر.Modifier.indication
: اصلاح کننده ای کهIndication
برای یک جزء ترسیم می کند.Modifier.clickable
و دیگر اصلاحکنندههای تعامل سطح بالا مستقیماً یک پارامتر نشاندهنده را میپذیرند، بنابراین نه تنهاInteraction
را منتشر میکنند، بلکه میتوانند جلوههای بصری را برایInteraction
هایی که منتشر میکنند نیز ترسیم کنند. بنابراین، برای موارد ساده، میتوانید بدون نیاز بهModifier.indication
ازModifier.clickable
استفاده کنید.
اثر را با یک 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
را ایجاد کنید که مسئول اعمال افکت مقیاس است . هنگام اتصال، گره منبع تعامل را مشاهده می کند، مشابه نمونه های قبلی. تنها تفاوت در اینجا این است که مستقیماً انیمیشن ها را به جای تبدیل Interaction های ورودی به حالت راه اندازی می کند.گره باید
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
فقط به اثرات تبدیل، مانند مقیاس بندی یک جزء محدود نمی شود. از آنجایی که IndicationNodeFactory
یک Modifier.Node
برمی گرداند، می توانید هر نوع افکتی را در بالا یا پایین محتوا مانند سایر API های طراحی بکشید. به عنوان مثال، می توانید یک حاشیه متحرک در اطراف کامپوننت و یک پوشش در بالای کامپوننت هنگام فشار دادن آن بکشید:
پیاده سازی 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
- اجزای متریال و طرحبندی