فهرست‌ها با Compose برای Wear OS


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

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

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

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

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

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

این تکه‌کد همچنین استفاده از minimumVerticalContentPadding اصلاح‌گر را نشان می‌دهد که باید روی عناصر فهرست تنظیم کنید تا فاصله‌گذاری صحیح در بالا و پایین فهرست اعمال شود.

برای نمایش نشانگر پیمایش، 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,
                )
            }
        }
    }
}

افزودن جلوه ضربه و پرتاب

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

برای افزودن رفتار چسباندن و پرتاب کردن، پارامتر 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 همچنین ترتیب دیداری موارد و جهت اشاره‌های پیمایش را معکوس می‌کند:

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

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

  • نمایش SurfaceTransformation: پارامتر اختیاری SurfaceTransformation را بپذیرید (پیش‌فرض null)، با عناصر استاندارد Wear Compose Material 3 مانند Card و Button مطابقت دارد. این کار به تماس‌گیرندگان اجازه می‌دهد در TransformingLazyColumn گذر SurfaceTransformation(transformationSpec) کنند و درعین‌حال به عنصر اجازه می‌دهد خارج از فهرست به‌طور عادی کار کند.
  • ابتدا Modifier.transformedHeight را در کد فراخوانی اعمال کنید: هنگام قرار دادن ترکیب‌پذیر سفارشی در 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. لایه تبدیل محتوا: با applyContentTransformation() و کلیپ Modifier.graphicsLayer دوم را به شکل محتوی اعمال کنید تا محتوای داخلی با نزدیک شدن به قاب زودتر محو شود.

تکه‌کد زیر نحوه پیاده‌سازی 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)
                )
            }
        }
    }
}