يُعدّ تمكين المستخدمين من سحب المكوّنات فعليًا عبر الشاشة أمرًا أساسيًا في تطبيقات 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 من الأجزاء الرئيسية التالية:
- Anchors (
DraggableAnchors): أداة إنشاء تحدّد عملية الربط بين الحالات المخصّصة (مثل فئة enum لـ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}
- الصور المتحركة المستندة إلى القيمة