تتيح القوائم للمستخدمين اختيار عنصر من مجموعة خيارات على أجهزة Wear OS.
تستخدم العديد من أجهزة Wear OS شاشات دائرية، ما يصعّب رؤية عناصر القائمة التي تظهر بالقرب من أعلى الشاشة وأسفلها. لهذا السبب، يتضمّن Compose for Wear OS إصدارًا من الفئة LazyColumn يُسمى TransformingLazyColumn، ويتيح هذا الإصدار تغيير الحجم والرسوم المتحركة التي تتضمّن تغيير الشكل.
عندما تنتقل العناصر إلى الحواف، تصبح أصغر حجمًا وتتلاشى.
لتطبيق تأثيرات التكبير والتصغير والتمرير المقترَحة، اتّبِع الخطوات التالية:
- استخدِم
Modifier.transformedHeightللسماح لـ Compose باحتساب التغيير في الارتفاع أثناء تنقّل العنصر على الشاشة. - استخدِم
transformation = SurfaceTransformation(transformationSpec)لتطبيق التأثيرات المرئية، بما في ذلك تصغير محتوى العنصر. - استخدِم
TransformationSpecمخصّصًا للمكوّنات التي لا تأخذtransformationكمَعلمة، مثلText.
تعرض الصورة المتحركة التالية كيفية تغيير حجم عنصر القائمة وشكله عند الاقتراب من أعلى الشاشة وأسفلها:
يوضّح مقتطف الرمز التالي كيفية إنشاء قائمة باستخدام التصميم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) ) { // ... // ... } }
التنسيق المعكوس
تتم تلقائيًا تثبيت قائمة قابلة للتمرير إلى حافتها العلوية. إذا انتقل المستخدم إلى أسفل قائمة عادية وتمت إضافة عنصر جديد إلى النهاية، ستحافظ القائمة على طريقة عرض المستخدم للعنصر الحالي. على سبيل المثال، إذا كان المستخدم يعرض العنصر 10 في أسفل الشاشة، وتمت إضافة العنصر 11، سيظل العرض مركّزًا على العنصر 10، وسيظهر العنصر 11 خارج الشاشة أسفل العرض الحالي.
في حالات الاستخدام، مثل تطبيقات المراسلة أو السجلات المباشرة، لا يكون هذا السلوك مطلوبًا عادةً. عندما تصل عناصر جديدة، يريد المستخدمون عادةً الاطّلاع على أحدث المحتوى على الفور إذا كانوا قد وصلوا إلى أسفل القائمة. إذا وصل عدد كبير من العناصر في الوقت نفسه، يجب أن تنتقل القائمة لعرض أحدث عنصر في الأسفل (ما يعني أنّه قد لا يتم عرض بعض العناصر الوسيطة على الإطلاق ما لم يعود المستخدم إلى الأعلى).
لمعالجة حالات الاستخدام هذه، تتيح لك TransformingLazyColumn عكس التنسيق من خلال ضبط reverseLayout = true. يؤدي ذلك إلى تغيير موضع تثبيت القائمة من الحافة العلوية إلى الحافة السفلية.
لتسهيل الأمر، يؤدي ضبط reverseLayout = true إلى عكس الترتيب المرئي للعناصر واتجاه إيماءات التمرير:
- يتم ترتيب العناصر من الأسفل إلى الأعلى، ما يعني أنّ الفهرس 0 يظهر في أسفل الشاشة.
- يؤدي التمرير للأعلى إلى عرض العناصر ذات المؤشرات الأعلى.
لإضافة سلوك "التقاط وتحريك" مع التنسيق العكسي، يمكنك الجمع بين 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") } } } }
تعرض الصور التالية الفرق بين قائمة عادية وقائمة معكوسة:
أزرار الحافة في القوائم
في Material 3، يمكنك إضافة 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 في القوائم
يتيح لك المكوّن 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أولاً في رمز الاستدعاء: عند وضع دالة مركّبة مخصّصة فيTransformingLazyColumn، مرِّرModifier.transformedHeight(this, transformationSpec)كأول عنصر تعديل في سلسلة عناصر التعديل لرمز الاستدعاء. في حين أنّSurfaceTransformationتطبّق تأثيرات تغيير الحجم والتلاشي المرئية، فإنّtransformedHeightضرورية لإخبار تخطيط القائمة بإعادة احتساب ارتفاع العنصر أثناء تصغيره. - تطبيق طبقات التحويل والبرنامج
modifierوالرسام بالترتيب:- طبقة تحويل الحاوية: ابدأ سلسلة المعدِّلات الخاصة بالحاوية الجذرية باستخدام
Modifier.graphicsLayerوapplyContainerTransformation()، وذلك لكي يتم رسم كل من الخلفية والمحتوى ضمن مساحة الإحداثيات التي تم تغيير حجمها وإمالتها. - المتصل
modifier: طبِّق المَعلمةmodifierالتي يمرّرها المتصل (والتي تتضمّنModifier.transformedHeight) بعد ذلك، قبل أي عملية تغيير حجم أو إضافة مساحة داخلية. - قصاصة الشكل عندما لا يكون هناك تحويل: إذا كانت قيمة
transformationهيnull، طبِّقModifier.clip(shape)قبل رسم الخلفية. يتم قصّ الرسام الذي تعرضهcreateContainerPainter()ليناسب الشكل، ولكن لا يتم قصّ الرسام العادي، لذا بدون هذا الإعداد، يتم رسم الخلفية بزوايا مربّعة خارج القائمة. - أداة رسم الخلفية المتحركة: ارسم الخلفية داخل طبقة الحاوية باستخدام
Modifier.drawBehindوأداة رسم تم إنشاؤها منcreateContainerPainter(). - طبقة تحويل المحتوى: طبِّق
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 } }
يمكنك بعد ذلك استخدام BoardingPassCard داخل TransformingLazyColumn من خلال تمرير
Modifier.transformedHeight كمعدِّل أول، بالإضافة إلى
SurfaceTransformation(transformationSpec):
@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) ) } } } }
اقتراحات مخصصة لك
- ملاحظة: يتم عرض نص الرابط عندما تكون JavaScript غير مفعّلة
- درس تطبيقي حول الترميز: Compose for Wear OS
- القوائم والشبكات
- استخدام طرق العرض في Compose