Как в Compose реализовать свайп, если я хочу вызвать действие после свайпа

#android #kotlin #android-jetpack-compose

Вопрос:

У меня проблема с сочинением. Я пытаюсь реализовать простой календарь. Я уже отобразил месяц и включил изменение месяцев стрелками.

введите описание изображения здесь

Теперь я хочу добавить месяц изменений с помощью салфетки. Я пытался с .swipeable

Это образец:

 val size = remember { mutableStateOf(Size.Zero) }
val swipeableState = rememberSwipeableState(0)
val width = if (size.value.width == 0f) 1f else size.value.width - 60.dp.value * 2
Box(
    modifier = Modifier
        .fillMaxWidth()
        .onSizeChanged { size.value = Size(it.width.toFloat(), it.height.toFloat()) }
        .swipeable(
            state = swipeableState,
            anchors = mapOf(0f to 0, width to 1),
            thresholds = { _, _ -> FractionalThreshold(0.3f) },
            orientation = Orientation.Horizontal
        )
        .background(Color.LightGray)
) {
    Box(
        Modifier
            .offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) }
            .size(60.dp)
            .background(Color.DarkGray)
    )
}
 

Но это не работает идеально. Мне нужно держаться за пределами области календаря, и я понятия не имею, как изменить состояние месяца (которое отвечает за отображение календаря).

Мой вопрос: «как реализовать свайп, если я хочу вызвать действие после свайпа».

Ответ №1:

Вы можете наблюдать любое состояние с побочными эффектами. В случае, если вам нужно дождаться завершения действия, вы можете использовать if DisposableEffect :

 if (swipeableState.isAnimationRunning) {
    DisposableEffect(Unit) {
        onDispose {
            println("animatin finished")
        }
    }
}
 

Когда анимация начинается, я создаю DisposableEffect , а когда она заканчивается, onDispose вызывается, что означает, что анимация завершена.

В случае вашего кода это также будет срабатывать при запуске, потому что, когда вы меняете якоря после onSizeChanged , это также запускает анимацию. Но вы можете проверить, было ли изменено значение, так что это не большая проблема.

Чтобы решить вашу основную проблему, вам также необходимо иметь три состояния: левое, среднее и правое.


Немного не по теме, насчет этой строчки:

 val width = if (size.value.width == 0f) 1f else size.value.width - 60.dp.value * 2
 
  1. Вы повторяете этот расчет каждый раз, когда перекомпозируете, вам не следует этого делать. Вы можете использовать LaunchedEffect with size.value.width в качестве ключа, потому width что его нужно пересчитывать только при изменении этого значения.
  2. Вы пытаетесь получить значение пикселя из DP, умножив его на 2. Это неправильно, вам нужно использовать Dencity.

Таким образом, окончательный код может выглядеть следующим образом:

 enum class SwipeDirection(val raw: Int) {
    Left(0),
    Initial(1),
    Right(2),
}

@Composable
fun TestScreen() {
    var size by remember { mutableStateOf(Size.Zero) }
    val swipeableState = rememberSwipeableState(SwipeDirection.Initial)
    val density = LocalDensity.current
    val boxSize = 60.dp
    val width = remember(size) {
        if (size.width == 0f) {
            1f
        } else {
            size.width - with(density) { boxSize.toPx() }
        }
    }
    val scope = rememberCoroutineScope()
    if (swipeableState.isAnimationRunning) {
        DisposableEffect(Unit) {
            onDispose {
                when (swipeableState.currentValue) {
                    SwipeDirection.Right -> {
                        println("swipe right")
                    }
                    SwipeDirection.Left -> {
                        println("swipe left")
                    }
                    else -> {
                        return@onDispose
                    }
                }
                scope.launch {
                    // in your real app if you don't have to display offset,
                    // snap without animation
                    // swipeableState.snapTo(SwipeDirection.Initial)
                    swipeableState.animateTo(SwipeDirection.Initial)
                }
            }
        }
    }
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .onSizeChanged { size = Size(it.width.toFloat(), it.height.toFloat()) }
            .clickable { }
            .swipeable(
                state = swipeableState,
                anchors = mapOf(
                    0f to SwipeDirection.Left,
                    width / 2 to SwipeDirection.Initial,
                    width to SwipeDirection.Right,
                ),
                thresholds = { _, _ -> FractionalThreshold(0.3f) },
                orientation = Orientation.Horizontal
            )
            .background(Color.LightGray)
    ) {
        Box(
            Modifier
                .offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) }
                .size(boxSize)
                .background(Color.DarkGray)
        )
    }
}
 

Результат:

p.s. На случай, если вам не нужно ничего анимировать во время свайпа, я перенес эту логику в отдельный модификатор.