שינוי התנהגות המיקוד

לפעמים יש צורך לעקוף את התנהגות ברירת המחדל של המיקוד של הרכיבים שבמסך. לדוגמה, יכול להיות שתרצו לקבץ תכנים קומפוזביליים, להתמקד בתוכן קומפוזבילי מסוים, שמבקשים התמקדות בתוכן אחד, לכוד או לשחרר את המיקוד, או לנתב מחדש את המיקוד בכניסה או ביציאה. הזה בקטע הזה מוסבר איך לשנות את התנהגות המיקוד כשברירות המחדל הן לא צריכים.

יצירת ניווט ברור באמצעות קבוצות מיקוד

לפעמים, 'Jetpack פיתוח נייטיב' לא מנחש מיד את הפריט הבא ניווט באמצעות כרטיסיות, במיוחד כשרכיב הורה מורכב, Composables, כמו כרטיסיות הרשימות נכנסות לתוקף.

אמנם החיפוש ממוקד בדרך כלל תואם לסדר ההצהרה של Composables, במקרים מסוימים, הדבר בלתי אפשרי, למשל כאשר אחד מהComposables ב- ההיררכיה היא פורמט אופקי שניתן לגלילה שאינה גלויה במלואה. מוצג ב בדוגמה הבאה.

'Jetpack פיתוח נייטיב' עשוי להחליט להתמקד בפריט הבא הקרוב ביותר לתחילת במסך, כפי שמוצג למטה, במקום להמשיך בנתיב הצפוי. ניווט חד-כיווני:

אנימציה של אפליקציה שמציגה ניווט אופקי בחלק העליון ורשימת פריטים למטה.
איור 1. אנימציה של אפליקציה שמציגה ניווט אופקי בחלק העליון ורשימת פריטים למטה

בדוגמה הזו, ברור שהמפתחים לא התכוונו שהמיקוד מהכרטיסייה שוקולד לתמונה הראשונה למטה, ואז חזרה אל בכרטיסייה מאפים. במקום זאת, הם רצו שהמיקוד ימשיך בכרטיסיות עד בכרטיסייה האחרונה, ואז מתמקדים בתוכן הפנימי:

אנימציה של אפליקציה שמציגה ניווט אופקי בחלק העליון ורשימת פריטים למטה.
איור 2. אנימציה של אפליקציה שמציגה ניווט אופקי בחלק העליון ורשימת פריטים למטה

במצבים שבהם חשוב שקבוצה של תכנים קומפוזביליים יתמקדו ברצף, כמו בשורת ה-Tab מהדוגמה הקודמת, צריך Composable בהורה עם הצירוף focusGroup():

LazyVerticalGrid(columns = GridCells.Fixed(4)) {
    item(span = { GridItemSpan(maxLineSpan) }) {
        Row(modifier = Modifier.focusGroup()) {
            FilterChipA()
            FilterChipB()
            FilterChipC()
        }
    }
    items(chocolates) {
        SweetsCard(sweets = it)
    }
}

ניווט דו-כיווני מחפש את התוכן הקומפוזבילי הקרוב ביותר כיוון - אם רכיב מקבוצה אחרת קרוב יותר מקבוצה שאינה גלויה במלואה הפריט בקבוצה הנוכחית, הניווט בוחר את הפריט הקרוב ביותר. כדי להימנע מכך ההתנהגות הזו, אפשר להחיל את מקש הצירוף focusGroup().

FocusGroup גורם לקבוצה שלמה להיראות כמו ישות אחת מבחינת המיקוד, אבל הקבוצה עצמה לא תמשוך את תשומת הלב, אלא הילד הקרוב ביותר להתמקד במקום זאת. באופן הזה, הניווט יידע לעבור לפריט שאינו גלוי במלואו לפני עזיבת הקבוצה.

במקרה הזה, שלושת המופעים של FilterChip יתמקדו לפני SweetsCard פריטים, גם אם SweetsCards גלוי באופן מלא משתמש מסוים וייתכן שחלק מ-FilterChip מוסתרים. הסיבה לכך היא מקש הצירוף focusGroup מורה למנהל המיקוד לשנות את הסדר שבו הפריטים כדי שהניווט יהיה קל ועקבי יותר עם ממשק המשתמש.

