תנועות ביד אחת ב-Compose


ב-Wear OS 7 (רמת API‏ 37) ומעלה, מסגרת לתנועות ביד אחת, יחד עם API שהוא חלק מ-Compose ל-Wear OS, מאפשרת למשתמשים להשתמש באפליקציה בלי לגעת במסך.

המסגרת נתמכת קודם כל במכשירי Pixel Watch (מ-Pixel Watch 3 ואילך), אבל היא זמינה לכל יצרני הציוד המקורי (OEM). אם תטמיעו את ה-API הזה, התמיכה של האפליקציה בתנועות תתאים את עצמה באופן אוטומטי לכל המערכת ככל שהתמיכה בחומרה תתרחב.

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

תנועות ופעולות נתמכות

מסגרת התנועות של Wear OS תומכת בשני סוגי תנועות:

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

הגדרת תנועות ב-Compose

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

כמו בהתנהגויות הרגילות של Compose, אתם יכולים להפעיל תנועות ביד אחת ברכיבים של ממשק המשתמש באמצעות תכונות לשינוי. אתם מגדירים את התנועות באפליקציה בהתאם לפעולה שרוצים לבצע – פעולה ראשית או סגירה – ו-gestureId כדי לתאם עם העדפות המשתמש ברמת המערכת, כמו קצב הצגת הרמזים והשתקת התדירות. כדי להגדיר את זה, יוצרים אובייקט OneHandedGestureConfiguration. מומלץ להשתמש בפונקציה rememberOneHandedGestureConfiguration כדי ליצור אותו. ב-OneHandedGestureConfiguration אפשר גם לציין את העדיפות של התנועה.

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

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

רכיבים אינטראקטיביים

כדי להפעיל תנועות בפקד אינטראקטיבי כמו כפתור, יוצרים הגדרה שמציינת את OneHandedGestureAction.Primary ומחילים את המשתנה oneHandedGesture. מעבירים את אותו MutableInteractionSource גם לפקד וגם ל-modifier, כדי שאירועי תנועה יפיקו משוב חזותי על לחיצה בפקד.

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

var isPlaying by remember { mutableStateOf(false) }
val onClick = { isPlaying = !isPlaying }

val gestureConfig = rememberOneHandedGestureConfiguration(
    action = OneHandedGestureAction.Primary
)
val indicatorState = remember { OneHandedGestureClickIndicatorState() }
val coroutineScope = rememberCoroutineScope()
val interactionSource = remember { MutableInteractionSource() }

Button(
    onClick = onClick,
    interactionSource = interactionSource,
    modifier = Modifier
        .fillMaxWidth()
        .oneHandedGesture(
            gestureConfiguration = gestureConfig,
            interactionSource = interactionSource,
            onGestureLabel = if (isPlaying) "pause" else "play",
            onGestureAvailable = { coroutineScope.launch { indicatorState.showIndicator() } },
            onGesture = onClick
        )
) {
    OneHandedGestureClickIndicator(
        gestureConfiguration = gestureConfig,
        state = indicatorState
    ) {
        Text(if (isPlaying) "Pause" else "Play", modifier = Modifier.fillMaxWidth())
    }
}

קונטיינרים שניתן לגלול

במסכים או ברשימות שאפשר לגלול, יוצרים הגדרה שמציינת את OneHandedGestureAction.Primary ומחילים את ה-modifier‏ oneHandedGesture על הקונטיינר, וקוראים לפונקציית עזר לגלילה כמו scrollDown.

כדי לספק משוב חזותי לפעולות גלילה, אפשר להשתמש ב-OneHandedGestureScrollIndicator. הרכיב הזה פועל כאינדיקטור גלילה רגיל שמראה את מיקום הגלילה, אבל הוא יכול גם לציין שפעולת הגלילה זמינה למשתמש. האינדיקטור הזה מועבר בדרך כלל למשבצת scrollIndicator של ScreenScaffold, והוא משולב עם המצב של קונטיינר שניתן לגלילה, כמו TransformingLazyColumn. הוא גם משתמש ב-OneHandedGestureScrollIndicatorState כדי לנהל את המעברים החזותיים שלו.

כדי להפעיל את המשוב החזותי, צריך להפעיל את showIndicator במצב הזה – בדרך כלל בתוך הקריאה החוזרת (callback) של onGestureAvailable של ה-modifier‏ oneHandedGesture. כשהאינדיקטור מופעל, הוא מחליף באופן זמני את המצב החזותי הרגיל שלו ברצף אנימציה של תנועה כדי להתריע למשתמש.

val scrollState = rememberTransformingLazyColumnState()
val gestureConfig = rememberOneHandedGestureConfiguration(
    action = OneHandedGestureAction.Primary,
    priority = OneHandedGesturePriority.Scrollable
)
val indicatorState = remember(gestureConfig) { OneHandedGestureScrollIndicatorState() }
val coroutineScope = rememberCoroutineScope()

