لیست هایی با Compose for Wear OS


لیست‌ها به کاربران اجازه می‌دهند تا یک مورد را از مجموعه‌ای از گزینه‌ها در دستگاه‌های Wear OS انتخاب کنند.

بسیاری از دستگاه‌های Wear OS از صفحه نمایش‌های گرد استفاده می‌کنند که دیدن آیتم‌های لیست که در نزدیکی بالا و پایین صفحه نمایش ظاهر می‌شوند را دشوارتر می‌کند. به همین دلیل، Compose for Wear OS شامل نسخه‌ای از کلاس LazyColumn به نام TransformingLazyColumn است که از انیمیشن‌های مقیاس‌بندی و تغییر شکل پشتیبانی می‌کند. وقتی آیتم‌ها به لبه‌ها حرکت می‌کنند، کوچک‌تر و محو می‌شوند.

برای اعمال جلوه‌های مقیاس‌بندی و پیمایش توصیه‌شده:

  1. از Modifier.transformedHeight استفاده کنید تا Compose بتواند تغییر ارتفاع را هنگام اسکرول کردن آیتم در صفحه محاسبه کند.
  2. برای اعمال جلوه‌های بصری، از جمله کوچک کردن محتوای آیتم، از transformation = SurfaceTransformation(transformationSpec) استفاده کنید.
  3. برای کامپوننت‌هایی که transformation به عنوان پارامتر نمی‌پذیرند، مانند Text از یک TransformationSpec سفارشی استفاده کنید.

انیمیشن زیر نشان می‌دهد که چگونه یک عنصر لیست هنگام نزدیک شدن به بالا و پایین صفحه، تغییر اندازه می‌دهد و شکل آن تغییر می‌کند:

قطعه کد زیر نحوه ایجاد یک لیست با استفاده از طرح TransformingLazyColumn را نشان می‌دهد تا محتوایی ایجاد شود که در اندازه‌های مختلف صفحه نمایش Wear OS عالی به نظر برسد .

این قطعه کد همچنین استفاده از اصلاح‌کننده‌ی minimumVerticalContentPadding را نشان می‌دهد که باید آن را روی آیتم‌های لیست تنظیم کنید تا فاصله‌ی (padding) صحیح در بالا و پایین لیست اعمال شود.

برای نمایش نشانگر اسکرول، columnState بین ScreenScaffold و TransformingLazyColumn به اشتراک بگذارید:

val columnState = rememberTransformingLazyColumnState()
val transformationSpec = rememberTransformationSpec()
ScreenScaffold(
    scrollState = columnState
) { contentPadding ->
    TransformingLazyColumn(
        state = columnState,
        contentPadding = contentPadding
    ) {
        item {
            ListHeader(
                modifier = Modifier
                    .fillMaxWidth()
                    .transformedHeight(this, transformationSpec)
                    .minimumVerticalContentPadding(ListHeaderDefaults.minimumTopListContentPadding),
                transformation = SurfaceTransformation(transformationSpec)
            ) {
                Text(text = "Header")
            }
        }
        // ... other items
        item {
            Button(
                modifier = Modifier
                    .fillMaxWidth()
                    .transformedHeight(this, transformationSpec)
                    .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding),
                transformation = SurfaceTransformation(transformationSpec),
                onClick = { /* ... */ },
                icon = {
                    Icon(
                        imageVector = Icons.Default.Build,
                        contentDescription = "build",
                    )
                },
            ) {
                Text(
                    text = "Build",
                    maxLines = 1,
                    overflow = TextOverflow.Ellipsis,
                )
            }
        }
    }
}

یک افکت ضربه محکم و ناگهانی اضافه کنید

قابلیت Snapping تضمین می‌کند که وقتی کاربر اسکرول یا حرکت Fling را تمام می‌کند، لیست با یک آیتم که دقیقاً در یک نقطه خاص، معمولاً مرکز صفحه، قرار گرفته است، تنظیم شود. در صفحه‌های نمایش گرد، که آیتم‌ها با دور شدن از مرکز، تغییر اندازه می‌دهند و شکل می‌گیرند، Snapping به ویژه برای اطمینان از اینکه مرتبط‌ترین آیتم در ناحیه دید بهینه کاملاً قابل مشاهده و خوانا باقی می‌ماند، مفید است.

برای افزودن رفتار snap-and-fling، پارامتر flingBehavior را روی TransformingLazyColumnDefaults.snapFlingBehavior(columnState) تنظیم کنید. برای ایجاد یک تجربه سازگار هنگام استفاده از تاج یا قاب فیزیکی، rotaryScrollableBehavior را با استفاده از RotaryScrollableDefaults.snapBehavior(columnState) مطابقت دهید.