בלי מקש הצירוף focusGroup, אם FilterChipC לא היה גלוי, המיקוד הוא ישפר אותו בפעם האחרונה. אבל, הוספת התאמה כזו לא מונעת רק ניתן לגילוי, אבל הוא תתמקד גם מיד לאחר FilterChipB, כמו המשתמשים יצפו לכך.

יצירת תוכן קומפוזבילי

יש תכנים קומפוזביליים שאפשר להתמקד בהם בעיצוב, כמו 'לחצן' או תוכן קומפוזבילי עם צירוף הצירוף clickable. אם רוצים להוסיף אפשר להתמקד בפורמט של תוכן קומפוזבילי, צריך להשתמש בתכונת הצירוף focusable:

var color by remember { mutableStateOf(Green) }
Box(
    Modifier
        .background(color)
        .onFocusChanged { color = if (it.isFocused) Blue else Green }
        .focusable()
) {
    Text("Focusable 1")
}

להפוך פריט קומפוזבילי לבלתי ניתן למיקוד

עשויים להיות מצבים שבהם חלק מהאלמנטים לא ייכללו בתוך הפוקוס. במקרים הנדירים האלה, תוכלו להשתמש ב-canFocus property כדי להחריג Composable כך שלא יהיה ניתן למיקוד.

var checked by remember { mutableStateOf(false) }

Switch(
    checked = checked,
    onCheckedChange = { checked = it },
    // Prevent component from being focused
    modifier = Modifier
        .focusProperties { canFocus = false }
)

בקשת מיקוד המקלדת באמצעות FocusRequester

במקרים מסוימים, ייתכן שתרצו לבקש מיקוד באופן מפורש כתגובה לאינטראקציה של המשתמשים. לדוגמה, אפשר לשאול את המשתמש אם הוא רוצה להפעיל מחדש מילוי טופס, ואם הם לחצו על 'כן' אתם רוצים למקד מחדש את השדה הראשון של הטופס הזה.

הדבר הראשון שצריך לעשות הוא לשייך אובייקט FocusRequester אל תוכן קומפוזבילי שאליו רוצים להעביר את מיקוד המקלדת. בקוד הבא קטע קוד, אובייקט FocusRequester משויך ל-TextField על ידי הגדרה של מקש הצירוף Modifier.focusRequester:

val focusRequester = remember { FocusRequester() }
var text by remember { mutableStateOf("") }

TextField(
    value = text,
    onValueChange = { text = it },
    modifier = Modifier.focusRequester(focusRequester)
)

אפשר לקרוא לשיטת requestFocus של FocusRequester כדי לשלוח בקשות למיקוד בפועל. צריך להפעיל את השיטה הזו מחוץ להקשר Composable (אחרת, הוא מופעל מחדש בכל יצירה מחדש). קטע הקוד הבא מראה איך לבקש מהמערכת להזיז את מיקוד המקלדת כשהלחצן לחצת:

val focusRequester = remember { FocusRequester() }
var text by remember { mutableStateOf("") }

TextField(
    value = text,
    onValueChange = { text = it },
    modifier = Modifier.focusRequester(focusRequester)
)

Button(onClick = { focusRequester.requestFocus() }) {
    Text("Request focus on TextField")
}

צילום ושחרור המיקוד

אפשר למנף את המיקוד כדי להנחות את המשתמשים לספק את הנתונים המתאימים לאפליקציה לבצע את המשימה שלו, כמו קבלת כתובת אימייל או מספר טלפון תקינים מספר. למרות שמצבי שגיאה מיידעים את המשתמשים לגבי מה שקורה, אולי יזדקק לשדה עם מידע שגוי כדי להישאר ממוקדים עד שהוא תוקן.

כדי לצלם את המיקוד, אפשר להפעיל את השיטה captureFocus(). אחר כך משחררים אותו ב-method freeFocus(), כמו בדוגמה הבאה דוגמה:

val textField = FocusRequester()

TextField(
    value = text,
    onValueChange = {
        text = it

        if (it.length > 3) {
            textField.captureFocus()
        } else {
            textField.freeFocus()
        }
    },
    modifier = Modifier.focusRequester(textField)
)

הקדימות של מגבילי המיקוד

Modifiers יכול להופיע כרכיבים שיש להם רק צאצא אחד, כך שכאשר הם נמצאים בתור כל Modifier בצד שמאל (או למעלה) כולל את ה-Modifier שעוקב הימני (או מתחתיו). המשמעות היא שהModifier השני נמצא בתוך הראשון, כך שכשמצהירים על שני focusProperties, רק החלק העליון אחת עובדת, כי הערכים הבאים נמצאים בחלק העליון.

כדי להבהיר את הנושא יותר, כדאי לעיין בקוד הבא:

Modifier
    .focusProperties { right = item1 }
    .focusProperties { right = item2 }
    .focusable()

במקרה הזה, focusProperties שמציין את item2 כמיקוד הנכון לא להשתמש בו, כפי שהוא נכלל בתנאי הקודם; ולכן, item1 יהיה שבו נעשה שימוש.

בעזרת הגישה הזו, ההורים יכולים לאפס את תבנית ההתנהגות לברירת המחדל: באמצעות FocusRequester.Default:

Modifier
    .focusProperties { right = Default }
    .focusProperties { right = item1 }
    .focusProperties { right = item2 }
    .focusable()

ההורה לא חייב להיות חלק מאותה שרשרת מגביל. הורה תוכן קומפוזבילי יכול להחליף מאפיין מיקוד של תוכן צאצא קומפוזבילי. לדוגמה, נבחן את הFancyButton הבא שלא מאפשר להתמקד בלחצן:

@Composable
fun FancyButton(modifier: Modifier = Modifier) {
    Row(modifier.focusProperties { canFocus = false }) {
        Text("Click me")
        Button(onClick = { }) { Text("OK") }
    }
}

המשתמש יכול להפוך את הלחצן הזה שוב למיקוד באמצעות הגדרה של canFocus לערך true:

FancyButton(Modifier.focusProperties { canFocus = true })

כמו בכל Modifier, מילות המפתח שקשורות למיקוד מתנהגות באופן שונה בהתאם לסדר שאתם מצהירים עליהם. לדוגמה, קוד כמו הבא הופך את הפונקציה Box ניתן למיקוד, אבל ה-FocusRequester לא משויך לתוכן הזה כי הוא מוצהר אחרי שניתן למיקוד.

Box(
    Modifier
        .focusable()
        .focusRequester(Default)
        .onFocusChanged {}
)

חשוב לזכור ש-focusRequester משויך שניתן להתמקד בו מתחתיה בהיררכיה, כך שהfocusRequester הזה מפנה אל ילדים שאפשר להתמקד בהם. אם אף אחת מהאפשרויות לא זמינה, היא לא תצביע על שום דבר. עם זאת, מכיוון שניתן להתמקד בBox (הודות למקש הצירוף focusable()), תוכל לנווט אליו באמצעות ניווט דו-כיווני.

דוגמה נוספת: כל אחד מהאפשרויות הבאות יעבוד, כמו onFocusChanged() מתייחס לאלמנט הראשון שניתן למיקוד שמופיע אחרי focusable() או focusTarget() מוסיפים.

Box(
    Modifier
        .onFocusChanged {}
        .focusRequester(Default)
        .focusable()
)
Box(
    Modifier
        .focusRequester(Default)
        .onFocusChanged {}
        .focusable()
)

הפניה אוטומטית של המיקוד בכניסה או ביציאה

לפעמים תצטרכו לספק סוג ניווט ספציפי מאוד, כמו שמוצגת באנימציה הבאה:

אנימציה של מסך שבו מוצגות שתי עמודות של לחצנים שמוצבים זה לצד זה ואנימציה של המיקוד מעמודה אחת לשנייה.
איור 3. אנימציה של מסך שבו מוצגות שתי עמודות של לחצנים שמוצבים זה לצד זה ואנימציה של המיקוד מעמודה אחת לאחרת

