#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
- Вы повторяете этот расчет каждый раз, когда перекомпозируете, вам не следует этого делать. Вы можете использовать
LaunchedEffect
withsize.value.width
в качестве ключа, потомуwidth
что его нужно пересчитывать только при изменении этого значения. - Вы пытаетесь получить значение пикселя из 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. На случай, если вам не нужно ничего анимировать во время свайпа, я перенес эту логику в отдельный модификатор.