val columnState = rememberTransformingLazyColumnState()
ScreenScaffold(scrollState = columnState) { contentPadding ->
    TransformingLazyColumn(
        state = columnState,
        flingBehavior = TransformingLazyColumnDefaults.snapFlingBehavior(columnState),
        rotaryScrollableBehavior = RotaryScrollableDefaults.snapBehavior(columnState)
    ) {
        // ...
        // ...
    }
}

طرح‌بندی معکوس

به طور پیش‌فرض، یک لیست قابل اسکرول به لبه بالایی خود متصل می‌شود. اگر کاربر به پایین یک لیست استاندارد اسکرول کرده باشد و یک آیتم جدید به انتهای آن اضافه شود، لیست نمای کاربر را روی آیتم فعلی حفظ می‌کند. به عنوان مثال، اگر کاربر در حال مشاهده آیتم ۱۰ در پایین صفحه باشد و آیتم ۱۱ اضافه شود، نما روی آیتم ۱۰ متمرکز می‌ماند و آیتم ۱۱ خارج از صفحه و در زیر نمای فعلی ظاهر می‌شود.

برای مواردی مانند برنامه‌های پیام‌رسان یا گزارش‌های زنده، این رفتار معمولاً مطلوب نیست. وقتی موارد جدید می‌رسند، کاربران معمولاً می‌خواهند آخرین محتوا را فوراً ببینند، اگر در پایین لیست باشند. اگر موارد زیادی به طور همزمان برسند، لیست باید پرش کند تا آخرین مورد را در پایین نمایش دهد (به این معنی که برخی از موارد میانی ممکن است اصلاً نمایش داده نشوند مگر اینکه کاربر به بالا اسکرول کند).

برای پشتیبانی از این موارد استفاده، TransformingLazyColumn به شما امکان می‌دهد با تنظیم reverseLayout = true ، طرح‌بندی را معکوس کنید. این کار لنگر لیست را از لبه بالا به لبه پایین تغییر می‌دهد.

برای راحتی، تنظیم reverseLayout = true ترتیب بصری آیتم‌ها و جهت اسکرول کردن را نیز معکوس می‌کند:

  • آیتم‌ها از پایین به بالا تشکیل می‌شوند، به این معنی که اندیس ۰ در پایین صفحه نمایش داده می‌شود.
  • با اسکرول کردن به بالا، آیتم‌هایی با شاخص‌های بالاتر نمایش داده می‌شوند.

برای افزودن رفتار snap-and-fling به همراه طرح‌بندی معکوس، می‌توانید flingBehavior و rotaryScrollableBehavior را همانطور که در قطعه کد زیر نشان داده شده است، ترکیب کنید:

val columnState = rememberTransformingLazyColumnState()
val transformationSpec = rememberTransformationSpec()
ScreenScaffold(scrollState = columnState) { contentPadding ->
    TransformingLazyColumn(
        state = columnState,
        contentPadding = contentPadding,
        reverseLayout = true,
        modifier = Modifier.fillMaxWidth()
    ) {
        items(10) { index ->
            Button(
                label = {
                    Text(
                        text = "Item ${index + 1}"
                    )
                },
                onClick = {},
                modifier = Modifier
                    .fillMaxWidth()
                    .transformedHeight(this, transformationSpec)
                    .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding),
                transformation = SurfaceTransformation(transformationSpec)
            )
        }
        item {
            // With reverseLayout = true, the last item declared appears at the top.
            ListHeader(
                modifier = Modifier
                    .fillMaxWidth()
                    .transformedHeight(this, transformationSpec)
                    .minimumVerticalContentPadding(ListHeaderDefaults.minimumTopListContentPadding),
                transformation = SurfaceTransformation(transformationSpec)
            ) {
                Text("Header")
            }
        }
    }
}

تصاویر زیر تفاوت بین یک لیست معمولی و یک لیست معکوس را نشان می‌دهند:

یک ستون TransformingLazyColumn با طرح‌بندی معمولی، که آیتم ۱ را در بالا و آیتم‌ها را به ترتیب صعودی نشان می‌دهد.
شکل ۱. یک طرح‌بندی استاندارد فهرست که در آن محتوا از بالا به پایین پر می‌شود.
یک ستون TransformingLazyColumn با طرح‌بندی معکوس، که آیتم ۱ را در پایین و آیتم‌ها را به ترتیب نزولی به سمت بالا نشان می‌دهد.
شکل ۲. طرح‌بندی لیست معکوس که در آن محتوا از پایین به بالا پر می‌شود.

دکمه‌های لبه‌ای در لیست‌ها

برای متریال ۳، می‌توانید یک EdgeButton اضافه کنید که یک دکمه‌ی لبه‌دار در پایین لیست‌ها است. با این حال، مراقب باشید که این را به عنوان یک آیتم در TransformingLazyColumn اضافه نکنید، بلکه از جایگاه edgeButton در ScreenScaffold استفاده کنید.

