امکان کشیدن فیزیکی مؤلفهها در سراسر صفحه توسط کاربران، برای برنامههای مدرن Android ضروری است. چه درحال ساختن یک لغزنده سفارشی، یک مؤلفه تند کشیدن برای رد کردن، یا یک بوم تعاملی باشید، Jetpack Compose مجموعهای جامع از میاناهای برنامهسازی کاربردی را برای مدیریت آسان رویدادهای کشیدن ارائه میدهد.
Compose dragging APIs پیچیدگی رویدادهای لمسی خام را انتزاعی میکند.
بهجای محاسبه دستی مختصات لمس و مدیریت چرخههای حیات اشاره با pointerInput، میتوانید از اصلاحکنندههای داخلی استفاده کنید که مستقیماً به سیستمهای پویانمایی و مدیریت حالت Compose متصل میشوند.
این صفحه نحوه و زمان استفاده از هر 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): سازندهای که نگاشت بین حالتهای سفارشی شما (برای نمونه، کلاس شمارشی برایStart, Center, End) و مقادیر جبران پیکسل مربوط به آنها را تعریف میکند. - State (
AnchoredDraggableState<T>): نگهدارنده حالتی که موقعیت فعلی مؤلفه (انحراف)، لنگری که در آن قرار گرفته است، و پیشرفت کشیدن آن را مدیریت میکند. باید بااستفاده ازrememberSaveableوAnchoredDraggableState.Saver<T>ایجاد شود تا وضعیت آن در تغییرات پیکربندی حفظ شود. - اصلاحکننده اشاره (
Modifier.anchoredDraggable): اصلاحکنندهای که اشارههای کشیدن را درطولOrientationشناسایی میکند، وضعیت را با دلتای کشیدن بهروز میکند، و اشاره پرتاب را برای پویانمایی کردن مؤلفه به نزدیکترین لنگر هنگام رها کردن مدیریت میکند.
مثال: ساختن مؤلفه SwipeToDismiss سفارشی
این مثال نشان میدهد که چگونه یک مؤلفه SwipeToDismiss سفارشی (یک برنما سفید) بسازید که کاربر بتواند برای رد کردن آن، تند به چپ بکشد یا برای دیدن جلوه کشش پیمایش بیشازحد، آن را به راست بکشد.
مقادیر لنگر را تعریف کنید. تعداد ثابتی را تعریف کنید که نشاندهنده موقعیتهای مجزایی باشد که محتوای قابلکشیدن میتواند در آنها قرار بگیرد:
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() } }
نتیجه
تکهکد قبلی منجر به یک برنمای سفید با نوشتار «Drag me to dismiss!» میشود که کاربر میتواند برای بستن آن تند به چپ بکشد (یا برای دیدن جلوه کشیدگی پیمایش بیشازحد، آن را به راست بکشد):
مثال: ایجاد کشوی پیمایش سفارشی با اشاره
این مثال نشان میدهد که چگونه یک کشوی پیمایش سفارشی و مبتنی بر اشاره بسازید. وقتی کاربر بهصورت افقی میکشد، صفحه پیشزمینه (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}
- پویانماییهای مبتنی بر مقدار