לפני שנתעמק באופן יצירתי, חשוב להבין מהי ברירת המחדל התנהגות החיפוש של המיקוד. ללא שינויים, ברגע שהמיקוד הוא מגיע לפריט Clickable 3, לחיצה על DOWN בלחצני החיצים (או מקבילה) מקש חץ) יעביר את המוקד למה שמוצג מתחת ל-Column, לעזוב את הקבוצה ומתעלמים מהקבוצה שמשמאל. אם אין יש פריטים שניתן להתמקד בהם, המיקוד לא זז לשום מקום אלא נשאר Clickable 3

כדי לשנות את ההתנהגות הזו ולספק את הניווט המיועד, אפשר להשתמש מגביל focusProperties, שעוזר לנהל מה יקרה כשהמיקוד החיפוש נכנס אל Composable או יוצא ממנו:

val otherComposable = remember { FocusRequester() }

Modifier.focusProperties {
    exit = { focusDirection ->
        when (focusDirection) {
            Right -> Cancel
            Down -> otherComposable
            else -> Default
        }
    }
}

אפשר להפנות את המוקד ל-Composable ספציפי בכל פעם שהוא נכנס או יוצא מחלק מסוים של ההיררכיה — לדוגמה, כשבממשק המשתמש שלך יש ואתם רוצים לוודא שבכל פעם שהעיבוד של העמודה הראשונה יסתיים, המיקוד עובר לשנייה:

אנימציה של מסך שבו מוצגות שתי עמודות של לחצנים שמוצבים זה לצד זה ואנימציה של המיקוד מעמודה אחת לשנייה.
איור 4. אנימציה של מסך שבו מוצגות שתי עמודות של לחצנים שמוצבים זה לצד זה ואנימציה של המיקוד מעמודה אחת לאחרת

ב-GIF הזה, כשהמיקוד מגיע ל-Clickable 3 Composable ב-Column 1, הפריט הבא שעליו מתמקדים הוא Clickable 4 בColumn אחר. ההתנהגות הזו ניתן להשיג באמצעות שילוב של focusDirection עם enter ו-exit ערכים בתוך מקש הצירוף focusProperties. לשניהם צריך lambda בתור פרמטר, הכיוון שממנו מגיע המוקד ומחזיר FocusRequester ה-lambda הזה יכול להתנהג בשלוש דרכים שונות: FocusRequester.Cancel מפסיק את הפוקוס להמשיך, אבל FocusRequester.Default לא משנה את ההתנהגות שלו. במקום זאת, מספקת את FocusRequester מצורף אל Composable אחר, והמיקוד עובר אליו ספציפי Composable.

שינוי כיוון ההתקדמות של המיקוד

כדי להעביר את המיקוד לפריט הבא או לכיוון מדויק, להשתמש בתכונת השינוי onPreviewKey ולרמז על LocalFocusManager כדי מקדמים את המיקוד בעזרת ההתאמה moveFocus.

הדוגמה הבאה מציגה את התנהגות ברירת המחדל של מנגנון ההתמקדות: זוהתה הקשה אחת (tab), המיקוד יתקדם לרכיב הבא בפוקוס חדשה. אומנם לא צריך להגדיר את זה בדרך כלל, אבל לדעת את הפעולות הפנימיות של המערכת, ולשנות את ברירת המחדל או התנהגות המשתמשים.

val focusManager = LocalFocusManager.current
var text by remember { mutableStateOf("") }

TextField(
    value = text,
    onValueChange = { text = it },
    modifier = Modifier.onPreviewKeyEvent {
        when {
            KeyEventType.KeyUp == it.type && Key.Tab == it.key -> {
                focusManager.moveFocus(FocusDirection.Next)
                true
            }

            else -> false
        }
    }
)

בדוגמה הזו, הפונקציה focusManager.moveFocus() מקדמת את המיקוד של הפריט שצוין, או של הכיוון המשתמע בפרמטר של הפונקציה.