استفاده از اسلات edgeButton تضمین می‌کند که دکمه به درستی در پایین صفحه قرار گرفته و هنگام پیمایش لیست، رفتار مناسبی داشته باشد.

قطعه کد زیر نحوه پیاده‌سازی EdgeButton را نشان می‌دهد:

val columnState = rememberTransformingLazyColumnState()
ScreenScaffold(
    scrollState = columnState,
    edgeButton = {
        EdgeButton(
            onClick = { /* TODO */ },
            modifier = Modifier.scrollable(
                columnState,
                orientation = Orientation.Vertical,
                reverseDirection = true,
                // Apply overscroll to the EdgeButton for proper scrolling behavior.
                overscrollEffect = rememberOverscrollEffect(),
            )
        ) {
            Text("More")
        }
    }
) { contentPadding ->
    TransformingLazyColumn(
        contentPadding = contentPadding,
        state = columnState,
    ) {
        // ...
        // ...
    }
}

کشیدن انگشت برای آشکار کردن در لیست‌ها

کامپوننت SwipeToReveal به شما امکان می‌دهد با کشیدن انگشت، به اقدامات مربوط به یک آیتم لیست، مانند Card یا Chip ، دسترسی پیدا کنید. کشیدن انگشت معمولاً یک یا دو دکمه عملیاتی (مانند "حذف" یا "بیشتر") را در کنار صفحه نمایش می‌دهد.

هنگام استفاده از SwipeToReveal درون یک TransformingLazyColumn ، این دستورالعمل‌ها را دنبال کنید:

  • تنظیم مجدد هنگام اسکرول : وقتی کاربر لیست را اسکرول می‌کند، هر آیتمی که با کشیدن انگشت باز شده را به حالت پوشیده شده‌اش برمی‌گرداند.
  • ارتفاع‌های ثابت : ارتفاع دکمه‌های عملیاتی را طوری تنظیم کنید که با آیتم داخلی که روی آن کشیده می‌شود (چه یک Button باشد و چه یک Card ) مطابقت داشته باشد تا ظاهری ثابت داشته باشد.
  • تغییر شکل دادن کانتینر : اصلاح‌کننده transformedHeight و transformationSpec را روی خود کامپوننت SwipeToReveal اعمال کنید.
  • تبدیل دوگانه انجام ندهید : اصلاح‌کننده‌های transformedHeight یا transformation را روی آیتم داخلیِ سوایپ‌شده (کارت یا دکمه‌ی داخل کانتینر SwipeToReveal ) اعمال نکنید.

ترکیبات سفارشی در لیست‌ها

هنگام ساخت اجزای سطح سفارشی برای TransformingLazyColumn ، این بهترین شیوه‌ها را دنبال کنید تا عناصر قابل ترکیب شما به آرامی در نزدیکی لبه‌های صفحه نمایش تغییر اندازه، محو شدن و شکل بگیرند:

  • Expose SurfaceTransformation : یک پارامتر اختیاری SurfaceTransformation (با مقدار پیش‌فرض null ) را می‌پذیرد که با کامپوننت‌های استاندارد Wear Compose Material 3 مانند Card و Button مطابقت دارد. این به فراخوانی‌کنندگان در یک TransformingLazyColumn اجازه می‌دهد SurfaceTransformation(transformationSpec) را ارسال کنند، در حالی که به کامپوننت اجازه می‌دهد به طور معمول خارج از یک لیست کار کند.
  • ابتدا در کد فراخوانی Modifier.transformedHeight اعمال کنید : هنگام قرار دادن composable سفارشی خود در یک TransformingLazyColumn ، Modifier.transformedHeight(this, transformationSpec) را به عنوان اولین اصلاح‌کننده در زنجیره اصلاح‌کننده کد فراخوانی‌شده ارسال کنید. در حالی که SurfaceTransformation جلوه‌های مقیاس‌بندی بصری و محو شدن را اعمال می‌کند، transformedHeight برای گفتن به طرح‌بندی لیست برای محاسبه مجدد ارتفاع آیتم هنگام کوچک شدن، بسیار مهم است.
  • لایه‌های تبدیل، modifier فراخوانی‌کننده و نقاش را به ترتیب اعمال کنید :
    1. لایه تبدیل کانتینر : زنجیره اصلاح‌کننده کانتینر ریشه را با Modifier.graphicsLayer شروع کنید و applyContainerTransformation() ، به طوری که هم پس‌زمینه و هم محتوا در فضای مختصات مقیاس‌بندی شده و کج‌شده ترسیم شوند.
    2. modifier فراخوانی‌کننده : پارامتر modifier ارسال‌شده توسط فراخوانی‌کننده (که شامل Modifier.transformedHeight می‌شود) را در مرحله‌ی بعد، قبل از هرگونه تغییر اندازه یا فاصله‌گذاری داخلی، اعمال کنید.
    3. برش شکل وقتی هیچ تبدیلی وجود ندارد : اگر transformation null باشد، قبل از ترسیم پس‌زمینه، Modifier.clip(shape) را اعمال کنید. نقاش برگردانده شده توسط createContainerPainter() خودش را به شکل می‌چسباند، اما یک نقاش ساده این کار را نمی‌کند، بنابراین بدون این، پس‌زمینه با گوشه‌های مربعی خارج از یک لیست ترسیم می‌شود.
    4. نقاش پس‌زمینه‌ی تغییرشکل‌دهنده : با استفاده از Modifier.drawBehind و یک نقاش ایجاد شده از createContainerPainter() پس‌زمینه را درون لایه‌ی ظرف رسم کنید.
    5. لایه تبدیل محتوا : یک لایه دوم Modifier.graphicsLayer با استفاده applyContentTransformation() اعمال کنید و آن را به شکل ظرف بچسبانید تا محتوای داخلی با نزدیک شدن به قاب، زودتر محو شود.