ScreenScaffold(
    scrollState = scrollState,
    scrollIndicator = {
        OneHandedGestureScrollIndicator(
            gestureConfiguration = gestureConfig,
            indicatorState = indicatorState,
            scrollState = scrollState,
            modifier = Modifier.align(Alignment.CenterEnd)
        )
    }
) { contentPadding ->
    TransformingLazyColumn(
        state = scrollState,
        contentPadding = contentPadding,
        modifier = Modifier
            .fillMaxSize()
            .oneHandedGesture(
                gestureConfiguration = gestureConfig,
                onGestureLabel = "scroll",
                onGestureAvailable = {
                    coroutineScope.launch { indicatorState.showIndicator() }
                },
                onGesture = { OneHandedGestureDefaults.scrollDown(scrollState) }
            )
    ) {
        items(10) { index ->
            Text("Item $index", modifier = Modifier.padding(8.dp))
        }
    }
}

שילוב של כמה תנועות

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

  • OneHandedGesturePriority.Clickable (הכי גבוה): מקצים לפקדים אינטראקטיביים, כמו אלה מסוג Button או Card, כדי שהם יתעדו תנועות כשהם מוצגים במסך.
  • OneHandedGesturePriority.Scrollable (בינוני): מקצים למאגרים שניתן לגלול או לדפדף בהם, כדי שהם יפנו את המקום לצאצאים שניתן ללחוץ עליהם, אבל ייגללו כשאין פקד גלוי שניתן ללחוץ עליו.
  • ‫OneHandedGesturePriority.Unspecified (הנמוכה ביותר): עדיפות שלא הוקצתה. זהו ערך ברירת המחדל לתנועה שלא מוגדר לה priority.

אם מגדירים באופן מפורש את priority = OneHandedGesturePriority.Clickable בכפתור פנימי ואת priority = OneHandedGesturePriority.Scrollable ברשימת ההורה שלו, המערכת יכולה להציג את ההתנהגות הזו של עדיפות התנועה. כשהמשתמש מפעיל את הפעולה הראשית באמצעות תנועה ביד אחת, הרשימה נגללת למטה עד שהכפתור מופיע, ואז מתבצעת פעולת הלחיצה על הכפתור.

בדיקה וניפוי באגים של תנועות באמצעות ADB

אתם יכולים לבדוק תנועות ביד אחת במכשיר פיזי או באמולטור בלי לבצע תנועות פיזיות של פרק כף היד, באמצעות Android Debug Bridge‏ (adb) ושירות המערכת IWearGestureService.

הפעלת סימולציה של תנועות

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

  1. מוודאים שבמכשיר Wear OS פועלת מערכת Wear OS 7 (רמת API ‏37) ומעלה:

    adb shell getprop ro.build.version.sdk
    
  2. אם אתם בודקים את האפליקציה במכשיר פיזי שלא נמצא על פרק כף היד או מונח על מטען, אתם צריכים לשנות את האילוץ "לא על הגוף" כדי שמסגרת התנועות תישאר פעילה:

    adb shell cmd IWearGestureService override-constraints offbody-state
    

הפעלת אירועי תנועה באמצעות ADB

כדי לדמות את התנועה הצמדת אצבעות כפולה (שהיא הפעולה Primary בשעוני Pixel Watch), מריצים את פקודת המעטפת הבאה של adb:

adb shell cmd IWearGestureService gesture DoublePinch

כדי לדמות את תנועת סיבוב פרק כף היד (שהיא הפעולה Dismiss בשעוני Pixel Watch), מריצים את פקודת המעטפת הבאה של adb:

adb shell cmd IWearGestureService gesture WristTurn

איפוס המעקב אחרי רמזים לתנועות

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

  • בגרסאות build או באמולטורים של userdebug:

    adb shell cmd IWearGestureService hint clear <your_package_name>
    
  • בגרסאות קמעונאיות (user):

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

    adb shell pm clear <your_package_name>
    

שחזור אילוצי ברירת המחדל

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

adb shell cmd IWearGestureService override-constraints reset

פתרון בעיות בהזרקת תנועות

אם האפליקציה לא מקבלת תנועות מדומות:

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

    adb shell input keyevent KEYCODE_WAKEUP
    
  2. בודקים אם האפליקציה רשומה כמנוי פעיל לתנועות, ואם החלון שלה נמצא כרגע במיקוד:

    adb shell cmd IWearGestureService get-active-gestures -readable
    

    כשהמסך של האפליקציה שבו מופיעה התנועה נמצא בחזית והמסך פעיל, הפקודה הזו מחזירה את הערך [DoublePinch] או [DoublePinch, WristTurn]. אם מוחזרת רשימה ריקה ([]), צריך לבדוק אם החלון נמצא במיקוד או אם אילוצים של מצב "לא על הגוף" חוסמים את ההפעלה.

  3. בודקים את המצב של שירות התנועות הפנימי ואת הטוקנים הפעילים של המנויים:

    adb shell dumpsys IWearGestureService
    

מקורות מידע נוספים

במאמר תנועות ביד אחת מוסבר מתי ואיפה כדאי להשתמש בתנועות ביד אחת.