רשימות מאפשרות למשתמשים לבחור פריט מתוך קבוצת אפשרויות במכשירי 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 מאפשר לכם לגשת לפעולות של פריט ברשימה, כמו Card או Chip, באמצעות החלקה. בדרך כלל, בהחלקה מופיעים מהצד לחצן פעולה אחד או שניים (למשל 'מחיקה' או 'עוד').
כשמשתמשים ב-SwipeToReveal בתוך TransformingLazyColumn, חשוב לפעול לפי ההנחיות הבאות:
- איפוס בגלילה: כשמשתמש גולל ברשימה, כל הפריטים שנפתחו בהחלקה יאופסו למצב המכוסה שלהם.
- גבהים עקביים: מגדירים את הגובה של כפתורי הפעולה כך שיתאים לגובה של הפריט הפנימי שמופיע אחרי ההחלקה (בין אם מדובר ב
Buttonאו בCard) כדי להבטיח מראה עקבי. - שינוי מאגר התגים: מוסיפים את המשנה
transformedHeightואתtransformationSpecלרכיבSwipeToRevealעצמו. - אל תבצעו טרנספורמציה כפולה: אל תחיל את המשנים
transformedHeightאוtransformationעל הפריט הפנימי שניתן להחליק (הכרטיס או הלחצן בתוך מאגר התגיםSwipeToReveal).
רכיבי Composable מותאמים אישית ברשימות
כשיוצרים רכיבי פלטפורמה בהתאמה אישית ל-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(), כך שגם הרקע וגם התוכן מצוירים במרחב הקואורדינטות המוקטן והמוטה. - Caller
modifier: הפרמטרmodifierשמועבר על ידי המתקשר (כוללModifier.transformedHeight) יוחל בשלב הבא, לפני כל שינוי גודל או ריווח פנימי. - Shape clip when there is no transformation: If
transformationisnull, applyModifier.clip(shape)before drawing the background. הצייר שמוחזר על ידיcreateContainerPainter()חותך את עצמו לצורה, אבל צייר רגיל לא עושה את זה, ולכן בלי זה הרקע מצויר עם פינות מרובעות מחוץ לרשימה. - Morphing background painter: ציור הרקע בתוך שכבת הקונטיינר באמצעות
Modifier.drawBehindוצייר שנוצר מ-createContainerPainter(). - שכבת שינוי תוכן: אפשר להחיל
Modifier.graphicsLayerשני עםapplyContentTransformation()ולחתוך את הצורה של מאגר התוכן כדי שהתוכן הפנימי ידהה מוקדם יותר כשהוא מתקרב למסגרת.
- שכבת טרנספורמציה של מאגר התגים: מתחילים את שרשרת המשנים של מאגר התגים הבסיסי עם
בקטע הקוד הבא אפשר לראות איך מטמיעים BoardingPassCardComposable מותאם אישית שמחיל את השינויים האלה לפי הסדר:
@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 Codelab
- רשימות ורשתות
- שימוש ב-Views ב-Compose