قطعه کد زیر نحوه پیاده‌سازی یک BoardingPassCard سفارشی را نشان می‌دهد که این تبدیل‌ها را به ترتیب اعمال می‌کند:

@Composable
fun BoardingPassCard(
    flightNumber: String,
    origin: String,
    destination: String,
    gate: String,
    seat: String,
    departureTime: String,
    modifier: Modifier = Modifier,
    transformation: SurfaceTransformation? = null,
    shape: Shape = RoundedCornerShape(18.dp),
    statusBadge: @Composable () -> Unit = {}
) {
    // 1. Create morphing container painter
    val backgroundPainter = ColorPainter(MaterialTheme.colorScheme.surfaceContainer)
    val finalPainter = if (transformation != null) {
        remember(transformation, backgroundPainter, shape) {
            transformation.createContainerPainter(backgroundPainter, shape, border = null)
        }
    } else {
        backgroundPainter
    }

    Column(
        modifier = Modifier
            // 2a. Container layer: Scales, fades, and tilts the whole card surface
            .then(
                if (transformation != null) {
                    Modifier.graphicsLayer {
                        transformation.run { applyContainerTransformation() }
                    }
                } else Modifier
            )
            // 2b. Caller modifier: Includes Modifier.transformedHeight in a list
            .then(modifier)
            .fillMaxWidth()
            // 2c. Shape clip: Only needed without a transformation, because the
            // painter from createContainerPainter clips itself to the shape
            .then(if (transformation == null) Modifier.clip(shape) else Modifier)
            // 2d. Morphing background: Drawn inside the transformed container layer
            .drawBehind {
                with(finalPainter) {
                    draw(size)
                }
            }
            // 2e. Content layer: Fades content earlier and clips children to shape
            .then(
                if (transformation != null) {
                    Modifier.graphicsLayer {
                        this.shape = shape
                        this.clip = true
                        transformation.run { applyContentTransformation() }
                    }
                } else Modifier
            )
            .padding(horizontal = 14.dp, vertical = 10.dp)
    ) {
        // Card content goes here
    }
}
شکل ۳. یک کارت پرواز سفارشی قابل ترکیب که با اسکرول کردن در یک لیست تغییر شکل می‌دهد.

سپس می‌توانید با ارسال Modifier.transformedHeight به عنوان اولین اصلاح‌کننده، به همراه SurfaceTransformation(transformationSpec) BoardingPassCard درون یک TransformingLazyColumn استفاده کنید:

@Composable
fun BoardingPassListSample(flights: List<FlightInfo>) {
    val listState = rememberTransformingLazyColumnState()
    val transformationSpec = rememberTransformationSpec()


    ScreenScaffold(scrollState = listState) { contentPadding ->
        TransformingLazyColumn(
            state = listState,
            contentPadding = contentPadding,
            modifier = Modifier.fillMaxSize()
        ) {
            items(flights.size) { index ->
                val flight = flights[index]
                BoardingPassCard(
                    flightNumber = flight.number,
                    origin = flight.origin,
                    destination = flight.destination,
                    gate = flight.gate,
                    seat = flight.seat,
                    departureTime = flight.time,
                    modifier = Modifier
                        .transformedHeight(this, transformationSpec)
                        .minimumVerticalContentPadding(
                            CardDefaults.minimumVerticalListContentPadding
                        ),
                    transformation = SurfaceTransformation(transformationSpec)
                )
            }
        }
    }
}
{% کلمه به کلمه %} {% فعل کمکی %} {% کلمه به کلمه %} {% فعل اضافه %}،

