يُعدّ السماح للمستخدمين بسحب المكوّنات فعليًا على الشاشة أمرًا أساسيًا في تطبيقات Android الحديثة. سواء كنت بصدد إنشاء شريط تمرير مخصّص أو مكوّن يمكن إغلاقه بالتمرير السريع أو لوحة رسم تفاعلية، يوفّر Jetpack Compose مجموعة شاملة من واجهات برمجة التطبيقات للتعامل بسهولة مع أحداث السحب.
تُبسّط واجهات برمجة التطبيقات الخاصة بالسحب في Compose تعقيدات أحداث اللمس الأولية.
بدلاً من احتساب إحداثيات اللمس وإدارة دورات حياة الإيماءات يدويًا باستخدام pointerInput، يمكنك استخدام أدوات تعديل مدمَجة ترتبط مباشرةً بأنظمة إدارة الحالة والرسوم المتحركة في Compose.
توضّح هذه الصفحة كيفية استخدام كل واجهة برمجة تطبيقات للسحب والإفلات ومتى يجب استخدامها.
اختيار واجهة برمجة التطبيقات المناسبة للسحب
هناك العديد من واجهات برمجة التطبيقات التي يمكنك استخدامها لتنفيذ محتوى قابل للسحب.
- استخدِم مكوّنات Material العادية (مثل
SwipeToDismissBoxأو أدراج التنقّل) في الحالات التالية: عندما تحتاج إلى السلوك التلقائي للتمرير السريع أو السحب في Material 3 (على سبيل المثال، حذف رسالة إلكترونية أو أرشفة مهمة أو فتح قائمة جانبية أو أوراق في أسفل الشاشة). وتوفّر سلوكيات مدمجة بدون الحاجة إلى إدارة الحالة أو التنسيق المخصّصَين. - اختيار
Modifier.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 من الأجزاء الرئيسية التالية:
- نقاط الارتكاز (
DraggableAnchors): أداة إنشاء تحدّد عملية الربط بين حالاتك المخصّصة (مثل فئة تعدادStart, Center, End) وقيم إزاحة البكسل المقابلة لها. - الحالة (
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()داخل دالة lambda للإزاحة. تضمن قراءة الإزاحة ضمن دالة lambda هذه عدم قراءة القيمة إلا أثناء مرحلة التحديد، ما يؤدي إلى تجنُّب عمليات إعادة التركيب غير الضرورية أثناء السحب. طبِّق هذا المعدِّل على المحتوى الذي يجب أن يتحرّك بشكل مرئي.
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() }
[اختياري] دمج ميزة تجاوز حد التمرير تنفّذ واجهة برمجة التطبيقات
overscrollEffectتأثيرًا مرئيًا يظهر عند الوصول إلى حواف حاوية قابلة للتمرير من خلال التمرير أو التحريك السريع. يمكنك تقديم ملاحظات مرئية عندما يسحب المستخدم العنصر متجاوزًا حدود الارتساء المحدّدة باستخدام تأثير تجاوز حد التمرير (عادةً ما يكون تمديدًا أو توهجًا).- الإرسال: تمرير مثيل
overscrollEffectإلى أداة تعديلanchoredDraggable. يؤدي ذلك إلى إرسال قيم السحب إلى حالةAnchoredDraggableأو تأثير تجاوز حد التمرير، استنادًا إلى ما إذا كانت الإيماءة داخل حدود الارتساء أو خارجها. - العرض: طبِّق
Modifier.overscrollمع مثيلOverscrollEffectنفسه على المكوّن الذي يجب أن يعرض التأثير بصريًا.
- الإرسال: تمرير مثيل
يوضّح المقتطف التالي كيفية إضافة تأثير تمديد عادي:
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، وتصبح زواياها مستديرة
للكشف عن لائحة التنقل الأساسية.
تحديد حالات ومواضع الإعلانات الثابتة في هذا المثال، نقاط الارتساء هي الحالتان المفتوحة والمغلقة للدرج. حدِّد تعدادًا لحالتَي
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للتأكّد من بقاء أداة الاختيار في المنتصف أثناء السحب.
التطبيقات المقترَحة لك
- ملاحظة: يتم عرض نص الرابط عندما تكون JavaScript غير مفعَّلة
- التعرّف على الإيماءات
- مثال على الحركة المتقدّمة: الإيماءات {:#gesture-and-animation}
- الصور المتحركة المستندة إلى القيمة