הקשה ולחיצה

לרכיבים רבים של Compose יש תמיכה מובנית בהקשות או בלחיצות, והם כוללים פונקציית lambda מסוג onClick. לדוגמה, אפשר ליצור Surface שניתן ללחוץ עליו, שכולל את כל ההתנהגויות של Material Design שמתאימות לאינטראקציה עם משטחים:

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

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

תנועה

תיאור

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

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

לחיצה פעמיים

הסמן עובר למטה, למעלה, למטה, למעלה

לחיצה ארוכה

הסמן יורד ונשאר במקום למשך זמן ארוך יותר

עיתונות

הסמן יורד

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

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

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

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

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

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

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

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