لیست‌ها به کاربران اجازه می‌دهند تا یک مورد را از مجموعه‌ای از گزینه‌ها در دستگاه‌های Wear OS انتخاب کنند.

بسیاری از دستگاه‌های Wear OS از صفحه نمایش‌های گرد استفاده می‌کنند که دیدن آیتم‌های لیست که در نزدیکی بالا و پایین صفحه نمایش ظاهر می‌شوند را دشوارتر می‌کند. به همین دلیل، Compose for Wear OS شامل نسخه‌ای از کلاس LazyColumn به نام TransformingLazyColumn است که از انیمیشن‌های مقیاس‌بندی و تغییر شکل پشتیبانی می‌کند. وقتی آیتم‌ها به لبه‌ها حرکت می‌کنند، کوچک‌تر و محو می‌شوند.

برای اعمال جلوه‌های مقیاس‌بندی و پیمایش توصیه‌شده:

  1. از Modifier.transformedHeight استفاده کنید تا Compose بتواند تغییر ارتفاع را هنگام اسکرول کردن آیتم در صفحه محاسبه کند.
  2. برای اعمال جلوه‌های بصری، از جمله کوچک کردن محتوای آیتم، از transformation = SurfaceTransformation(transformationSpec) استفاده کنید.
  3. برای کامپوننت‌هایی که transformation به عنوان پارامتر نمی‌پذیرند، مانند Text از یک TransformationSpec سفارشی استفاده کنید.

انیمیشن زیر نشان می‌دهد که چگونه یک عنصر لیست هنگام نزدیک شدن به بالا و پایین صفحه، تغییر اندازه می‌دهد و شکل آن تغییر می‌کند:

قطعه کد زیر نحوه ایجاد یک لیست با استفاده از طرح TransformingLazyColumn را نشان می‌دهد تا محتوایی ایجاد شود که در اندازه‌های مختلف صفحه نمایش Wear OS عالی به نظر برسد .

این قطعه کد همچنین استفاده از اصلاح‌کننده‌ی minimumVerticalContentPadding را نشان می‌دهد که باید آن را روی آیتم‌های لیست تنظیم کنید تا فاصله‌ی (padding) صحیح در بالا و پایین لیست اعمال شود.

برای نمایش نشانگر اسکرول، columnState بین ScreenScaffold و TransformingLazyColumn به اشتراک بگذارید:

val columnState = rememberTransformingLazyColumnState()
val transformationSpec = rememberTransformationSpec()
ScreenScaffold(
    scrollState = columnState
) { contentPadding ->
    TransformingLazyColumn(
        state = columnState,
        contentPadding = contentPadding
    ) {
        item {
            ListHeader(
                modifier = Modifier
                    .fillMaxWidth()
                    .transformedHeight(this, transformationSpec)
                    .minimumVerticalContentPadding(ListHeaderDefaults.minimumTopListContentPadding),
                transformation = SurfaceTransformation(transformationSpec)
            ) {
                Text(text = "Header")
            }
        }
        // ... other items
        item {
            Button(
                modifier = Modifier
                    .fillMaxWidth()
                    .transformedHeight(this, transformationSpec)
                    .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding),
                transformation = SurfaceTransformation(transformationSpec),
                onClick = { /* ... */ },
                icon = {
                    Icon(
                        imageVector = Icons.Default.Build,
                        contentDescription = "build",
                    )
                },
            ) {
                Text(
                    text = "Build",
                    maxLines = 1,
                    overflow = TextOverflow.Ellipsis,
                )
            }
        }
    }
}

یک افکت ضربه محکم و ناگهانی اضافه کنید

قابلیت Snapping تضمین می‌کند که وقتی کاربر اسکرول یا حرکت Fling را تمام می‌کند، لیست با یک آیتم که دقیقاً در یک نقطه خاص، معمولاً مرکز صفحه، قرار گرفته است، تنظیم شود. در صفحه‌های نمایش گرد، که آیتم‌ها با دور شدن از مرکز، تغییر اندازه می‌دهند و شکل می‌گیرند، Snapping به ویژه برای اطمینان از اینکه مرتبط‌ترین آیتم در ناحیه دید بهینه کاملاً قابل مشاهده و خوانا باقی می‌ماند، مفید است.

برای افزودن رفتار snap-and-fling، پارامتر flingBehavior را روی TransformingLazyColumnDefaults.snapFlingBehavior(columnState) تنظیم کنید. برای ایجاد یک تجربه سازگار هنگام استفاده از تاج یا قاب فیزیکی، rotaryScrollableBehavior را با استفاده از RotaryScrollableDefaults.snapBehavior(columnState) مطابقت دهید.

