diff --git a/README.md b/README.md index 5cac84a..9bdb0b6 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ val state = rememberReorderState() LazyColumn( state = state.listState, - modifier = Modifier.reorderable(state, { from, to -> data.move(from, to) })) { + modifier = Modifier.reorderable(state, { from, to -> data.move(from.index, to.index) })) { ... } ``` @@ -69,7 +69,7 @@ fun ReorderableList(){ val data = List(100) { "item $it" }.toMutableStateList() LazyColumn( state = state.listState, - modifier = Modifier.reorderable(state, { a, b -> data.move(a, b) }) + modifier = Modifier.reorderable(state, { from, to -> data.move(from.index, to.index) }) ) { items(data, { it }) { item -> Box( diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 0634f96..01aa537 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -14,6 +14,8 @@ dependencies { implementation("com.google.android.material:material:1.4.0") implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1") implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.4.0-rc01") + implementation("androidx.navigation:navigation-compose:2.4.0-alpha10") + implementation("io.coil-kt:coil-compose:1.4.0") } android { diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index e390a61..ad68d3b 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ + , navController: NavController) { + BottomNavigation(contentColor = Color.White) { + val navBackStackEntry = navController.currentBackStackEntryAsState() + val currentRoute = navBackStackEntry.value?.destination?.route + items.forEach { item -> + BottomNavigationItem( + icon = { Icon(item.icon, item.title) }, + label = { Text(text = item.title) }, + selected = currentRoute == item.route, + onClick = { + navController.navigate(item.route) { + navController.graph.startDestinationRoute?.let { route -> + popUpTo(route) { + saveState = true + } + } + launchSingleTop = true + restoreState = true + } + } + ) + } + } +} + +private sealed class NavigationItem(var route: String, var icon: ImageVector, var title: String) { + object Lists : NavigationItem("lists", Icons.Default.List, "Lists") + object Music : NavigationItem("fixed", Icons.Default.Star, "Fixed") +} diff --git a/android/src/main/kotlin/org/burnoutcrew/android/ui/reorderlist/ImageListViewModel.kt b/android/src/main/kotlin/org/burnoutcrew/android/ui/reorderlist/ImageListViewModel.kt new file mode 100644 index 0000000..0d9faa5 --- /dev/null +++ b/android/src/main/kotlin/org/burnoutcrew/android/ui/reorderlist/ImageListViewModel.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2021 André Claßen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.burnoutcrew.android.ui.reorderlist + +import androidx.compose.runtime.toMutableStateList +import androidx.lifecycle.ViewModel +import org.burnoutcrew.reorderable.ItemPosition +import org.burnoutcrew.reorderable.move +import kotlin.random.Random + + +class ImageListViewModel : ViewModel() { + val images = List(20) { "https://picsum.photos/seed/compose$it/200/300" }.toMutableStateList() + val headerImage = "https://picsum.photos/seed/compose${Random.nextInt(Int.MAX_VALUE)}/400/200" + val footerImage = "https://picsum.photos/seed/compose${Random.nextInt(Int.MAX_VALUE)}/400/200" + + fun onMove(from: ItemPosition, to: ItemPosition) { + images.move(images.indexOfFirst { it == from.key }, images.indexOfFirst { it == to.key }) + } + + fun canDragOver(pos: ItemPosition) = images.any { it == pos.key } +} \ No newline at end of file diff --git a/android/src/main/kotlin/org/burnoutcrew/android/ui/reorderlist/ItemData.kt b/android/src/main/kotlin/org/burnoutcrew/android/ui/reorderlist/ItemData.kt index 8c755d7..5ea89e8 100644 --- a/android/src/main/kotlin/org/burnoutcrew/android/ui/reorderlist/ItemData.kt +++ b/android/src/main/kotlin/org/burnoutcrew/android/ui/reorderlist/ItemData.kt @@ -15,7 +15,4 @@ */ package org.burnoutcrew.android.ui.reorderlist -import androidx.compose.runtime.Stable - -@Stable data class ItemData(val title: String, val key: String, val isLocked: Boolean = false) \ No newline at end of file diff --git a/android/src/main/kotlin/org/burnoutcrew/android/ui/reorderlist/ReorderImageList.kt b/android/src/main/kotlin/org/burnoutcrew/android/ui/reorderlist/ReorderImageList.kt new file mode 100644 index 0000000..2dea8b6 --- /dev/null +++ b/android/src/main/kotlin/org/burnoutcrew/android/ui/reorderlist/ReorderImageList.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2021 André Claßen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.burnoutcrew.android.ui.reorderlist + + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.Divider +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import coil.compose.rememberImagePainter +import org.burnoutcrew.android.R +import org.burnoutcrew.reorderable.* + +@Composable +fun ReorderImageList( + vm: ImageListViewModel = viewModel(), + state: ReorderableState = rememberReorderState(), + modifier: Modifier = Modifier, +) { + LazyColumn( + state = state.listState, + modifier = modifier + .then( + Modifier.reorderable( + state, + onMove = { from, to -> vm.onMove(from, to) }, + canDragOver = { vm.canDragOver(it) }) + ) + ) { + item { + HeaderFooter(stringResource(R.string.header_title), vm.headerImage) + } + items(vm.images, { it }) { item -> + Column( + modifier = Modifier + .fillMaxWidth() + .draggedItem(state.offsetByKey(item)) + .background(MaterialTheme.colors.surface) + .detectReorderAfterLongPress(state) + ) { + Row { + Image( + painter = rememberImagePainter(item), + contentDescription = null, + modifier = Modifier.size(128.dp) + ) + Text( + text = item, + modifier = Modifier.padding(16.dp) + ) + } + Divider() + } + } + item { + HeaderFooter(stringResource(R.string.footer_title), vm.footerImage) + } + } +} + +@Composable +private fun HeaderFooter(title: String, url: String) { + Box(modifier = Modifier.height(128.dp).fillMaxWidth()) { + Image( + painter = rememberImagePainter(url), + contentDescription = null, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop + ) + Text( + title, + style = MaterialTheme.typography.h2, + color = Color.White, + modifier = Modifier.align(Alignment.Center) + ) + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/org/burnoutcrew/android/ui/reorderlist/ReorderList.kt b/android/src/main/kotlin/org/burnoutcrew/android/ui/reorderlist/ReorderList.kt index a201780..fa73186 100644 --- a/android/src/main/kotlin/org/burnoutcrew/android/ui/reorderlist/ReorderList.kt +++ b/android/src/main/kotlin/org/burnoutcrew/android/ui/reorderlist/ReorderList.kt @@ -16,7 +16,6 @@ package org.burnoutcrew.android.ui.reorderlist -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.layout.* @@ -28,8 +27,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Divider import androidx.compose.material.MaterialTheme import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Menu import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -58,11 +55,11 @@ fun ReorderList(vm: ReorderListViewModel = viewModel()) { } @Composable -fun HorizontalReorderList( +private fun HorizontalReorderList( modifier: Modifier = Modifier, items: List, state: ReorderableState = rememberReorderState(), - onMove: (fromPos: Int, toPos: Int) -> (Unit), + onMove: (fromPos: ItemPosition, toPos: ItemPosition) -> (Unit), ) { LazyRow( state = state.listState, @@ -91,12 +88,12 @@ fun HorizontalReorderList( } @Composable -fun VerticalReorderList( +private fun VerticalReorderList( modifier: Modifier = Modifier, items: List, state: ReorderableState = rememberReorderState(), - onMove: (fromPos: Int, toPos: Int) -> (Unit), - canDragOver: ((index: Int) -> Boolean), + onMove: (fromPos: ItemPosition, toPos: ItemPosition) -> (Unit), + canDragOver: ((pos: ItemPosition) -> Boolean), ) { LazyColumn( state = state.listState, diff --git a/android/src/main/kotlin/org/burnoutcrew/android/ui/reorderlist/ReorderListViewModel.kt b/android/src/main/kotlin/org/burnoutcrew/android/ui/reorderlist/ReorderListViewModel.kt index dcf3e82..75a5646 100644 --- a/android/src/main/kotlin/org/burnoutcrew/android/ui/reorderlist/ReorderListViewModel.kt +++ b/android/src/main/kotlin/org/burnoutcrew/android/ui/reorderlist/ReorderListViewModel.kt @@ -17,6 +17,7 @@ package org.burnoutcrew.android.ui.reorderlist import androidx.compose.runtime.toMutableStateList import androidx.lifecycle.ViewModel +import org.burnoutcrew.reorderable.ItemPosition import org.burnoutcrew.reorderable.move @@ -26,13 +27,13 @@ class ReorderListViewModel : ViewModel() { if (it.mod(10) == 0) ItemData("Locked", "id$it", true) else ItemData("Dog $it", "id$it") }.toMutableStateList() - fun moveCat(from: Int, to: Int) { - cats.move(from, to) + fun moveCat(from: ItemPosition, to: ItemPosition) { + cats.move(from.index, to.index) } - fun moveDog(from: Int, to: Int) { - dogs.move(from, to) + fun moveDog(from: ItemPosition, to: ItemPosition) { + dogs.move(from.index, to.index) } - fun isDogDragEnabled(idx: Int) = dogs.getOrNull(idx)?.isLocked != true + fun isDogDragEnabled(pos: ItemPosition) = dogs.getOrNull(pos.index)?.isLocked != true } \ No newline at end of file diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index accd4e6..d04bf85 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -1,3 +1,5 @@ Lazy reorder list + Header + Footer \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index e07f133..147dc2a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,7 +10,7 @@ buildscript { dependencies { classpath("org.jetbrains.compose:compose-gradle-plugin:1.0.0-alpha3") - classpath("com.android.tools.build:gradle:4.2.2") + classpath("com.android.tools.build:gradle:7.0.3") classpath(kotlin("gradle-plugin", version = "1.5.21")) } } diff --git a/desktop/src/jvmMain/kotlin/Main.kt b/desktop/src/jvmMain/kotlin/Main.kt index e499e5e..5079f02 100644 --- a/desktop/src/jvmMain/kotlin/Main.kt +++ b/desktop/src/jvmMain/kotlin/Main.kt @@ -41,7 +41,7 @@ fun main() = application { onCloseRequest = ::exitApplication, title = "Lazy reorder list" ) { - VerticalReorderList(items = data) { a, b -> data.move(a, b) } + VerticalReorderList(items = data) { a, b -> data.move(a.index, b.index) } } } @@ -49,7 +49,7 @@ fun main() = application { fun VerticalReorderList( items: List, state: ReorderableState = rememberReorderState(), - onMove: (fromPos: Int, toPos: Int) -> (Unit), + onMove: (fromPos: ItemPosition, toPos: ItemPosition) -> (Unit), ) { Box { LazyColumn( diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 549d844..0f80bbf 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/reorderable/build.gradle.kts b/reorderable/build.gradle.kts index d97b5bf..266752b 100644 --- a/reorderable/build.gradle.kts +++ b/reorderable/build.gradle.kts @@ -10,7 +10,7 @@ plugins { } group = "org.burnoutcrew.composereorderable" -version = "0.6.2" +version = "0.7.0" kotlin { android { @@ -41,7 +41,7 @@ android { } sourceSets { - val main by getting { + named("main") { manifest.srcFile("src/androidMain/AndroidManifest.xml") } } diff --git a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ItemPosition.kt b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ItemPosition.kt new file mode 100644 index 0000000..d243094 --- /dev/null +++ b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ItemPosition.kt @@ -0,0 +1,3 @@ +package org.burnoutcrew.reorderable + +data class ItemPosition(val index: Int, val key: Any) \ No newline at end of file diff --git a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderLogic.kt b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderLogic.kt index bd9eaa8..5708d5d 100644 --- a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderLogic.kt +++ b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderLogic.kt @@ -25,8 +25,8 @@ import kotlin.math.sign internal class ReorderLogic( private val state: ReorderableState, - private val onMove: (fromIndex: Int, toIndex: Int) -> (Unit), - private val canDragOver: ((index: Int) -> Boolean)? = null, + private val onMove: (fromIndex: ItemPosition, toIndex: ItemPosition) -> (Unit), + private val canDragOver: ((index: ItemPosition) -> Boolean)? = null, private val onDragEnd: ((startIndex: Int, endIndex: Int) -> (Unit))? = null, ) { fun startDrag(key: Any) = @@ -79,29 +79,28 @@ internal class ReorderLogic( val end = (start + selected.size) .coerceIn(viewportStartOffset, viewportEndOffset + selected.size) state.draggedIndex?.also { draggedItem -> - chooseDropIndex( + chooseDropItem( state.listState.layoutInfo.visibleItemsInfo .filterNot { it.offsetEnd() < start || it.offset > end || it.index == draggedItem } - .filter { canDragOver?.invoke(it.index) != false }, + .filter { canDragOver?.invoke(ItemPosition(it.index, it.key)) != false }, start, end )?.also { targetIdx -> - onMove(draggedItem, targetIdx) - state.draggedIndex = targetIdx + onMove(ItemPosition(draggedItem, selected.key), ItemPosition(targetIdx.index, targetIdx.key)) + state.draggedIndex = targetIdx.index state.listState.scrollToItem(state.listState.firstVisibleItemIndex, state.listState.firstVisibleItemScrollOffset) - } } } } - private fun chooseDropIndex( + private fun chooseDropItem( items: List, curStart: Float, curEnd: Float, - ): Int? = + ): LazyListItemInfo? = draggedItem.let { draggedItem -> - var targetIndex: Int? = null + var targetItem: LazyListItemInfo? = null if (draggedItem != null) { val distance = curStart - draggedItem.offset if (distance != 0f) { @@ -118,14 +117,14 @@ internal class ReorderLogic( ?.takeIf { it > targetDiff } ?.also { targetDiff = it - targetIndex = item.index + targetItem = item } } } } else if (state.draggedIndex != null) { - targetIndex = items.lastOrNull()?.index + targetItem = items.lastOrNull() } - targetIndex + targetItem } diff --git a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/Reorderable.kt b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/Reorderable.kt index 5b6318f..f8fd2ec 100644 --- a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/Reorderable.kt +++ b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/Reorderable.kt @@ -72,8 +72,8 @@ class ReorderableState(val listState: LazyListState) { @OptIn(ExperimentalCoroutinesApi::class) fun Modifier.reorderable( state: ReorderableState, - onMove: (fromPos: Int, toPos: Int) -> (Unit), - canDragOver: ((index: Int) -> Boolean)? = null, + onMove: (fromPos: ItemPosition, toPos: ItemPosition) -> (Unit), + canDragOver: ((index: ItemPosition) -> Boolean)? = null, onDragEnd: ((startIndex: Int, endIndex: Int) -> (Unit))? = null, orientation: Orientation = Orientation.Vertical, maxScrollPerFrame: Dp = 20.dp,