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

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

  • Anchors (DraggableAnchors): أداة إنشاء تحدّد عملية الربط بين الحالات المخصّصة (مثل فئة enum لـ 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: يتم سحب شاشة المقدّمة (Sleep) إلى اليسار، ما يؤدي إلى عرض لائحة التنقل الأساسية.

السحب في بُعدَين باستخدام 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 للتأكّد من بقاء أداة الاختيار في المنتصف أثناء السحب.