val columnState = rememberTransformingLazyColumnState()
ScreenScaffold(scrollState = columnState) { contentPadding ->
    TransformingLazyColumn(
        state = columnState,
        flingBehavior = TransformingLazyColumnDefaults.snapFlingBehavior(columnState),
        rotaryScrollableBehavior = RotaryScrollableDefaults.snapBehavior(columnState)
    ) {
        // ...
        // ...
    }
}

طرح‌بندی معکوس

به طور پیش‌فرض، یک لیست قابل اسکرول به لبه بالایی خود متصل می‌شود. اگر کاربر به پایین یک لیست استاندارد اسکرول کرده باشد و یک آیتم جدید به انتهای آن اضافه شود، لیست نمای کاربر را روی آیتم فعلی حفظ می‌کند. به عنوان مثال، اگر کاربر در حال مشاهده آیتم ۱۰ در پایین صفحه باشد و آیتم ۱۱ اضافه شود، نما روی آیتم ۱۰ متمرکز می‌ماند و آیتم ۱۱ خارج از صفحه و در زیر نمای فعلی ظاهر می‌شود.

برای مواردی مانند برنامه‌های پیام‌رسان یا گزارش‌های زنده، این رفتار معمولاً مطلوب نیست. وقتی موارد جدید می‌رسند، کاربران معمولاً می‌خواهند آخرین محتوا را فوراً ببینند، اگر در پایین لیست باشند. اگر موارد زیادی به طور همزمان برسند، لیست باید پرش کند تا آخرین مورد را در پایین نمایش دهد (به این معنی که برخی از موارد میانی ممکن است اصلاً نمایش داده نشوند مگر اینکه کاربر به بالا اسکرول کند).

برای پشتیبانی از این موارد استفاده، TransformingLazyColumn به شما امکان می‌دهد با تنظیم reverseLayout = true ، طرح‌بندی را معکوس کنید. این کار لنگر لیست را از لبه بالا به لبه پایین تغییر می‌دهد.

برای راحتی، تنظیم reverseLayout = true ترتیب بصری آیتم‌ها و جهت اسکرول کردن را نیز معکوس می‌کند:

  • آیتم‌ها از پایین به بالا تشکیل می‌شوند، به این معنی که اندیس ۰ در پایین صفحه نمایش داده می‌شود.
  • با اسکرول کردن به بالا، آیتم‌هایی با شاخص‌های بالاتر نمایش داده می‌شوند.

برای افزودن رفتار snap-and-fling به همراه طرح‌بندی معکوس، می‌توانید flingBehavior و rotaryScrollableBehavior را همانطور که در قطعه کد زیر نشان داده شده است، ترکیب کنید:

val columnState = rememberTransformingLazyColumnState()
val transformationSpec = rememberTransformationSpec()
ScreenScaffold(scrollState = columnState) { contentPadding ->
    TransformingLazyColumn(
        state = columnState,
        contentPadding = contentPadding,
        reverseLayout = true,
        modifier = Modifier.fillMaxWidth()
    ) {
        items(10) { index ->
            Button(
                label = {
                    Text(
                        text = "Item ${index + 1}"
                    )
                },
                onClick = {},
                modifier = Modifier
                    .fillMaxWidth()
                    .transformedHeight(this, transformationSpec)
                    .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding),
                transformation = SurfaceTransformation(transformationSpec)
            )
        }
        item {
            // With reverseLayout = true, the last item declared appears at the top.
            ListHeader(
                modifier = Modifier
                    .fillMaxWidth()
                    .transformedHeight(this, transformationSpec)
                    .minimumVerticalContentPadding(ListHeaderDefaults.minimumTopListContentPadding),
                transformation = SurfaceTransformation(transformationSpec)
            ) {
                Text("Header")
            }
        }
    }
}

تصاویر زیر تفاوت بین یک لیست معمولی و یک لیست معکوس را نشان می‌دهند:

یک ستون TransformingLazyColumn با طرح‌بندی معمولی، که آیتم ۱ را در بالا و آیتم‌ها را به ترتیب صعودی نشان می‌دهد.
شکل ۱. یک طرح‌بندی استاندارد فهرست که در آن محتوا از بالا به پایین پر می‌شود.
یک ستون TransformingLazyColumn با طرح‌بندی معکوس، که آیتم ۱ را در پایین و آیتم‌ها را به ترتیب نزولی به سمت بالا نشان می‌دهد.
شکل ۲. طرح‌بندی لیست معکوس که در آن محتوا از پایین به بالا پر می‌شود.

دکمه‌های لبه‌ای در لیست‌ها

برای متریال ۳، می‌توانید یک EdgeButton اضافه کنید که یک دکمه‌ی لبه‌دار در پایین لیست‌ها است. با این حال، مراقب باشید که این را به عنوان یک آیتم در TransformingLazyColumn اضافه نکنید، بلکه از جایگاه edgeButton در ScreenScaffold استفاده کنید.

