הקשה ולחיצה

תכנים קומפוזביליים רבים כוללים תמיכה מובנית בהקשה או בקליקים, והם כוללים onClick למבדה. לדוגמה, אפשר ליצור Surface קליקבילית כוללת את כל ההתנהגות של עיצוב Material Design המתאימה לאינטראקציה עם פלטפורמות:

Surface(onClick = { /* handle click */ }) {
    Text("Click me!", Modifier.padding(24.dp))
}

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

תנועה

תיאור

מקישים (או לוחצים)

המצביע יורד ואז עולה

לחיצה פעמיים

המצביע יורד, למעלה, למטה, למעלה

לחיצה ארוכה

המצביע יורד ונשמר למשך זמן ארוך יותר

עיתונות

המצביע מופנה כלפי מטה

להגיב להקשה או ללחיצה

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

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

כדי להטמיע את הפרמטר הזה, אפשר להוסיף את מגביל clickable לכל פריט ברשת התנהגות:

@Composable
private fun ImageGrid(photos: List<Photo>) {
    var activePhotoId by rememberSaveable { mutableStateOf<Int?>(null) }
    LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 128.dp)) {
        items(photos, { it.id }) { photo ->
            ImageItem(
                photo,
                Modifier.clickable { activePhotoId = photo.id }
            )
        }
    }
    if (activePhotoId != null) {
        FullScreenImage(
            photo = photos.first { it.id == activePhotoId },
            onDismiss = { activePhotoId = null }
        )
    }
}

הצירוף clickable גם מוסיף עוד התנהגות:

  • interactionSource ו-indication, שמושכים גל כברירת מחדל כאשר משתמש מקיש על התוכן הקומפוזבילי. מידע נוסף על התאמה אישית זמין בקטע טיפול במשתמשים אינטראקציות.
  • מאפשר לשירותי נגישות לקיים אינטראקציה עם האלמנט על ידי הגדרה של סמנטיקה.
  • תמיכה באינטראקציה באמצעות מקלדת או ג'ויסטיק על ידי הפעלת מיקוד ולחיצה Enter או במרכז של מקשי החיצים (D-pad) כדי לבצע אינטראקציה.
  • מגדירים שאפשר יהיה להעביר את הרכיב מעל פריטים, כך שהוא יגיב לעכבר או לסטיילוס שמרחפים מעליו מעליו.

אפשר ללחוץ לחיצה ארוכה כדי להציג תפריט הקשר לפי הקשר

combinedClickable מאפשרת להוסיף פעולות של הקשה כפולה או לחיצה ארוכה בנוסף להתנהגות קליקים רגילה. אפשר להשתמש ב-combinedClickable כדי להראות תפריט ההקשר כשמשתמש לוחץ לחיצה ארוכה על תמונת רשת:

var contextMenuPhotoId by rememberSaveable { mutableStateOf<Int?>(null) }
val haptics = LocalHapticFeedback.current
LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 128.dp)) {
    items(photos, { it.id }) { photo ->
        ImageItem(
            photo,
            Modifier
                .combinedClickable(
                    onClick = { activePhotoId = photo.id },
                    onLongClick = {
                        haptics.performHapticFeedback(HapticFeedbackType.LongPress)
                        contextMenuPhotoId = photo.id
                    },
                    onLongClickLabel = stringResource(R.string.open_context_menu)
                )
        )
    }
}
if (contextMenuPhotoId != null) {
    PhotoActionsSheet(
        photo = photos.first { it.id == contextMenuPhotoId },
        onDismissSheet = { contextMenuPhotoId = null }
    )
}

השיטה המומלצת היא לכלול משוב פיזי כשהמשתמש לוחץ לחיצה ארוכה על רכיבים, ולכן הקטע כולל הפעלה של performHapticFeedback.

כדי לסגור תוכן קומפוזבילי על ידי הקשה על מסמך קומפוזבילי

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

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

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

@Composable
private fun Scrim(onClose: () -> Unit, modifier: Modifier = Modifier) {
    val strClose = stringResource(R.string.close)
    Box(
        modifier
            // handle pointer input
            .pointerInput(onClose) { detectTapGestures { onClose() } }
            // handle accessibility services
            .semantics(mergeDescendants = true) {
                contentDescription = strClose
                onClick {
                    onClose()
                    true
                }
            }
            // handle physical keyboard input
            .onKeyEvent {
                if (it.key == Key.Escape) {
                    onClose()
                    true
                } else {
                    false
                }
            }
            // draw scrim
            .background(Color.DarkGray.copy(alpha = 0.75f))
    )
}

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

אפשר להקיש הקשה כפולה כדי להגדיל את הזום

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

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

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

var zoomed by remember { mutableStateOf(false) }
var zoomOffset by remember { mutableStateOf(Offset.Zero) }
Image(
    painter = rememberAsyncImagePainter(model = photo.highResUrl),
    contentDescription = null,
    modifier = modifier
        .pointerInput(Unit) {
            detectTapGestures(
                onDoubleTap = { tapOffset ->
                    zoomOffset = if (zoomed) Offset.Zero else
                        calculateOffset(tapOffset, size)
                    zoomed = !zoomed
                }
            )
        }
        .graphicsLayer {
            scaleX = if (zoomed) 2f else 1f
            scaleY = if (zoomed) 2f else 1f
            translationX = zoomOffset.x
            translationY = zoomOffset.y
        }
)