کشیدن عناصر ساختنی

امکان کشیدن فیزیکی مؤلفه‌ها در سراسر صفحه توسط کاربران، برای برنامه‌های مدرن 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 سفارشی (یک برنما سفید) بسازید که کاربر بتواند برای رد کردن آن، تند به چپ بکشد یا برای دیدن جلوه کشش پیمایش بیش‌ازحد، آن را به راست بکشد.

  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() در لامبدای فاصله می‌خواند. خواندن آفست در این لامبدا تضمین می‌کند که مقدار فقط در فاز جای‌گذاری خوانده می‌شود و از بازترکیب‌های غیرضروری درطول کشیدن جلوگیری می‌کند. این اصلاح‌کننده را روی محتوایی که باید به‌صورت بصری حرکت کند اعمال کنید.

    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 API جلوه دیداری‌ای را پیاده‌سازی می‌کند که وقتی با پیمایش یا پرتاب به لبه‌های محفظه پیمایش می‌رسید نمایش داده می‌شود. وقتی کاربر با جلوه پیمایش بیش‌ازحد (معمولاً کشش یا درخشش) از حدود لنگر تعریف‌شده فراتر می‌رود، می‌توانید بازخورد بصری ارائه دهید.

    1. Dispatch: Pass an overscrollEffect instance to the anchoredDraggable modifier. این کار باعث می‌شود تغییرات درگ به حالت AnchoredDraggable یا جلوه پیمایش بیش‌ازحد ارسال شود، بسته به اینکه اشاره درون یا بیرون مرزهای لنگر باشد.
    2. Render: 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()
    }
}

نتیجه

تکه‌کد قبلی منجر به یک برنمای سفید با نوشتار «Drag me to dismiss!» می‌شود که کاربر می‌تواند برای بستن آن تند به چپ بکشد (یا برای دیدن جلوه کشیدگی پیمایش بیش‌ازحد، آن را به راست بکشد):

ویدیو ۲. پیاده‌سازی عنصر سفارشی برای کشیدن و بستن.

مثال: ایجاد کشوی پیمایش سفارشی با اشاره

این مثال نشان می‌دهد که چگونه یک کشوی پیمایش سفارشی و مبتنی بر اشاره بسازید. وقتی کاربر به‌صورت افقی می‌کشد، صفحه پیش‌زمینه (ScreenContents) به‌سمت راست می‌لغزد، از 1.0f به 0.8f کوچک می‌شود، و گوشه‌هایش گرد می‌شود تا کشوی پیمایش زیرین را نشان دهد.

  1. تعریف وضعیت‌ها و موقعیت‌های لنگر. در این مثال، لنگرها حالت‌های باز و بسته کشو هستند. یک 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,
            )
        }

  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)
            }
        }
    }

نتیجه

ویدیو ۳. صفحه پیش‌زمینه (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)
        }
    }
}

ویدیو ۴. جعبه‌ای کوچک به‌رنگ بنفش که روی پس‌زمینه‌ای خاکستری درحال جابه‌جایی است و کشیدن مستقیم دوبعدی را نشان می‌دهد که در آن عنصر به‌محض اینکه انگشت کاربر برداشته می‌شود از حرکت بازمی‌ایستد.

تکه‌کد قبلی شامل موارد زیر است:

  • موقعیت جعبه را بااستفاده از وضعیت 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 را تنظیم می‌کند تا مطمئن شود انتخابگر هنگام کشیدن در مرکز باقی می‌ماند.