استفاده از اسلات edgeButton تضمین می‌کند که دکمه به درستی در پایین صفحه قرار گرفته و هنگام پیمایش لیست، رفتار مناسبی داشته باشد.

قطعه کد زیر نحوه پیاده‌سازی EdgeButton را نشان می‌دهد:

val columnState = rememberTransformingLazyColumnState()
ScreenScaffold(
    scrollState = columnState,
    edgeButton = {
        EdgeButton(
            onClick = { /* TODO */ },
            modifier = Modifier.scrollable(
                columnState,
                orientation = Orientation.Vertical,
                reverseDirection = true,
                // Apply overscroll to the EdgeButton for proper scrolling behavior.
                overscrollEffect = rememberOverscrollEffect(),
            )
        ) {
            Text("More")
        }
    }
) { contentPadding ->
    TransformingLazyColumn(
        contentPadding = contentPadding,
        state = columnState,
    ) {
        // ...
        // ...
    }
}

کشیدن انگشت برای آشکار کردن در لیست‌ها

کامپوننت SwipeToReveal به شما امکان می‌دهد با کشیدن انگشت، به اقدامات مربوط به یک آیتم لیست، مانند Card یا Chip ، دسترسی پیدا کنید. کشیدن انگشت معمولاً یک یا دو دکمه عملیاتی (مانند "حذف" یا "بیشتر") را در کنار صفحه نمایش می‌دهد.

هنگام استفاده از SwipeToReveal درون یک TransformingLazyColumn ، این دستورالعمل‌ها را دنبال کنید:

  • تنظیم مجدد هنگام اسکرول : وقتی کاربر لیست را اسکرول می‌کند، هر آیتمی که با کشیدن انگشت باز شده را به حالت پوشیده شده‌اش برمی‌گرداند.
  • ارتفاع‌های ثابت : ارتفاع دکمه‌های عملیاتی را طوری تنظیم کنید که با آیتم داخلی که روی آن کشیده می‌شود (چه یک Button باشد و چه یک Card ) مطابقت داشته باشد تا ظاهری ثابت داشته باشد.
  • تغییر شکل دادن کانتینر : اصلاح‌کننده transformedHeight و transformationSpec را روی خود کامپوننت SwipeToReveal اعمال کنید.
  • تبدیل دوگانه انجام ندهید : اصلاح‌کننده‌های transformedHeight یا transformation را روی آیتم داخلیِ سوایپ‌شده (کارت یا دکمه‌ی داخل کانتینر SwipeToReveal ) اعمال نکنید.

ترکیبات سفارشی در لیست‌ها

هنگام ساخت اجزای سطح سفارشی برای TransformingLazyColumn ، این بهترین شیوه‌ها را دنبال کنید تا عناصر قابل ترکیب شما به آرامی در نزدیکی لبه‌های صفحه نمایش تغییر اندازه، محو شدن و شکل بگیرند:

  • Expose SurfaceTransformation : یک پارامتر اختیاری SurfaceTransformation (با مقدار پیش‌فرض null ) را می‌پذیرد که با کامپوننت‌های استاندارد Wear Compose Material 3 مانند Card و Button مطابقت دارد. این به فراخوانی‌کنندگان در یک TransformingLazyColumn اجازه می‌دهد SurfaceTransformation(transformationSpec) را ارسال کنند، در حالی که به کامپوننت اجازه می‌دهد به طور معمول خارج از یک لیست کار کند.
  • ابتدا در کد فراخوانی Modifier.transformedHeight اعمال کنید : هنگام قرار دادن composable سفارشی خود در یک TransformingLazyColumn ، Modifier.transformedHeight(this, transformationSpec) را به عنوان اولین اصلاح‌کننده در زنجیره اصلاح‌کننده کد فراخوانی‌شده ارسال کنید. در حالی که SurfaceTransformation جلوه‌های مقیاس‌بندی بصری و محو شدن را اعمال می‌کند، transformedHeight برای گفتن به طرح‌بندی لیست برای محاسبه مجدد ارتفاع آیتم هنگام کوچک شدن، بسیار مهم است.
  • لایه‌های تبدیل، modifier فراخوانی‌کننده و نقاش را به ترتیب اعمال کنید :
    1. لایه تبدیل کانتینر : زنجیره اصلاح‌کننده کانتینر ریشه را با Modifier.graphicsLayer شروع کنید و applyContainerTransformation() ، به طوری که هم پس‌زمینه و هم محتوا در فضای مختصات مقیاس‌بندی شده و کج‌شده ترسیم شوند.
    2. modifier فراخوانی‌کننده : پارامتر modifier ارسال‌شده توسط فراخوانی‌کننده (که شامل Modifier.transformedHeight می‌شود) را در مرحله‌ی بعد، قبل از هرگونه تغییر اندازه یا فاصله‌گذاری داخلی، اعمال کنید.
    3. برش شکل وقتی هیچ تبدیلی وجود ندارد : اگر transformation null باشد، قبل از ترسیم پس‌زمینه، Modifier.clip(shape) را اعمال کنید. نقاش برگردانده شده توسط createContainerPainter() خودش را به شکل می‌چسباند، اما یک نقاش ساده این کار را نمی‌کند، بنابراین بدون این، پس‌زمینه با گوشه‌های مربعی خارج از یک لیست ترسیم می‌شود.
    4. نقاش پس‌زمینه‌ی تغییرشکل‌دهنده : با استفاده از Modifier.drawBehind و یک نقاش ایجاد شده از createContainerPainter() پس‌زمینه را درون لایه‌ی ظرف رسم کنید.
    5. لایه تبدیل محتوا : یک لایه دوم Modifier.graphicsLayer با استفاده applyContentTransformation() اعمال کنید و آن را به شکل ظرف بچسبانید تا محتوای داخلی با نزدیک شدن به قاب، زودتر محو شود.

