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

امکان‌پذیر کردن کشیدن فیزیکی مؤلفه‌ها در سراسر صفحه‌نمایش برای کاربران، یکی از اصول اساسی برنامه‌های مدرن Android است. چه درحال ساختن لغزنده سفارشی، مؤلفه تند کشیدن برای رد کردن، یا بوم تعاملی باشید، Jetpack Compose مجموعه جامعی از میاناهای برنامه‌سازی کاربردی را برای مدیریت آسان رویدادهای کشیدن ارائه می‌دهد.

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

این صفحه نحوه و زمان استفاده از هر API کشیدن را شرح می‌دهد.

انتخاب کردن 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): سازنده‌ای که نگاشت بین حالت‌های سفارشی شما (مثلاً کلاس enum برای Start, Center, End) و مقادیر آفست پیکسل مربوطه را تعریف می‌کند.
  • State (AnchoredDraggableState<T>): نگه‌دارنده حالتی که موقعیت فعلی مؤلفه (انحراف)، لنگری که در آن قرار گرفته است، و پیشرفت کشیدن آن را مدیریت می‌کند. باید بااستفاده از rememberSaveable و AnchoredDraggableState.Saver<T> ایجاد شود تا وضعیت آن در تغییرات پیکربندی حفظ شود.
  • اصلاح‌کننده اشاره (Modifier.anchoredDraggable): اصلاح‌کننده‌ای که اشاره‌های کشیدن را درطول Orientation شناسایی می‌کند، وضعیت را با دلتای کشیدن به‌روز می‌کند، و اشاره پرتاب کردن را برای پویانمایی کردن مؤلفه به نزدیک‌ترین لنگر هنگام رها کردن مدیریت می‌کند.

مثال: ساختن مؤلفه SwipeToDismiss سفارشی

این مثال نشان می‌دهد که چگونه می‌توان یک مؤلفه SwipeToDismiss سفارشی (یک برنمای سفید) ساخت که کاربر بتواند با تند کشیدن به چپ آن را رد کند یا با کشیدن به راست، جلوه کشیدگی فراتومار را ببیند.

  1. مقادیر لنگر را تعریف کنید. یک enum تعریف کنید که نشان‌دهنده موقعیت‌های گسسته‌ای باشد که محتوای کشیدنی می‌تواند در آن‌ها قرار گیرد:

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

نتیجه

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

ویدیو ۲. پیاده‌سازی مؤلفه سفارشی «تند کشیدن برای بستن».

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

این مثال نشان می‌دهد که چگونه یک کشوی پیمایش سفارشی و مبتنی بر اشاره بسازید. وقتی کاربر به‌صورت افقی می‌کشد، صفحه پیش‌زمینه (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 را تنظیم می‌کند تا مطمئن شود که گزینشگر هنگام کشیدن در مرکز باقی می‌ماند.