Нажатие

Многие composable-функции имеют встроенную поддержку нажатий или кликов и включают лямбда-выражение onClick. Например, вы можете создать кликабельный Surface, который включает в себя все поведение Material Design, подходящее для взаимодействия с поверхностями:

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

Но клики – не единственный способ взаимодействия с composable-функциями. На этой странице рассматриваются жесты, в которых задействован один указатель, положение которого не имеет значения для обработки события. В таблице ниже перечислены типы жестов.

Жест

Описание

Нажмите

Указатель перемещается вниз, а затем вверх.

Двойное касание

Указатель перемещается вниз, вверх, вниз, вверх.

Нажать и удерживать

Указатель опускается и удерживается дольше.

Пресса

Указатель перемещается вниз

Реакция на нажатие или клик

clickable – часто используемый модификатор, который заставляет composable-функцию реагировать на нажатия или клики. Этот модификатор также добавляет дополнительные функции, такие как поддержка фокуса, наведение мыши и стилуса, а также настраиваемую визуальную индикацию при нажатии. Модификатор реагирует на клики в самом широком смысле этого слова – не только мышью или пальцем, но и с помощью клавиатуры или специальных возможностей.

Представьте себе сетку изображений, каждое из которых открывается в полноэкранном режиме при нажатии.

Чтобы реализовать такое поведение, вы можете добавить модификатор 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.

Закрытие composable-функции нажатием на полупрозрачный фон

В примерах выше clickable и combinedClickable добавляют полезные функции в composable-функции. Они визуально реагируют на взаимодействие, например на наведение указателя, и поддерживают фокус, клавиатуру и специальные возможности. Но такое поведение не всегда желательно.

Давайте посмотрим на экран сведений об изображении. Фон должен быть полупрозрачным, и пользователь должен иметь возможность коснуться этого фона, чтобы закрыть экран сведений:

В этом случае фон не должен визуально реагировать на взаимодействие, наведение указателя, не должен быть фокусируемым, а его реакция на события клавиатуры и специальных возможностей должна отличаться от реакции типичной 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 не содержат достаточно информации, чтобы правильно ответить на запрос. Например, composable-функциям может потребоваться доступ к позиции в пределах границ composable-функции, где произошло взаимодействие.

Давайте снова посмотрим на экран сведений об изображении. Рекомендуется сделать так, чтобы изображение можно было увеличить двойным нажатием.

Как видно на видео, увеличение происходит вокруг позиции нажатия. Результат будет разным, если мы увеличим левую часть изображения по сравнению с правой. Мы можем использовать модификатор 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
        }
)