قطعه کد زیر نحوه پیاده‌سازی یک BoardingPassCard سفارشی را نشان می‌دهد که این تبدیل‌ها را به ترتیب اعمال می‌کند:

@Composable
fun BoardingPassCard(
    flightNumber: String,
    origin: String,
    destination: String,
    gate: String,
    seat: String,
    departureTime: String,
    modifier: Modifier = Modifier,
    transformation: SurfaceTransformation? = null,
    shape: Shape = RoundedCornerShape(18.dp),
    statusBadge: @Composable () -> Unit = {}
) {
    // 1. Create morphing container painter
    val backgroundPainter = ColorPainter(MaterialTheme.colorScheme.surfaceContainer)
    val finalPainter = if (transformation != null) {
        remember(transformation, backgroundPainter, shape) {
            transformation.createContainerPainter(backgroundPainter, shape, border = null)
        }
    } else {
        backgroundPainter
    }

    Column(
        modifier = Modifier
            // 2a. Container layer: Scales, fades, and tilts the whole card surface
            .then(
                if (transformation != null) {
                    Modifier.graphicsLayer {
                        transformation.run { applyContainerTransformation() }
                    }
                } else Modifier
            )
            // 2b. Caller modifier: Includes Modifier.transformedHeight in a list
            .then(modifier)
            .fillMaxWidth()
            // 2c. Shape clip: Only needed without a transformation, because the
            // painter from createContainerPainter clips itself to the shape
            .then(if (transformation == null) Modifier.clip(shape) else Modifier)
            // 2d. Morphing background: Drawn inside the transformed container layer
            .drawBehind {
                with(finalPainter) {
                    draw(size)
                }
            }
            // 2e. Content layer: Fades content earlier and clips children to shape
            .then(
                if (transformation != null) {
                    Modifier.graphicsLayer {
                        this.shape = shape
                        this.clip = true
                        transformation.run { applyContentTransformation() }
                    }
                } else Modifier
            )
            .padding(horizontal = 14.dp, vertical = 10.dp)
    ) {
        // Card content goes here
    }
}
شکل ۳. یک کارت پرواز سفارشی قابل ترکیب که با اسکرول کردن در یک لیست تغییر شکل می‌دهد.

سپس می‌توانید با ارسال Modifier.transformedHeight به عنوان اولین اصلاح‌کننده، به همراه SurfaceTransformation(transformationSpec) BoardingPassCard درون یک TransformingLazyColumn استفاده کنید:

@Composable
fun BoardingPassListSample(flights: List<FlightInfo>) {
    val listState = rememberTransformingLazyColumnState()
    val transformationSpec = rememberTransformationSpec()


    ScreenScaffold(scrollState = listState) { contentPadding ->
        TransformingLazyColumn(
            state = listState,
            contentPadding = contentPadding,
            modifier = Modifier.fillMaxSize()
        ) {
            items(flights.size) { index ->
                val flight = flights[index]
                BoardingPassCard(
                    flightNumber = flight.number,
                    origin = flight.origin,
                    destination = flight.destination,
                    gate = flight.gate,
                    seat = flight.seat,
                    departureTime = flight.time,
                    modifier = Modifier
                        .transformedHeight(this, transformationSpec)
                        .minimumVerticalContentPadding(
                            CardDefaults.minimumVerticalListContentPadding
                        ),
                    transformation = SurfaceTransformation(transformationSpec)
                )
            }
        }
    }
}
{% کلمه به کلمه %} {% فعل کمکی %} {% کلمه به کلمه %} {% فعل کمکی %}