From e653e7724934cf4990c95b02a43d1828da35b7ac Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Wed, 21 Jun 2023 17:40:06 -0700 Subject: [PATCH] Create draggable functions --- .../pitchedapps/frost/compose/Draggable.kt | 138 ++++++++++++++++++ .../com/pitchedapps/frost/ext/Modifier.kt | 22 +++ .../frost/main/MainScreenWebView.kt | 10 +- .../frost/tabselector/TabSelectorScreen.kt | 63 ++++++-- 4 files changed, 218 insertions(+), 15 deletions(-) create mode 100644 app-compose/src/main/kotlin/com/pitchedapps/frost/compose/Draggable.kt create mode 100644 app-compose/src/main/kotlin/com/pitchedapps/frost/ext/Modifier.kt diff --git a/app-compose/src/main/kotlin/com/pitchedapps/frost/compose/Draggable.kt b/app-compose/src/main/kotlin/com/pitchedapps/frost/compose/Draggable.kt new file mode 100644 index 000000000..9a74d2dda --- /dev/null +++ b/app-compose/src/main/kotlin/com/pitchedapps/frost/compose/Draggable.kt @@ -0,0 +1,138 @@ +/* + * Copyright 2023 Allan Wang + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.pitchedapps.frost.compose + +import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.LayoutScopeMarker +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInWindow +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import kotlin.math.roundToInt + +/* + * Resources: + * + * https://blog.canopas.com/android-drag-and-drop-ui-element-in-jetpack-compose-14922073b3f1 + */ + +@Composable +fun DragContainer(modifier: Modifier = Modifier, content: @Composable DragScope.() -> Unit) { + val draggable = remember { Draggable() } + val dragScope = remember(draggable) { DragScopeImpl(draggable) } + Box(modifier = modifier) { + dragScope.content() + + DraggingContent(draggable = draggable) + } +} + +private class DragScopeImpl(private val draggable: Draggable) : DragScope { + @Composable + override fun DragTarget( + key: String, + content: @Composable BoxScope.(isDragging: Boolean) -> Unit + ) { + + var isDragging by remember { mutableStateOf(false) } + + var positionInWindow by remember { mutableStateOf(Offset.Zero) } + + var size by remember { mutableStateOf(IntSize.Zero) } + + Box( + modifier = + Modifier.onGloballyPositioned { + positionInWindow = it.positionInWindow() + size = it.size + } + .pointerInput(Unit) { + detectDragGesturesAfterLongPress( + onDragStart = { + isDragging = true + draggable.composable = content + draggable.composableSize = size + draggable.dragPosition = positionInWindow + }, + onDrag = { _, offset -> draggable.dragPosition += offset }, + onDragEnd = { + isDragging = false + draggable.composable = null + }, + ) + }, + ) { + if (!isDragging) { + content(false) + } + } + } +} + +private fun IntSize.toDpSize(density: Density): DpSize { + return with(density) { DpSize(width.toDp(), height.toDp()) } +} + +@Composable +private fun DraggingContent(draggable: Draggable) { + val composable = draggable.composable ?: return + + val density = LocalDensity.current + val sizeDp = + remember { derivedStateOf { draggable.composableSize?.toDpSize(density) } }.value ?: return + + Box( + modifier = Modifier.size(sizeDp).offset { draggable.dragPosition.toIntOffset() }, + ) { + composable(true) + } +} + +private fun Offset.toIntOffset() = IntOffset(x.roundToInt(), y.roundToInt()) + +@LayoutScopeMarker +@Immutable +interface DragScope { + @Composable + fun DragTarget(key: String, content: @Composable BoxScope.(isDragging: Boolean) -> Unit) +} + +class Draggable { + var composable by mutableStateOf<(@Composable BoxScope.(isDragging: Boolean) -> Unit)?>(null) + var composableSize by mutableStateOf(null) + var dragPosition by mutableStateOf(Offset.Zero) +} + +internal val LocalDraggable = compositionLocalOf {} diff --git a/app-compose/src/main/kotlin/com/pitchedapps/frost/ext/Modifier.kt b/app-compose/src/main/kotlin/com/pitchedapps/frost/ext/Modifier.kt new file mode 100644 index 000000000..1a163ce70 --- /dev/null +++ b/app-compose/src/main/kotlin/com/pitchedapps/frost/ext/Modifier.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Allan Wang + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.pitchedapps.frost.ext + +import androidx.compose.ui.Modifier + +fun Modifier.thenIf(condition: Boolean, action: () -> Modifier): Modifier = + if (condition) then(action()) else this diff --git a/app-compose/src/main/kotlin/com/pitchedapps/frost/main/MainScreenWebView.kt b/app-compose/src/main/kotlin/com/pitchedapps/frost/main/MainScreenWebView.kt index ea891fdfe..22b4cc131 100644 --- a/app-compose/src/main/kotlin/com/pitchedapps/frost/main/MainScreenWebView.kt +++ b/app-compose/src/main/kotlin/com/pitchedapps/frost/main/MainScreenWebView.kt @@ -19,6 +19,7 @@ package com.pitchedapps.frost.main import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.ExperimentalMaterialApi @@ -42,6 +43,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.pitchedapps.frost.compose.webview.FrostWebCompose import com.pitchedapps.frost.ext.WebTargetId @@ -98,7 +100,13 @@ fun MainBottomBar( NavigationBar(modifier = modifier) { items.forEach { item -> NavigationBarItem( - icon = { Icon(item.icon, contentDescription = item.title) }, + icon = { + Icon( + modifier = Modifier.size(24.dp), + imageVector = item.icon, + contentDescription = item.title, + ) + }, selected = selectedTab == item.id, onClick = { onSelect(item.id) }, ) diff --git a/app-compose/src/main/kotlin/com/pitchedapps/frost/tabselector/TabSelectorScreen.kt b/app-compose/src/main/kotlin/com/pitchedapps/frost/tabselector/TabSelectorScreen.kt index 8a10d6394..867f9151d 100644 --- a/app-compose/src/main/kotlin/com/pitchedapps/frost/tabselector/TabSelectorScreen.kt +++ b/app-compose/src/main/kotlin/com/pitchedapps/frost/tabselector/TabSelectorScreen.kt @@ -20,6 +20,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding @@ -28,6 +29,8 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -40,8 +43,10 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import com.pitchedapps.frost.compose.DragContainer import com.pitchedapps.frost.compose.effects.rememberShakeState import com.pitchedapps.frost.compose.effects.shake +import com.pitchedapps.frost.ext.thenIf import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.facebook.tab @@ -61,7 +66,7 @@ fun TabSelectorScreen(modifier: Modifier = Modifier) { val unselected = remember(selected) { options.values - selected.toSet() } TabSelector( - modifier = modifier.statusBarsPadding(), + modifier = modifier, selected = selected, unselected = unselected, onSelect = { selected = it }, @@ -76,19 +81,49 @@ fun TabSelector( unselected: List, onSelect: (List) -> Unit ) { - LazyVerticalGrid( - modifier = modifier, - columns = GridCells.Fixed(4), - ) { - items(unselected, key = { it.key }) { - val shakeState = rememberShakeState() - TabItem( - modifier = - Modifier.animateItemPlacement().shake(shakeState).clickable { - shakeState.shake() - // onSelect(listOf(it)) - }, - data = it, + DragContainer(modifier = modifier) { + Column(modifier = Modifier.statusBarsPadding()) { + LazyVerticalGrid( + modifier = Modifier.weight(1f), + columns = GridCells.Fixed(4), + ) { + items(unselected, key = { it.key }) { + this@DragContainer.DragTarget(key = it.key) { isDragging -> + val shakeState = rememberShakeState() + + TabItem( + modifier = + Modifier.thenIf(!isDragging) { Modifier.animateItemPlacement() } + .shake(shakeState) + .clickable { + shakeState.shake() + // onSelect(listOf(it)) + }, + data = it, + ) + } + } + } + + TabBottomBar(modifier = Modifier.navigationBarsPadding(), items = selected) + } + } +} + +@Composable +fun TabBottomBar(modifier: Modifier = Modifier, items: List) { + NavigationBar(modifier = modifier) { + items.forEach { item -> + NavigationBarItem( + icon = { + Icon( + modifier = Modifier.size(24.dp), + imageVector = item.icon, + contentDescription = item.title, + ) + }, + selected = false, + onClick = {}, ) } }