امکانپذیر کردن کشیدن فیزیکی مؤلفهها در سراسر صفحهنمایش برای کاربران، یکی از اصول اساسی برنامههای مدرن Android است. چه درحال ساختن لغزنده سفارشی، مؤلفه تند کشیدن برای رد کردن، یا بوم تعاملی باشید، Jetpack Compose مجموعه جامعی از میاناهای برنامهسازی کاربردی را برای مدیریت آسان رویدادهای کشیدن ارائه میدهد.
میاناهای برنامهسازی کاربردی کشیدن Compose پیچیدگی رویدادهای لمسی خام را انتزاعی میکنند.
بهجای محاسبه دستی مختصات لمس و مدیریت چرخههای حیات اشاره با pointerInput، میتوانید از اصلاحکنندههای داخلی استفاده کنید که مستقیماً به سیستمهای پویانمایی و مدیریت وضعیت Compose متصل میشوند.
این صفحه نحوه و زمان استفاده از هر API کشیدن را شرح میدهد.
انتخاب کردن API کشیدن درست
چندین میانای برنامهسازی کاربردی وجود دارد که میتوانید برای پیادهسازی محتوای کشیدنی استفاده کنید.
- مؤلفههای استاندارد Material (مانند
SwipeToDismissBoxیا کشوهای پیمایش) را زمانی انتخاب کنید که: به رفتار پیشفرض تند کشیدن یا کشیدن Material 3 نیاز دارید (برای مثال، حذف ایمیل، بایگانی کردن تکلیف، یا باز کردن منو کناری / برگههای پایین). این عناصر رفتارهای داخلی را بدون نیاز به مدیریت وضعیت یا چیدمان سفارشی ارائه میدهند. - انتخاب
Modifier.draggableزمان: فقط به دلتاهای کشیدن خام نیاز دارید (برای مثال، کشیدن لغزنده سفارشی). برای اطلاعات بیشتر، به کشیدن در امتداد یک محور با draggable مراجعه کنید.Modifier.draggable2Dچه زمانی را انتخاب کنید: به مؤلفهای نیاز دارید که بهطور مداوم در هر دو محور X و Y بهطور همزمان حرکت کند (برای مثال، حرکت در سراسر نقشه یا ایجاد دکمه کنش شناور که کاربر بتواند آن را به هرجایی از صفحه بکشد). برای اطلاعات بیشتر، کشیدن در دو بعد باModifier.draggable2Dرا ببینید.
-
Modifier.anchoredDraggableرا زمانی انتخاب کنید که: درحال ساختن مؤلفههای تعاملی سفارشی هستید که باید بین دو یا چند موقعیت مجزا جابهجا شوند (برای مثال، برگه پایین، کشوی پیمایش، یا کنش تند کشیدن برای آشکار کردن). برای اطلاعات بیشتر، کشیدن مؤلفهها بین نقاط لنگر باanchoredDraggableرا ببینید.
کشیدن در امتداد یک محور با draggable
اصلاحکننده draggable نقطه ورود سطح بالا برای کشیدن اشارهها در
یک جهت است و فاصله کشیدن را برحسب پیکسل گزارش میکند.
توجه به این نکته مهم است که این اصلاحکننده شبیه به scrollable است، زیرا
فقط اشاره را تشخیص میدهد. باید وضعیت را نگه دارید و آن را روی صفحهنمایش نشان دهید،
برای مثال، با جابهجا کردن عنصر ازطریق اصلاحکننده offset:
@Composable private fun DraggableText() { var offsetX by remember { mutableFloatStateOf(0f) } Text( modifier = Modifier .offset { IntOffset(offsetX.roundToInt(), 0) } .draggable( orientation = Orientation.Horizontal, state = rememberDraggableState { delta -> offsetX += delta } ), text = "Drag me!" ) }
اگر لازم است کل اشاره کشیدن را کنترل کنید، استفاده از آشکارساز اشاره کشیدن را بهجای آن ازطریق اصلاحکننده pointerInput درنظر بگیرید.
@Composable private fun DraggableTextLowLevel() { Box(modifier = Modifier.fillMaxSize()) { var offsetX by remember { mutableFloatStateOf(0f) } var offsetY by remember { mutableFloatStateOf(0f) } Box( Modifier .offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) } .background(Color.Blue) .size(50.dp) .pointerInput(Unit) { detectDragGestures { change, dragAmount -> change.consume() offsetX += dragAmount.x offsetY += dragAmount.y } } ) } }
کشیدن مؤلفهها بین نقاط لنگر با anchoredDraggable
اصلاحکننده anchoredDraggable به شما امکان میدهد مؤلفهای را بین مجموعهای از
موقعیتهای ازپیش تعریفشده بهنام نقاط لنگر بکشید. وقتی کاربر اشاره کشیدن را رها میکند، مؤلفه در نزدیکترین لنگر قرار میگیرد. anchoredDraggable برای ایجاد مؤلفههایی مانند برگههای پایین سفارشی، لغزندههای پیچیده، یا پانلهای کشویی مفید است.
سطح میانای برنامهسازی کاربردی
AnchoredDraggable API از بخشهای اصلی زیر تشکیل شده است:
- لنگرها (
DraggableAnchors): سازندهای که نگاشت بین حالتهای سفارشی شما (مثلاً کلاس enum برایStart, Center, End) و مقادیر آفست پیکسل مربوطه را تعریف میکند. - State (
AnchoredDraggableState<T>): نگهدارنده حالتی که موقعیت فعلی مؤلفه (انحراف)، لنگری که در آن قرار گرفته است، و پیشرفت کشیدن آن را مدیریت میکند. باید بااستفاده ازrememberSaveableوAnchoredDraggableState.Saver<T>ایجاد شود تا وضعیت آن در تغییرات پیکربندی حفظ شود. - اصلاحکننده اشاره (
Modifier.anchoredDraggable): اصلاحکنندهای که اشارههای کشیدن را درطولOrientationشناسایی میکند، وضعیت را با دلتای کشیدن بهروز میکند، و اشاره پرتاب کردن را برای پویانمایی کردن مؤلفه به نزدیکترین لنگر هنگام رها کردن مدیریت میکند.
مثال: ساختن مؤلفه SwipeToDismiss سفارشی
این مثال نشان میدهد که چگونه میتوان یک مؤلفه SwipeToDismiss سفارشی (یک برنمای سفید) ساخت که کاربر بتواند با تند کشیدن به چپ آن را رد کند یا با کشیدن به راست، جلوه کشیدگی فراتومار را ببیند.
مقادیر لنگر را تعریف کنید. یک enum تعریف کنید که نشاندهنده موقعیتهای گسستهای باشد که محتوای کشیدنی میتواند در آنها قرار گیرد:
private enum class AnchoredDraggableSwipeDismissValue { DismissedStart, Resting }
نمونهسازی و ذخیره
AnchoredDraggableState. ازrememberSaveableباAnchoredDraggableState.Saver()استفاده کنید تا موقعیت فعلی مؤلفه در سراسر بازسازی فعالیت و تغییرات پیکربندی (مثلاً چرخش صفحهنمایش یا تغییرات زمینه) حفظ شود:val state = rememberSaveable(saver = AnchoredDraggableState.Saver()) { AnchoredDraggableState(initialValue = AnchoredDraggableSwipeDismissValue.Resting) }
اصلاحکنندههای
layout،offset، وdraggableرا اعمال کنید. برای اینکه مؤلفه کشیدنی شود و اطمینان حاصل شود که نقاط لنگر آن با اندازهاش سازگار است، اصلاحکنندههای زیر را روی محتوایی که باید بهصورت بصری حرکت کند اعمال کنید:Modifier.layout: موقعیتهای لنگر اغلب به اندازه مؤلفه بستگی دارد که درطول ترکیببندی اولیه مشخص نیست. از اصلاحکنندهlayoutبرای اندازهگیری مؤلفه استفاده کنید، نقشهDraggableAnchorsرا بهطور پویا براساس عرض آن محاسبه کنید، و وضعیت را بااستفاده ازstate.updateAnchors()بهروز کنید.
Modifier.anchoredDraggable: به وضعیت متصل میشود، دلتاهای کشیدن را در جهت مشخصشده ارسال میکند، و اشاره حرکت سریع را برای پویانمایی مؤلفه به نزدیکترین لنگر هنگام رها شدن مدیریت میکند. این اصلاحکننده را به مؤلفهای که باید به اشارههای کشیدن پاسخ دهد اضافه کنید وstateوorientationکشیدن (و بهصورت اختیاریoverscrollEffect) را به آن ارسال کنید.-
Modifier.offset: آفست را ازstate.requireOffset()درون آفست لامبدا میخواند. خواندن آفست در این لامبدا تضمین میکند که مقدار فقط درطول فاز جایگذاری خوانده میشود و از بازترکیبهای غیرضروری درطول کشیدن جلوگیری میکند. این اصلاحکننده را روی محتوایی که باید بهصورت بصری حرکت کند اعمال کنید.
Box( modifier = Modifier .fillMaxWidth() // Read state.requireOffset() inside the offset lambda to defer reading to the placement phase .offset { IntOffset(state.requireOffset().roundToInt(), 0) } .layout { measurable, constraints -> val placeable = measurable.measure(constraints) val anchors = DraggableAnchors { AnchoredDraggableSwipeDismissValue.DismissedStart at -placeable.width.toFloat() AnchoredDraggableSwipeDismissValue.Resting at 0f } state.updateAnchors(anchors) layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) } } // Pass overscrollEffect to anchoredDraggable so deltas beyond the bounds (min and max anchor) are dispatched to overscrollEffect, and render the visual effect with Modifier.overscroll. .overscroll(overscrollEffect) .anchoredDraggable( state, Orientation.Horizontal, overscrollEffect = overscrollEffect ) ) { dismissContent() }
[اختیاری] ادغام کردن پیمایش بیشازحد.
overscrollEffectAPI جلوه دیداریای را پیادهسازی میکند که وقتی با پیمایش یا پرتاب به لبههای محفظه پیمایش میرسید نمایش داده میشود. میتوانید بازخورد بصری ارائه دهید وقتی کاربر با جلوه فراتر از پیمایش (معمولاً کشش یا درخشش) از مرزهای لنگر تعریفشده فراتر میرود.- Dispatch: Pass an
overscrollEffectinstance to theanchoredDraggablemodifier. این کار بسته به اینکه اشاره داخل یا خارج از مرزهای لنگر باشد، دلتاهای کشیدن را به حالتAnchoredDraggableیا جلوه پیمایش بیشازحد ارسال میکند. - Render:
Modifier.overscrollرا با همان نمونهOverscrollEffectروی مؤلفهای که باید جلوه را بهصورت دیداری رندر کند اعمال کنید.
- Dispatch: Pass an
قطعه کد زیر نحوه افزودن جلوه کشیدگی استاندارد را نشان میدهد:
val overscrollEffect = rememberOverscrollEffect() Box( modifier.fillMaxWidth() ) { Box(Modifier.fillMaxWidth()) { background() } Box( modifier = Modifier .fillMaxWidth() // Read state.requireOffset() inside the offset lambda to defer reading to the placement phase .offset { IntOffset(state.requireOffset().roundToInt(), 0) } .layout { measurable, constraints -> val placeable = measurable.measure(constraints) val anchors = DraggableAnchors { AnchoredDraggableSwipeDismissValue.DismissedStart at -placeable.width.toFloat() AnchoredDraggableSwipeDismissValue.Resting at 0f } state.updateAnchors(anchors) layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) } } // Pass overscrollEffect to anchoredDraggable so deltas beyond the bounds (min and max anchor) are dispatched to overscrollEffect, and render the visual effect with Modifier.overscroll. .overscroll(overscrollEffect) .anchoredDraggable( state, Orientation.Horizontal, overscrollEffect = overscrollEffect ) ) { dismissContent() } }
نتیجه
تکهکد قبلی منجر به یک برنمای سفید با متن «برای بستن من را بکشید!» میشود که کاربر میتواند برای بستن آن، انگشتش را تند به چپ بکشد (یا برای دیدن جلوه کشش فراتر از پیمایش، آن را به راست بکشد):
مثال: ایجاد کشوی پیمایش سفارشی مبتنی بر اشاره
این مثال نشان میدهد که چگونه یک کشوی پیمایش سفارشی و مبتنی بر اشاره بسازید. وقتی کاربر بهصورت افقی میکشد، صفحه پیشزمینه (ScreenContents)
به راست میلغزد، از 1.0f به 0.8f کوچک میشود، و گوشههای آن گرد میشود
تا کشوی پیمایش زیرین نمایان شود.
حالتها و موقعیتهای لنگر را تعریف کنید. در این مثال، لنگرها حالتهای باز و بسته کشو هستند. یک enum برای حالتهای
OpenوClosedتعریف کنید و آنها را بااستفاده ازDraggableAnchorsبه آفستهای پیکسل نگاشت کنید:enum class DrawerState { Open, Closed } val anchors = DraggableAnchors { DrawerState.Open at drawerWidth DrawerState.Closed at 0f } val state = remember { AnchoredDraggableState( initialValue = DrawerState.Closed, anchors = anchors, ) }
پویانمایی کردن
anchoredDraggableStateبا کلیک کردن روی یک مورد.HomeScreenDrawerرا در پایینBoxوScreenContentsرا در بالا قرار دهید. کشوی ثابت میماند، درحالیکه لایه بالایی ترجمه میشود و روی آن مقیاسبندی میشود.وقتی کاربر موردی را در کشو انتخاب میکند، وضعیت صفحه فعال را بهروز کنید و کشو را بهطور خودکار بااستفاده از
state.animateTo(DrawerState.Closed)ببندید:@Composable private fun AnchoredDraggableDrawerOnClickSample( modifier: Modifier = Modifier, state: AnchoredDraggableState<DrawerState>, coroutineScope: CoroutineScope ) { var screenState by remember { mutableStateOf(ScreenState.Home) } Box(modifier = modifier.fillMaxSize()) { HomeScreenDrawer( selectedScreen = screenState, onScreenSelected = { screen -> screenState = screen // Dismiss drawer when a destination is selected coroutineScope.launch { state.animateTo(DrawerState.Closed) } } ) // ScreenContents(...) } }
از
state.requireOffset()برای تغییر دادن چندین ویژگی دیداری استفاده کنید. بهجای اینکه فقط محتوا را جابهجا کنید، میتوانید چندین ویژگی را باModifier.graphicsLayerتغییر دهید تا بهطور همزمان انتقال، مقیاس، و شعاع گوشه را براساس پیشرفت کشیدن پویانمایی کنید:- از
state.requireOffset()برای دریافت افست کشیدن فعلی استفاده کنید. - از
lerp()(درونیابی خطی) برای محاسبه مقیاس تناسبی (1fتا0.8f) و شعاع گوشه (0.dpتا32.dp) نسبت به انحراف و عرض کشو استفاده کنید. Modifier.graphicsLayerتغییرات را در فاز رسم اجرا میکند و پویانماییهای با عملکرد بالا را بدون راهاندازی بازترکیبها درطول کشیدن تضمین میکند.
@Composable private fun AnchoredDraggableDrawerGraphicsLayerSample( state: AnchoredDraggableState<DrawerState>, drawerWidth: Float, selectedScreen: ScreenState, onDrawerClicked: () -> Unit ) { ScreenContents( selectedScreen = selectedScreen, onDrawerClicked = onDrawerClicked, modifier = Modifier .graphicsLayer { this.translationX = state.requireOffset() val scale = lerp(1f, 0.8f, state.requireOffset() / drawerWidth) this.scaleX = scale this.scaleY = scale val cornerRadius = lerp(0f, 32f, state.requireOffset() / drawerWidth) this.shape = RoundedCornerShape(cornerRadius.dp) this.clip = true } .anchoredDraggable(state, Orientation.Horizontal) ) }
- از
راهاندازی پویانماییهای برنامهای با
animateTo. با راهاندازیstate.animateTo()در محدوده کوروتین، کشو را بهصورت برنامهنویسیشده (مثلاً هنگام تکضرب زدن روی نماد منو یا انتخاب کردن موردی در کشو) کنترل کنید:fun toggleDrawerState( state: AnchoredDraggableState<DrawerState>, coroutineScope: CoroutineScope ) { coroutineScope.launch { if (state.currentValue == DrawerState.Open) { state.animateTo(DrawerState.Closed) } else { state.animateTo(DrawerState.Open) } } }
نتیجه
کشیدن در دو بعد با Modifier.draggable2D
از اصلاحکننده draggable2D برای جابهجا کردن عناصر میانای کاربر بهطور همزمان در هر دو محور X و Y استفاده کنید.
عنصر قابلترکیبی را بکشید
این مثال رایجترین مورد استفاده برای draggable2D را نشان میدهد — به کاربر اجازه میدهد
یک عنصر میانای کاربر را انتخاب کند و آن را در هر جایی از محفظه ولی قرار دهد.
@Composable private fun DraggableComposableElement() { // 1. Track the position of the floating window var offset by remember { mutableStateOf(Offset.Zero) } Box(modifier = Modifier.fillMaxSize().background(Color(0xFFF5F5F5))) { Box( modifier = Modifier // 2. Apply the offset to the box's position .offset { IntOffset(offset.x.roundToInt(), offset.y.roundToInt()) } // ... // 3. Attach the 2D drag logic .draggable2D( state = rememberDraggable2DState { delta -> // 4. Update the position based on the movement delta offset += delta } ), contentAlignment = Alignment.Center ) { Text("Video Preview", color = Color.White, fontSize = 12.sp) } } }
تکهکد قبلی شامل موارد زیر است:
- موقعیت جعبه را بااستفاده از وضعیت
offsetردیابی میکند. - از اصلاحکننده
offsetبرای تغییر موقعیت مؤلفه براساس دلتاهای کشیدن استفاده میکند. - ازآنجاییکه از حرکت سریع پشتیبانی نمیشود، جعبه بهمحض اینکه کاربر انگشتش را برمیدارد از حرکت میایستد.
کشیدن یک ترکیبپذیر فرزند براساس ناحیه کشیدن والد
این مثال نشان میدهد که چگونه از draggable2D برای ایجاد یک ناحیه ورودی دوبعدی استفاده کنید
که در آن یک دکمه انتخابگر در یک سطح خاص محدود شده است. برخلاف
مثال عنصر کشیدنی که خود مؤلفه را جابهجا میکند، این پیادهسازی
از دلتاهای دوبعدی برای جابهجا کردن «انتخابگر» ترکیبشدنی فرزند در انتخابگر رنگ استفاده میکند:
@Composable private fun ExampleColorSelector( // ... ) { // 1. Maintain the 2D position of the selector in state. var selectorOffset by remember { mutableStateOf(Offset.Zero) } // 2. Track the size of the background container. var containerSize by remember { mutableStateOf(IntSize.Zero) } Box( modifier = Modifier .size(300.dp, 200.dp) // Capture the actual pixel dimensions of the container when it's laid out. .onSizeChanged { containerSize = it } .clip(RoundedCornerShape(12.dp)) .background( brush = remember(hue) { // Create a simple gradient representing Saturation and Value for the given Hue. Brush.linearGradient(listOf(Color.White, Color.hsv(hue, 1f, 1f))) } ) ) { Box( modifier = Modifier .size(24.dp) .graphicsLayer { // Center the selector on the finger by subtracting half its size. translationX = selectorOffset.x - (24.dp.toPx() / 2) translationY = selectorOffset.y - (24.dp.toPx() / 2) } // ... // 3. Configure 2D touch dragging. .draggable2D( state = rememberDraggable2DState { delta -> // 4. Calculate the new position and clamp it to the container bounds val newX = (selectorOffset.x + delta.x) .coerceIn(0f, containerSize.width.toFloat()) val newY = (selectorOffset.y + delta.y) .coerceIn(0f, containerSize.height.toFloat()) selectorOffset = Offset(newX, newY) } ) ) } }
گزیده قبلی شامل موارد زیر است:
- این کد از اصلاحکننده
onSizeChangedبرای ضبط ابعاد واقعی ظرف گرادیان استفاده میکند. انتخابگر دقیقاً میداند لبهها کجا هستند. - درون
graphicsLayer،translationXوtranslationYرا تنظیم میکند تا مطمئن شود که گزینشگر هنگام کشیدن در مرکز باقی میماند.
توصیهشده برای شما
- توجه: وقتی جاوا اسکریپت خاموش باشد، نوشتار پیوند نمایش داده میشود
- آشنایی با اشارهها
- نمونه پویانمایی پیشرفته: اشارهها {:#gesture-and-animation}
- پویانماییهای مبتنی بر مقدار