הקשה ולחיצה

להרבה רכיבים קומפוזביליים יש תמיכה מובנית בהקשות או בקליקים, והם כוללים פונקציית למבדה 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, שמציגים אפקט אדווה כברירת מחדל כשהמשתמש מקיש על הרכיב הקומפוזבילי. איך מתאימים אישית את האינטראקציות של המשתמשים
  • מאפשר לשירותי נגישות לקיים אינטראקציה עם הרכיב על ידי הגדרת המידע הסמנטי.
  • תמיכה באינטראקציה עם המקלדת או כפתורי החיצים (D-pad) באמצעות התמקדות ולחיצה על 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.

סגירת רכיב Composable בלחיצה על מסך מעומעם

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

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

במקרה כזה, לרקע לא צריכה להיות אינדיקציה חזותית לאינטראקציה, הוא לא צריך להגיב להעברת העכבר מעליו, לא צריך להיות אפשר להתמקד בו והתגובה שלו לאירועי מקלדת ונגישות שונה מזו של רכיב Composable רגיל. במקום לנסות להתאים את ההתנהגות של 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, מעבירים את פונקציית הלמבדה onClose. הפעולה הזו מפעילה מחדש באופן אוטומטי את פונקציית הלמבדה, ומוודאת שהקריאה החוזרת הנכונה מתבצעת כשהמשתמש מקיש על ה-scrim.

כדי לשנות את המרחק מהתצוגה, צריך ללחוץ לחיצה כפולה

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

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

כמו שאפשר לראות בסרטון, ההגדלה מתרחשת סביב המיקום של אירוע ההקשה. התוצאה שונה כשמגדילים את החלק השמאלי של התמונה לעומת החלק הימני. אנחנו יכולים להשתמש ב-modifier‏ 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
        }
)