Styles API מציע גישה הצהרתית ויעילה לניהול שינויים בממשק המשתמש במהלך מצבי אינטראקציה כמו hovered, focused ו-pressed. באמצעות ה-API הזה, אפשר להפחית באופן משמעותי את קוד ה-boilerplate שנדרש בדרך כלל כשמשתמשים בשינויים.
כדי לאפשר עיצוב תגובתי, StyleState פועל כממשק יציב לקריאה בלבד שעוקב אחרי המצב הפעיל של רכיב (למשל, הסטטוס שלו: מופעל, נלחץ או מודגש). בתוך StyleScope, אפשר לגשת לזה דרך המאפיין state כדי להטמיע לוגיקה מותנית ישירות בהגדרות הסגנון.
אינטראקציה מבוססת-מצב: ריחוף, מיקוד, לחיצה, בחירה, הפעלה, החלפה
הסגנונות כוללים תמיכה מובנית באינטראקציות נפוצות:
- לחוץ
- הסמן הוצב מעל
- נבחר
- מופעל
- הסוג הוחלף
אפשר גם לתמוך במצבים מותאמים אישית. מידע נוסף זמין בקטע התאמה אישית של סגנונות של מצבים באמצעות StyleState.
טיפול במצבי אינטראקציה באמצעות פרמטרים של סגנון
בדוגמה הבאה מוצגת שינוי של background ושל borderColor בתגובה למצבי אינטראקציה, במיוחד מעבר לסגול כשמעבירים את העכבר מעל הרכיב ולכחול כשמתמקדים בו:
@Preview @Composable private fun OpenButton() { BaseButton( style = outlinedButtonStyle then { background(Color.White) hovered { background(lightPurple) border(2.dp, lightPurple) } focused { background(lightBlue) } }, onClick = { }, content = { BaseText("Open in Studio", style = { contentColor(Color.Black) fontSize(26.sp) textAlign(TextAlign.Center) }) } ) }
אפשר גם ליצור הגדרות מצב מקוננות. לדוגמה, אפשר להגדיר סגנון ספציפי למצב שבו גם לוחצים על כפתור וגם מעבירים מעליו את העכבר בו-זמנית:
@Composable private fun OpenButton_CombinedStates() { BaseButton( style = outlinedButtonStyle then { background(Color.White) hovered { // light purple background(lightPurple) pressed { // When running on a device that can hover, whilst hovering and then pressing the button this would be invoked background(lightOrange) } } pressed { // when running on a device without a mouse attached, this would be invoked as you wouldn't be in a hovered state only background(lightRed) } focused { background(lightBlue) } }, onClick = { }, content = { BaseText("Open in Studio", style = { contentColor(Color.Black) fontSize(26.sp) textAlign(TextAlign.Center) }) } ) }
רכיבים מורכבים בהתאמה אישית עם Modifier.styleable
כשיוצרים רכיבי styleable משלכם, צריך לקשר interactionSource ל-styleState. אחר כך מעבירים את המצב הזה אל Modifier.styleable כדי להשתמש בו.
נניח שמערכת העיצוב שלכם כוללת GradientButton. אפשר ליצור LoginButton שמוגדר כצאצא של GradientButton, אבל הצבעים שלו משתנים במהלך אינטראקציות, כמו לחיצה.
- כדי להפעיל עדכוני סגנון של
interactionSource, צריך לכלול אתinteractionSourceכפרמטר בתוך הרכיב הניתן להרכבה. משתמשים בפרמטר שסופק, או אם לא סופק פרמטר, מאתחליםMutableInteractionSourceחדש. - מפעילים את
styleStateעל ידי ציוןinteractionSource. צריך לוודא שסטטוס ההפעלה שלstyleStateמשקף את הערך של הפרמטר enabled שצוין. - מקצים את
interactionSourceלשינוייםfocusableו-clickable. לבסוף, מחילים אתstyleStateעל הפרמטרstyleableשל המשנה.
@Composable private fun GradientButton( onClick: () -> Unit, modifier: Modifier = Modifier, style: Style = Style, enabled: Boolean = true, interactionSource: MutableInteractionSource? = null, content: @Composable RowScope.() -> Unit, ) { val interactionSource = interactionSource ?: remember { MutableInteractionSource() } val styleState = remember(interactionSource) { MutableStyleState(interactionSource) } styleState.isEnabled = enabled Row( modifier = modifier .clickable( onClick = onClick, enabled = enabled, interactionSource = interactionSource, indication = null, ) .styleable(styleState, baseGradientButtonStyle then style), content = content, ) }
עכשיו אפשר להשתמש במצב interactionSource כדי לבצע שינויים בסגנון באמצעות האפשרויות pressed, focused ו-hovered בתוך בלוק הסגנון:
@Preview @Composable fun LoginButton() { val loginButtonStyle = Style { pressed { background( Brush.linearGradient( listOf(Color.Magenta, Color.Red) ) ) } } GradientButton(onClick = { // Login logic }, style = loginButtonStyle) { BaseText("Login") } }
interactionSource.הנפשת שינויים בסגנון
שינויים במצב של סגנונות מגיעים עם תמיכה מובנית באנימציה. אפשר להוסיף את המאפיין החדש לכל בלוק של שינוי מצב עם animate כדי להוסיף אוטומטית אנימציות בין מצבים שונים. הוא דומה לממשקי ה-API של animate*AsState. בדוגמה הבאה, הצבע של borderColor משתנה משחור לכחול כשהמצב משתנה למצב ממוקד:
val animatingStyle = Style { externalPadding(48.dp) border(3.dp, Color.Black) background(Color.White) size(100.dp) pressed { animate { borderColor(Color.Magenta) background(Color(0xFFB39DDB)) } } } @Preview @Composable private fun AnimatingStyleChanges() { val interactionSource = remember { MutableInteractionSource() } val styleState = remember(interactionSource) { MutableStyleState(interactionSource) } Box(modifier = Modifier .clickable( interactionSource, enabled = true, indication = null, onClick = { } ) .styleable(styleState, animatingStyle)) { } }
ה-API של animate מקבל animationSpec כדי לשנות את משך הזמן או את הצורה של עקומת האנימציה. בדוגמה הבאה מוגדרת אנימציה של גודל התיבה באמצעות מפרט spring:
val animatingStyleSpec = Style { externalPadding(48.dp) border(3.dp, Color.Black) background(Color.White) size(100.dp) transformOrigin(TransformOrigin.Center) pressed { animate { borderColor(Color.Magenta) background(Color(0xFFB39DDB)) } animate(spring(dampingRatio = Spring.DampingRatioMediumBouncy)) { scale(1.2f) } } } @Preview(showBackground = true) @Composable fun AnimatingStyleChangesSpec() { val interactionSource = remember { MutableInteractionSource() } val styleState = remember(interactionSource) { MutableStyleState(interactionSource) } Box(modifier = Modifier .clickable( interactionSource, enabled = true, indication = null, onClick = { } ) .styleable(styleState, animatingStyleSpec)) }
עיצוב מצב מותאם אישית באמצעות StyleState
בהתאם לתרחיש השימוש שלכם ברכיבים שאפשר להרכיב, יכול להיות שיהיו לכם סגנונות שונים שמגובים על ידי מצבים מותאמים אישית. לדוגמה, אם יש לכם אפליקציית מדיה, יכול להיות שתרצו להגדיר סגנון שונה ללחצנים ב-MediaPlayer composable בהתאם למצב ההפעלה של הנגן. כדי ליצור מצב מותאם אישית משלכם ולהשתמש בו:
- הגדרת מפתח בהתאמה אישית
- יצירת תוסף
StyleState - קישור למצב מותאם אישית
הגדרת מפתח בהתאמה אישית
כדי ליצור סגנון מותאם אישית שמבוסס על מצב, קודם יוצרים StyleStateKey ומעבירים את ערך ברירת המחדל של המצב. כשהאפליקציה מופעלת, נגן המדיה נמצא במצב Stopped, ולכן הוא מאותחל באופן הבא:
enum class PlayerState { Stopped, Playing, Paused } val playerStateKey = StyleStateKey(PlayerState.Stopped)
יצירה של פונקציות הרחבה של StyleState
הגדרת פונקציית תוסף ב-StyleState כדי לשלוח שאילתה לגבי playState הנוכחי.
לאחר מכן, יוצרים פונקציות הרחבה ב-StyleScope עם המצבים המותאמים אישית שמועברים ב-playStateKey, למדא עם המצב הספציפי והסגנון.
// Extension Function on MutableStyleState to query and set the current playState var MutableStyleState.playerState get() = this[playerStateKey] set(value) { this[playerStateKey] = value } fun StyleScope.playerPlaying(value: Style) { state(playerStateKey, value, { key, state -> state[key] == PlayerState.Playing }) } fun StyleScope.playerPaused(value: Style) { state(playerStateKey, value, { key, state -> state[key] == PlayerState.Paused }) }
קישור למצב מותאם אישית
מגדירים את styleState ברכיב הניתן להרכבה וקובעים ש-styleState.playState יהיה שווה למצב הנכנס. מעבירים את styleState לפונקציה styleable בשינוי.
@Composable fun MediaPlayer( url: String, modifier: Modifier = Modifier, style: Style = Style, state: PlayerState = remember { PlayerState.Paused } ) { // Hoist style state, set playstate as a parameter, val styleState = remember { MutableStyleState(null) } // Set equal to incoming state to link the two together styleState.playerState = state Box( modifier = modifier.styleable(styleState, style)) { ///.. } }
בתוך ה-lambda של style, אפשר להחיל עיצוב מבוסס-מצב למצבים מותאמים אישית, באמצעות פונקציות ההרחבה שהוגדרו קודם.
@Composable fun StyleStateKeySample() { // Using the extension function to change the border color to green while playing val style = Style { borderColor(Color.Gray) playerPlaying { animate { borderColor(Color.Green) } } playerPaused { animate { borderColor(Color.Blue) } } } val styleState = remember { MutableStyleState(null) } styleState[playerStateKey] = PlayerState.Playing // Using the style in a composable that sets the state -> notice if you change the state parameter, the style changes. You can link this up to an ViewModel and change the state from there too. MediaPlayer(url = "https://example.com/media/video", style = style, state = PlayerState.Stopped) }
קטע הקוד המלא לדוגמה הזו:
enum class PlayerState { Stopped, Playing, Paused } val playerStateKey = StyleStateKey<PlayerState>(PlayerState.Stopped) var MutableStyleState.playerState get() = this[playerStateKey] set(value) { this[playerStateKey] = value } fun StyleScope.playerPlaying(value: Style) { state(playerStateKey, value, { key, state -> state[key] == PlayerState.Playing }) } fun StyleScope.playerPaused(value: Style) { state(playerStateKey, value, { key, state -> state[key] == PlayerState.Paused }) } @Composable fun MediaPlayer( url: String, modifier: Modifier = Modifier, style: Style = Style, state: PlayerState = remember { PlayerState.Paused } ) { // Hoist style state, set playstate as a parameter, val styleState = remember { MutableStyleState(null) } // Set equal to incoming state to link the two together styleState.playerState = state Box( modifier = modifier.styleable(styleState, Style { size(100.dp) border(2.dp, Color.Red) }, style, )) { ///.. } } @Composable fun StyleStateKeySample() { // Using the extension function to change the border color to green while playing val style = Style { borderColor(Color.Gray) playerPlaying { animate { borderColor(Color.Green) } } playerPaused { animate { borderColor(Color.Blue) } } } val styleState = remember { MutableStyleState(null) } styleState[playerStateKey] = PlayerState.Playing // Using the style in a composable that sets the state -> notice if you change the state parameter, the style changes. You can link this up to an ViewModel and change the state from there too. MediaPlayer(url = "https://example.com/media/video", style = style, state = PlayerState.Stopped) }