سحب الدوال المركّبة

يُعدّ السماح للمستخدمين بسحب المكوّنات فعليًا على الشاشة أمرًا أساسيًا في تطبيقات 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
                    }
                }
        )
    }
}

INSERT ALT TEXT HERE
الشكل 1. عنصر في واجهة المستخدم يتم سحبه بإصبع

سحب المكوّنات بين نقاط الارتكاز باستخدام anchoredDraggable

يتيح لك المعدِّل anchoredDraggable سحب أحد المكوّنات بين مجموعة من المواضع المحدّدة مسبقًا والتي تُعرف باسم نقاط الارتكاز. عندما يحرّر المستخدم إيماءة السحب، يستقر المكوّن عند أقرب نقطة تثبيت. anchoredDraggable مفيد لإنشاء مكوّنات مثل أوراق الأنشطة السفلية المخصّصة أو أشرطة التمرير المعقّدة أو لوحات الأدراج.

مساحة عرض واجهة برمجة التطبيقات

تتألف واجهة برمجة التطبيقات AnchoredDraggable من الأجزاء الرئيسية التالية:

  • نقاط الارتكاز (DraggableAnchors): أداة إنشاء تحدّد عملية الربط بين حالاتك المخصّصة (مثل فئة تعداد Start, Center, End) وقيم إزاحة البكسل المقابلة لها.
  • الحالة (AnchoredDraggableState<T>): عنصر الاحتفاظ بالحالة يدير الموضع الحالي للمكوّن (الإزاحة) ونقطة الارتكاز التي تم ضبطه عليها ومستوى تقدّمه في السحب. يجب إنشاء هذا الكائن باستخدام rememberSaveable وAnchoredDraggableState.Saver<T> للحفاظ على حالته عند إجراء تغييرات في الإعدادات.
  • معدِّل الإيماءات (Modifier.anchoredDraggable): معدِّل يرصد إيماءات السحب على طول Orientation، ويعدّل الحالة باستخدام دلتا السحب، ويتعامل مع إيماءة النقر السريع لتحريك المكوّن إلى أقرب نقطة تثبيت عند الإفلات.

مثال: إنشاء مكوّن SwipeToDismiss مخصّص

يوضّح هذا المثال كيفية إنشاء مكوّن SwipeToDismiss مخصّص (بانر أبيض) يمكن للمستخدم التمرير سريعًا إلى اليمين لإغلاقه، أو السحب إلى اليسار لرؤية تأثير تمديد تجاوز حد التمرير.

  1. تحديد قيم الربط حدِّد تعدادًا يمثّل المواضع المنفصلة التي يمكن أن يستقر فيها المحتوى القابل للسحب:

    private enum class AnchoredDraggableSwipeDismissValue { DismissedStart, Resting }

  2. إنشاء مثيل وحفظ AnchoredDraggableState استخدِم rememberSaveable مع AnchoredDraggableState.Saver() للاحتفاظ بالموضع الحالي للمكوّن أثناء إعادة إنشاء النشاط وتغييرات الإعدادات (مثل تدوير الشاشة أو تغييرات المظهر):

    val state =
        rememberSaveable(saver = AnchoredDraggableState.Saver()) {
            AnchoredDraggableState(initialValue = AnchoredDraggableSwipeDismissValue.Resting)
        }

  3. تطبيق المعدِّلات 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()
    }

  4. [اختياري] دمج ميزة تجاوز حد التمرير تنفّذ واجهة برمجة التطبيقات overscrollEffect تأثيرًا مرئيًا يظهر عند الوصول إلى حواف حاوية قابلة للتمرير من خلال التمرير أو التحريك السريع. يمكنك تقديم ملاحظات مرئية عندما يسحب المستخدم العنصر متجاوزًا حدود الارتساء المحدّدة باستخدام تأثير تجاوز حد التمرير (عادةً ما يكون تمديدًا أو توهجًا).

    1. الإرسال: تمرير مثيل overscrollEffect إلى أداة تعديل anchoredDraggable. يؤدي ذلك إلى إرسال قيم السحب إلى حالة AnchoredDraggable أو تأثير تجاوز حد التمرير، استنادًا إلى ما إذا كانت الإيماءة داخل حدود الارتساء أو خارجها.
    2. العرض: طبِّق 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()
    }
}

النتيجة

ينتج عن مقتطف الرمز السابق بانر أبيض يحتوي على النص "اسحبني لإغلاقي"، ويمكن للمستخدم التمرير سريعًا لليسار لإغلاقه (أو السحب لليمين للاطّلاع على تأثير تمديد تجاوز حد التمرير):

الفيديو 2. مثال يوضّح تنفيذ مكوّن مخصّص لإجراء التمرير السريع للإغلاق.

مثال: إنشاء لائحة تنقّل مخصّصة تستند إلى الإيماءات

يوضّح هذا المثال كيفية إنشاء لائحة تنقّل مخصّصة يتم التحكّم فيها بالإيماءات. عندما يسحب المستخدم الشاشة أفقيًا، تنزلق الشاشة الأمامية (ScreenContents) إلى اليمين، ويتم تصغير حجمها من 1.0f إلى 0.8f، وتصبح زواياها مستديرة للكشف عن لائحة التنقل الأساسية.

  1. تحديد حالات ومواضع الإعلانات الثابتة في هذا المثال، نقاط الارتساء هي الحالتان المفتوحة والمغلقة للدرج. حدِّد تعدادًا لحالتَي 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,
            )
        }

  2. تحريك 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(...)
        }
    }

  3. استخدِموا 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)
        )
    }

  4. يمكنك تشغيل الصور المتحركة الآلية باستخدام 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)
            }
        }
    }

النتيجة

الفيديو 3: يتم سحب شاشة المقدّمة (النوم) إلى اليسار، ما يؤدي إلى عرض لائحة التنقل الأساسية.

السحب في بُعدَين باستخدام 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)
        }
    }
}

الفيديو 4 مربّع أرجواني صغير يتم تغيير موضعه على خلفية رمادية، ما يوضّح السحب المباشر ثنائي الأبعاد حيث يتوقف العنصر عن الحركة فور رفع إصبع المستخدم

يتضمّن مقتطف الرمز السابق ما يلي:

  • يتتبّع موضع المربّع باستخدام حالة 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)
                    }
                )
        )
    }
}

الفيديو 5: تدرّج الألوان مع مقبض اختيار دائري أبيض يمكن سحبه في أي اتجاه، يوضّح كيفية تقييد دلتا ثنائية الأبعاد على حدود الحاوية لتعديل قيم الألوان المحدّدة.

يتضمّن المقتطف السابق ما يلي:

  • يستخدم المعدِّل onSizeChanged لالتقاط الأبعاد الفعلية لحاوية التدرّج. تتعرّف أداة الاختيار على الحواف بدقة.
  • داخل graphicsLayer، يتم ضبط translationX وtranslationY للتأكّد من بقاء أداة الاختيار في المنتصف أثناء السحب.