Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- name: Check out code
uses: actions/checkout@v3.3.0
uses: actions/checkout@v3.5.2
with:
fetch-depth: '0'

Expand All @@ -33,4 +33,4 @@ jobs:
with:
name: Reports
path: '**/build/reports/*'
retention-days: 2
retention-days: 2
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
**Note:** *gitflow will be used for this project. Make sure your PRs are against the develop branch.*

# Compose LazyList/Grid reorder
[![Latest release](https://img.shields.io/github/v/release/aclassen/ComposeReorderable?color=brightgreen&label=latest%20release)](https://github.com/aclassen/ComposeReorderable/releases/latest)

Expand Down
25 changes: 14 additions & 11 deletions android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,30 @@ plugins {

dependencies {
implementation(project(":reorderable"))
implementation("androidx.compose.runtime:runtime:1.3.3")
implementation("androidx.compose.material:material:1.3.1")
implementation("androidx.activity:activity-compose:1.6.1")
implementation("com.google.android.material:material:1.8.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1")
implementation("androidx.navigation:navigation-compose:2.5.3")
implementation("io.coil-kt:coil-compose:2.2.2")
implementation("androidx.compose.runtime:runtime:1.6.0")
implementation("androidx.compose.material:material:1.6.0")
implementation("androidx.activity:activity-compose:1.8.2")
implementation("com.google.android.material:material:1.11.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
implementation("androidx.navigation:navigation-compose:2.7.6")
implementation("io.coil-kt:coil-compose:2.5.0")

}

android {

sourceSets {
map { it.java.srcDir("src/${it.name}/kotlin") }
}

val minSdkVersion: Int by rootProject.extra
val targetSdkVersion: Int by rootProject.extra
val compileSdkVersion: Int by rootProject.extra
compileSdk = compileSdkVersion
defaultConfig {
minSdk = minSdkVersion
targetSdk = targetSdkVersion
compileSdk = compileSdkVersion
versionCode = 1
versionName = "1.0"
}
Expand All @@ -38,7 +40,8 @@ android {
}

kotlinOptions {
jvmTarget = "1.8"
jvmTarget = "11"
}
namespace = "org.burnoutcrew.android"
}
buildToolsVersion = "34.0.0"
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,25 @@
package org.burnoutcrew.android.ui.reorderlist

import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
import androidx.compose.foundation.lazy.staggeredgrid.items
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
Expand All @@ -38,8 +44,10 @@ import androidx.compose.ui.draw.shadow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import org.burnoutcrew.reorderable.ReorderableItem
import org.burnoutcrew.reorderable.detectReorder
import org.burnoutcrew.reorderable.detectReorderAfterLongPress
import org.burnoutcrew.reorderable.rememberReorderableLazyGridState
import org.burnoutcrew.reorderable.rememberReorderableLazyVerticalStaggeredGridState
import org.burnoutcrew.reorderable.reorderable

@Composable
Expand All @@ -49,10 +57,60 @@ fun ReorderGrid(vm: ReorderListViewModel = viewModel()) {
vm = vm,
modifier = Modifier.padding(vertical = 16.dp)
)
VerticalGrid(vm = vm)
VerticalGrid(vm = vm, modifier = Modifier.padding(vertical = 16.dp))
VerticalStaggeredGrid(vm = vm, modifier = Modifier.padding(vertical = 16.dp))
}
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun VerticalStaggeredGrid(
vm: ReorderListViewModel,
modifier: Modifier = Modifier,
) {
val state = rememberReorderableLazyVerticalStaggeredGridState(
onMove = vm::moveDog,
canDragOver = vm::isDogDragEnabled
)
LazyVerticalStaggeredGrid(
columns = StaggeredGridCells.Fixed(4),
state = state.gridState,
contentPadding = PaddingValues(horizontal = 8.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalItemSpacing = 4.dp,
modifier = modifier.reorderable(state),
) {
items(items = vm.dogs, key = { it.key }) { item ->
if (item.isLocked) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(100.dp)
.background(MaterialTheme.colors.surface)
) {
Text(item.title)
}
} else {
ReorderableItem(state, item.key) { isDragging ->
val elevation = animateDpAsState(if (isDragging) 8.dp else 0.dp)
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxWidth()
.detectReorder(state)
.shadow(elevation.value)
.aspectRatio(1f)
.background(MaterialTheme.colors.primary)
) {
Text(item.title)
}
}
}
}
}
}


@Composable
private fun HorizontalGrid(
vm: ReorderListViewModel,
Expand Down Expand Up @@ -92,7 +150,8 @@ private fun VerticalGrid(
vm: ReorderListViewModel,
modifier: Modifier = Modifier,
) {
val state = rememberReorderableLazyGridState(onMove = vm::moveDog, canDragOver = vm::isDogDragEnabled)
val state =
rememberReorderableLazyGridState(onMove = vm::moveDog, canDragOver = vm::isDogDragEnabled)
LazyVerticalGrid(
columns = GridCells.Fixed(4),
state = state.gridState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ import androidx.lifecycle.ViewModel
import org.burnoutcrew.reorderable.ItemPosition

class ReorderListViewModel : ViewModel() {
var cats by mutableStateOf(List(500) { ItemData("Cat $it", "id$it") })
var dogs by mutableStateOf(List(500) {
var cats by mutableStateOf(List(6) { ItemData("Cat $it", "id$it") })
var dogs by mutableStateOf(List(6) {
if (it.mod(10) == 0) ItemData("Locked", "id$it", true) else ItemData("Dog $it", "id$it")
})

Expand All @@ -40,4 +40,4 @@ class ReorderListViewModel : ViewModel() {
}

fun isDogDragEnabled(draggedOver: ItemPosition, dragging: ItemPosition) = dogs.getOrNull(draggedOver.index)?.isLocked != true
}
}
14 changes: 7 additions & 7 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
plugins {
`maven-publish`
id("com.android.library") version "7.4.0" apply false
id("org.jetbrains.kotlin.multiplatform") version "1.8.0" apply false
id("org.jetbrains.kotlin.android") version "1.8.0" apply false
id("org.jetbrains.compose") version "1.3.0" apply false
id("com.android.library") version "8.2.0" apply false
id("org.jetbrains.kotlin.multiplatform") version "1.8.20" apply false
id("org.jetbrains.kotlin.android") version "1.8.20" apply false
id("org.jetbrains.compose") version "1.4.0" apply false
}

ext {
extra["compileSdkVersion"] = 33
extra["compileSdkVersion"] = 34
extra["minSdkVersion"] = 21
extra["targetSdkVersion"] = 33
extra["targetSdkVersion"] = 34
}

allprojects {
Expand All @@ -18,4 +18,4 @@ allprojects {
mavenCentral()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
}
}
14 changes: 13 additions & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,16 @@ kotlin.code.style=official
org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M"
android.useAndroidX=true

org.jetbrains.compose.experimental.jscanvas.enabled=true
org.jetbrains.compose.experimental.jscanvas.enabled=true

# Use of implicitly-created components in maven-publish.
# Starting with version 8.0, Android Gradle Plugin will no longer implicitly create
# components for the maven-publish plugin. You will have to adapt the publishing blocks
# to use the new API (and mark the project as migrated
# by adding android.disableAutomaticComponentCreation=true to the project's gradle.properties file).
# See: https://developer.android.com/build/publish-library
#android.disableAutomaticComponentCreation=true

android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Original file line number Diff line number Diff line change
Expand Up @@ -15,46 +15,44 @@
*/
package org.burnoutcrew.reorderable

import androidx.compose.foundation.gestures.awaitDragOrCancellation
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.forEachGesture
import androidx.compose.foundation.gestures.awaitLongPressOrCancellation
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.AwaitPointerEventScope
import androidx.compose.ui.input.pointer.PointerId
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInWindow

fun Modifier.detectReorder(state: ReorderableState<*>) =
this.then(
Modifier.pointerInput(Unit) {
forEachGesture {
awaitPointerEventScope {
val down = awaitFirstDown(requireUnconsumed = false)
var drag: PointerInputChange?
var overSlop = Offset.Zero
do {
drag = awaitPointerSlopOrCancellation(down.id, down.type) { change, over ->
change.consume()
overSlop = over
}
} while (drag != null && !drag.isConsumed)
if (drag != null) {
state.interactions.trySend(StartDrag(down.id, overSlop))
}
}
}
}
)


fun Modifier.detectReorderAfterLongPress(state: ReorderableState<*>) =
this.then(
Modifier.pointerInput(Unit) {
forEachGesture {
val down = awaitPointerEventScope {
awaitFirstDown(requireUnconsumed = false)
}
awaitLongPressOrCancellation(down)?.also {
state.interactions.trySend(StartDrag(down.id))
}
fun Modifier.detectReorder(state: ReorderableState<*>) = detect(state){
awaitDragOrCancellation(it)
}

fun Modifier.detectReorderAfterLongPress(state: ReorderableState<*>) = detect(state) {
awaitLongPressOrCancellation(it)
}


private fun Modifier.detect(state: ReorderableState<*>, detect: suspend AwaitPointerEventScope.(PointerId)->PointerInputChange?) = composed {

val itemPosition = remember { mutableStateOf(Offset.Zero) }

Modifier.onGloballyPositioned { itemPosition.value = it.positionInWindow() }.pointerInput(Unit) {
awaitEachGesture {
val down = awaitFirstDown(requireUnconsumed = false)
val start = detect(down.id)

if (start != null) {
val relativePosition = itemPosition.value - state.layoutWindowPosition.value + start.position
state.onDragStart(relativePosition.x.toInt(), relativePosition.y.toInt())
}
}
)
}
}
Loading