mirror of
https://github.com/f-droid/fdroidclient.git
synced 2026-02-15 09:33:37 -05:00
Initial implementation of repo list re-ordering
This commit is contained in:
@@ -47,6 +47,9 @@ import org.fdroid.ui.lists.AppListType
|
||||
import org.fdroid.ui.lists.AppListViewModel
|
||||
import org.fdroid.ui.repositories.Repositories
|
||||
import org.fdroid.ui.repositories.RepositoriesViewModel
|
||||
import org.fdroid.ui.repositories.RepositoryInfo
|
||||
import org.fdroid.ui.repositories.RepositoryItem
|
||||
import org.fdroid.ui.repositories.RepositoryModel
|
||||
import org.fdroid.ui.settings.Settings
|
||||
import org.fdroid.ui.settings.SettingsViewModel
|
||||
|
||||
@@ -190,18 +193,29 @@ fun Main(onListeningForIntent: () -> Unit = {}) {
|
||||
},
|
||||
) {
|
||||
val viewModel = hiltViewModel<RepositoriesViewModel>()
|
||||
val repos = viewModel.repos.collectAsStateWithLifecycle(null).value
|
||||
Repositories(
|
||||
repositories = repos,
|
||||
currentRepositoryId = if (isBigScreen) {
|
||||
val info = object : RepositoryInfo {
|
||||
override val model: RepositoryModel =
|
||||
viewModel.model.collectAsStateWithLifecycle().value
|
||||
|
||||
override val currentRepositoryId: Long? = if (isBigScreen) {
|
||||
(backStack.last() as? NavigationKey.RepoDetails)?.repoId
|
||||
} else null,
|
||||
onRepositorySelected = {
|
||||
viewModel.setVisibleRepository(it)
|
||||
backStack.add(NavigationKey.RepoDetails(it.repoId))
|
||||
},
|
||||
onAddRepo = viewModel::addRepo,
|
||||
) {
|
||||
} else null
|
||||
|
||||
override fun onRepositorySelected(repositoryItem: RepositoryItem) {
|
||||
backStack.add(NavigationKey.RepoDetails(repositoryItem.repoId))
|
||||
}
|
||||
|
||||
override fun onAddRepo() = viewModel.addRepo()
|
||||
|
||||
override fun onRepositoryMoved(fromIndex: Int, toIndex: Int) =
|
||||
viewModel.onRepositoriesMoved(fromIndex, toIndex)
|
||||
|
||||
override fun onRepositoriesFinishedMoving(
|
||||
fromRepoId: Long,
|
||||
toRepoId: Long,
|
||||
) = viewModel.onRepositoriesFinishedMoving(fromRepoId, toRepoId)
|
||||
}
|
||||
Repositories(info) {
|
||||
backStack.removeLastOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,14 +7,12 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -23,17 +21,12 @@ import androidx.compose.ui.unit.dp
|
||||
import org.fdroid.fdroid.ui.theme.FDroidContent
|
||||
import org.fdroid.next.R
|
||||
import org.fdroid.ui.utils.BigLoadingIndicator
|
||||
import org.fdroid.ui.utils.getRepositoriesInfo
|
||||
|
||||
@Composable
|
||||
@OptIn(
|
||||
ExperimentalMaterial3Api::class, ExperimentalMaterial3AdaptiveApi::class,
|
||||
ExperimentalMaterial3ExpressiveApi::class
|
||||
)
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
fun Repositories(
|
||||
repositories: List<RepositoryItem>?,
|
||||
currentRepositoryId: Long?,
|
||||
onRepositorySelected: (RepositoryItem) -> Unit,
|
||||
onAddRepo: () -> Unit,
|
||||
info: RepositoryInfo,
|
||||
onBackClicked: () -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
@@ -54,7 +47,7 @@ fun Repositories(
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = onAddRepo,
|
||||
onClick = info::onAddRepo,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
Icon(
|
||||
@@ -64,13 +57,9 @@ fun Repositories(
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
if (repositories == null) BigLoadingIndicator()
|
||||
if (info.model.repositories == null) BigLoadingIndicator()
|
||||
else RepositoriesList(
|
||||
repositories = repositories,
|
||||
currentRepositoryId = currentRepositoryId,
|
||||
onRepositorySelected = {
|
||||
onRepositorySelected(it)
|
||||
},
|
||||
info = info,
|
||||
modifier = Modifier
|
||||
.padding(paddingValues),
|
||||
)
|
||||
@@ -84,7 +73,9 @@ fun Repositories(
|
||||
@Composable
|
||||
fun RepositoriesScaffoldLoadingPreview() {
|
||||
FDroidContent {
|
||||
Repositories(null, null, {}, {}) { }
|
||||
val model = RepositoryModel(null)
|
||||
val info = getRepositoriesInfo(model)
|
||||
Repositories(info) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,10 +102,12 @@ fun RepositoriesScaffoldPreview() {
|
||||
timestamp = System.currentTimeMillis(),
|
||||
lastUpdated = null,
|
||||
weight = 2,
|
||||
enabled = true,
|
||||
enabled = false,
|
||||
name = "My second repository",
|
||||
),
|
||||
)
|
||||
Repositories(repos, repos[0].repoId, {}, {}) { }
|
||||
val model = RepositoryModel(repos)
|
||||
val info = getRepositoriesInfo(model, repos[0].repoId)
|
||||
Repositories(info) { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +1,92 @@
|
||||
package org.fdroid.ui.repositories
|
||||
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement.spacedBy
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.selection.selectableGroup
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.dropShadow
|
||||
import androidx.compose.ui.graphics.shadow.Shadow
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.fdroid.utils.DraggableItem
|
||||
import org.fdroid.utils.dragContainer
|
||||
import org.fdroid.utils.rememberDragDropState
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3AdaptiveApi::class)
|
||||
fun RepositoriesList(
|
||||
repositories: List<RepositoryItem>,
|
||||
currentRepositoryId: Long?,
|
||||
onRepositorySelected: (RepositoryItem) -> Unit,
|
||||
info: RepositoryInfo,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val repositories = info.model.repositories ?: return
|
||||
val currentRepositoryId = info.currentRepositoryId
|
||||
val listState = rememberLazyListState()
|
||||
val dragDropState = rememberDragDropState(
|
||||
lazyListState = listState,
|
||||
onMove = info::onRepositoryMoved,
|
||||
onEnd = { from, to ->
|
||||
from as? Long ?: error("from $from was not a repoId")
|
||||
to as? Long ?: error("to $to was not a repoId")
|
||||
info.onRepositoriesFinishedMoving(from, to)
|
||||
},
|
||||
)
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
contentPadding = PaddingValues(vertical = 8.dp),
|
||||
verticalArrangement = spacedBy(8.dp),
|
||||
modifier = modifier.then(
|
||||
if (currentRepositoryId == null) Modifier
|
||||
else Modifier.selectableGroup()
|
||||
),
|
||||
modifier = modifier
|
||||
.then(
|
||||
if (repositories.size > 1) Modifier.dragContainer(dragDropState)
|
||||
else Modifier
|
||||
)
|
||||
.then(
|
||||
if (currentRepositoryId == null) Modifier
|
||||
else Modifier.selectableGroup()
|
||||
),
|
||||
) {
|
||||
items(repositories) { repoItem ->
|
||||
itemsIndexed(
|
||||
items = repositories,
|
||||
key = { _, item -> item.repoId },
|
||||
) { index, repoItem ->
|
||||
val isSelected = currentRepositoryId == repoItem.repoId
|
||||
val interactionModifier = if (currentRepositoryId == null) {
|
||||
Modifier.clickable(
|
||||
onClick = { onRepositorySelected(repoItem) }
|
||||
onClick = { info.onRepositorySelected(repoItem) }
|
||||
)
|
||||
} else {
|
||||
Modifier.selectable(
|
||||
selected = isSelected,
|
||||
onClick = { onRepositorySelected(repoItem) }
|
||||
onClick = { info.onRepositorySelected(repoItem) }
|
||||
)
|
||||
}
|
||||
DraggableItem(dragDropState, index) { isDragging ->
|
||||
val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp)
|
||||
RepositoryRow(
|
||||
repoItem = repoItem,
|
||||
isSelected = isSelected,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(interactionModifier)
|
||||
.let {
|
||||
if (isDragging) it.dropShadow(
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
shadow = Shadow(
|
||||
radius = 8.dp,
|
||||
offset = DpOffset(x = elevation, elevation)
|
||||
)
|
||||
) else it
|
||||
}
|
||||
)
|
||||
}
|
||||
RepositoryRow(
|
||||
repoItem = repoItem,
|
||||
isSelected = isSelected,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(interactionModifier),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.fdroid.ui.repositories
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
@Composable
|
||||
fun RepositoriesPresenter(
|
||||
repositoriesFlow: Flow<List<RepositoryItem>>,
|
||||
repoSortingMapFlow: StateFlow<Map<Long, Int>>,
|
||||
): RepositoryModel {
|
||||
val repositories = repositoriesFlow.collectAsState(null).value
|
||||
val repoSortingMap = repoSortingMapFlow.collectAsState().value
|
||||
return RepositoryModel(
|
||||
repositories = repositories?.sortedBy { repo ->
|
||||
// newly added repos will not be in repoSortingMap, so they need fallback
|
||||
repoSortingMap[repo.repoId] ?: repositories.find { it.repoId == repo.repoId }?.weight
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -3,30 +3,74 @@ package org.fdroid.ui.repositories
|
||||
import android.app.Application
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.cash.molecule.AndroidUiDispatcher
|
||||
import app.cash.molecule.RecompositionMode.ContextClock
|
||||
import app.cash.molecule.launchMolecule
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import mu.KotlinLogging
|
||||
import org.fdroid.index.RepoManager
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class RepositoriesViewModel @Inject constructor(
|
||||
app: Application,
|
||||
repoManager: RepoManager,
|
||||
private val repoManager: RepoManager,
|
||||
) : AndroidViewModel(app) {
|
||||
|
||||
private val log = KotlinLogging.logger { }
|
||||
private val localeList = LocaleListCompat.getDefault()
|
||||
val repos: Flow<List<RepositoryItem>> = repoManager.repositoriesState.map { repos ->
|
||||
repos.map { RepositoryItem(it, localeList) }
|
||||
private val moleculeScope =
|
||||
CoroutineScope(viewModelScope.coroutineContext + AndroidUiDispatcher.Main)
|
||||
private val repos: Flow<List<RepositoryItem>> = repoManager.repositoriesState.map { repos ->
|
||||
repos.mapNotNull {
|
||||
if (it.isArchiveRepo) null
|
||||
else RepositoryItem(it, localeList)
|
||||
}
|
||||
}
|
||||
private val repoSortingMap: MutableStateFlow<Map<Long, Int>>
|
||||
|
||||
init {
|
||||
// just add repos to sortingMap, because they are already pre-sorted by weight
|
||||
val sortingMap = mutableMapOf<Long, Int>()
|
||||
repoManager.getRepositories().forEachIndexed { index, repository ->
|
||||
sortingMap[repository.repoId] = index
|
||||
}
|
||||
repoSortingMap = MutableStateFlow(sortingMap)
|
||||
}
|
||||
|
||||
private val _visibleRepositoryItem = MutableStateFlow<RepositoryItem?>(null)
|
||||
val visibleRepositoryItem = _visibleRepositoryItem.asStateFlow()
|
||||
// define below init, because this only defines repoSortingMap
|
||||
val model: StateFlow<RepositoryModel> = moleculeScope.launchMolecule(mode = ContextClock) {
|
||||
RepositoriesPresenter(
|
||||
repositoriesFlow = repos,
|
||||
repoSortingMapFlow = repoSortingMap,
|
||||
)
|
||||
}
|
||||
|
||||
fun setVisibleRepository(repositoryItem: RepositoryItem?) {
|
||||
_visibleRepositoryItem.value = repositoryItem
|
||||
fun onRepositoriesMoved(fromIndex: Int, toIndex: Int) {
|
||||
log.info { "onRepositoriesMoved($fromIndex, $toIndex)" }
|
||||
val repoItems = model.value.repositories ?: error("Model had null repositories")
|
||||
val fromItem = repoItems[fromIndex]
|
||||
val toItem = repoItems[toIndex]
|
||||
repoSortingMap.value = repoSortingMap.value.toMutableMap().apply {
|
||||
replace(fromItem.repoId, toIndex)
|
||||
replace(toItem.repoId, fromIndex)
|
||||
}
|
||||
}
|
||||
|
||||
fun onRepositoriesFinishedMoving(fromRepoId: Long, toRepoId: Long) {
|
||||
log.info { "onRepositoriesFinishedMoving($fromRepoId, $toRepoId)" }
|
||||
val fromRepo = repoManager.getRepository(fromRepoId)
|
||||
?: error("No repo for repoId $fromRepoId")
|
||||
val toRepo = repoManager.getRepository(toRepoId)
|
||||
?: error("No repo for repoId $toRepoId")
|
||||
log.info { " ${fromRepo.address} => ${toRepo.address}" }
|
||||
repoManager.reorderRepositories(fromRepo, toRepo)
|
||||
}
|
||||
|
||||
fun addRepo() {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.fdroid.ui.repositories
|
||||
|
||||
interface RepositoryInfo {
|
||||
val model: RepositoryModel
|
||||
val currentRepositoryId: Long?
|
||||
fun onRepositorySelected(repositoryItem: RepositoryItem)
|
||||
fun onAddRepo()
|
||||
fun onRepositoryMoved(fromIndex: Int, toIndex: Int)
|
||||
fun onRepositoriesFinishedMoving(fromRepoId: Long, toRepoId: Long)
|
||||
}
|
||||
|
||||
data class RepositoryModel(
|
||||
val repositories: List<RepositoryItem>?,
|
||||
)
|
||||
@@ -8,7 +8,6 @@ import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.fdroid.ui.utils.AsyncShimmerImage
|
||||
|
||||
@@ -39,7 +38,7 @@ fun RepositoryRow(
|
||||
containerColor = if (isSelected) {
|
||||
MaterialTheme.colorScheme.surfaceVariant
|
||||
} else {
|
||||
Color.Transparent
|
||||
MaterialTheme.colorScheme.background
|
||||
}
|
||||
),
|
||||
modifier = modifier,
|
||||
|
||||
@@ -19,6 +19,9 @@ import org.fdroid.ui.lists.AppListActions
|
||||
import org.fdroid.ui.lists.AppListInfo
|
||||
import org.fdroid.ui.lists.AppListModel
|
||||
import org.fdroid.ui.lists.AppListType
|
||||
import org.fdroid.ui.repositories.RepositoryInfo
|
||||
import org.fdroid.ui.repositories.RepositoryItem
|
||||
import org.fdroid.ui.repositories.RepositoryModel
|
||||
import java.util.concurrent.TimeUnit.DAYS
|
||||
|
||||
object Names {
|
||||
@@ -219,3 +222,15 @@ fun getMyAppsInfo(model: MyAppsModel): MyAppsInfo = object : MyAppsInfo {
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
fun getRepositoriesInfo(
|
||||
model: RepositoryModel,
|
||||
currentRepositoryId: Long? = null,
|
||||
): RepositoryInfo = object : RepositoryInfo {
|
||||
override val model: RepositoryModel = model
|
||||
override val currentRepositoryId: Long? = currentRepositoryId
|
||||
override fun onRepositorySelected(repositoryItem: RepositoryItem) {}
|
||||
override fun onAddRepo() {}
|
||||
override fun onRepositoryMoved(fromIndex: Int, toIndex: Int) {}
|
||||
override fun onRepositoriesFinishedMoving(fromRepoId: Long, toRepoId: Long) {}
|
||||
}
|
||||
|
||||
224
next/src/main/kotlin/org/fdroid/utils/DragDropState.kt
Normal file
224
next/src/main/kotlin/org/fdroid/utils/DragDropState.kt
Normal file
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
* Copyright 2021 The Android Open Source Project
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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.
|
||||
*
|
||||
* Taken from (and then modified to provide onEnd callback):
|
||||
* https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt;drc=e6d33dd5d0a60001a5784d84123b05308d35f410
|
||||
*/
|
||||
|
||||
package org.fdroid.utils
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
|
||||
import androidx.compose.foundation.gestures.scrollBy
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.foundation.lazy.LazyListItemInfo
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.zIndex
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun rememberDragDropState(
|
||||
lazyListState: LazyListState,
|
||||
onMove: (Int, Int) -> Unit,
|
||||
onEnd: (Any, Any) -> Unit
|
||||
): DragDropState {
|
||||
val scope = rememberCoroutineScope()
|
||||
val state = remember(lazyListState) {
|
||||
DragDropState(state = lazyListState, onMove = onMove, onEnd = onEnd, scope = scope)
|
||||
}
|
||||
LaunchedEffect(state) {
|
||||
while (true) {
|
||||
val diff = state.scrollChannel.receive()
|
||||
lazyListState.scrollBy(diff)
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
class DragDropState internal constructor(
|
||||
private val state: LazyListState,
|
||||
private val scope: CoroutineScope,
|
||||
private val onMove: (Int, Int) -> Unit,
|
||||
private val onEnd: (Any, Any) -> Unit,
|
||||
) {
|
||||
private var movedFrom: Int? = null
|
||||
private var movedFromKey: Any? = null
|
||||
private var movedToKey: Any? = null
|
||||
var draggingItemIndex by mutableStateOf<Int?>(null)
|
||||
private set
|
||||
|
||||
internal val scrollChannel = Channel<Float>()
|
||||
|
||||
private var draggingItemDraggedDelta by mutableFloatStateOf(0f)
|
||||
private var draggingItemInitialOffset by mutableIntStateOf(0)
|
||||
internal val draggingItemOffset: Float
|
||||
get() =
|
||||
draggingItemLayoutInfo?.let { item ->
|
||||
draggingItemInitialOffset + draggingItemDraggedDelta - item.offset
|
||||
} ?: 0f
|
||||
|
||||
private val draggingItemLayoutInfo: LazyListItemInfo?
|
||||
get() = state.layoutInfo.visibleItemsInfo.firstOrNull { it.index == draggingItemIndex }
|
||||
|
||||
internal var previousIndexOfDraggedItem by mutableStateOf<Int?>(null)
|
||||
private set
|
||||
|
||||
internal var previousItemOffset = Animatable(0f)
|
||||
private set
|
||||
|
||||
internal fun onDragStart(offset: Offset) {
|
||||
state.layoutInfo.visibleItemsInfo
|
||||
.firstOrNull { item -> offset.y.toInt() in item.offset..(item.offset + item.size) }
|
||||
?.also {
|
||||
movedFrom = it.index
|
||||
movedFromKey = it.key
|
||||
draggingItemIndex = it.index
|
||||
draggingItemInitialOffset = it.offset
|
||||
}
|
||||
}
|
||||
|
||||
internal fun onDragInterrupted() {
|
||||
if (draggingItemIndex != null) {
|
||||
previousIndexOfDraggedItem = draggingItemIndex
|
||||
val startOffset = draggingItemOffset
|
||||
scope.launch {
|
||||
previousItemOffset.snapTo(startOffset)
|
||||
previousItemOffset.animateTo(
|
||||
0f,
|
||||
spring(stiffness = Spring.StiffnessMediumLow, visibilityThreshold = 1f),
|
||||
)
|
||||
previousIndexOfDraggedItem = null
|
||||
}
|
||||
}
|
||||
draggingItemDraggedDelta = 0f
|
||||
draggingItemIndex = null
|
||||
draggingItemInitialOffset = 0
|
||||
movedFrom = null
|
||||
movedFromKey = null
|
||||
movedToKey = null
|
||||
}
|
||||
|
||||
internal fun onDragEnd() {
|
||||
val from = movedFromKey ?: error("Moved from was null")
|
||||
val to = movedToKey ?: from
|
||||
if (from != to) onEnd(from, to)
|
||||
onDragInterrupted()
|
||||
}
|
||||
|
||||
internal fun onDrag(offset: Offset) {
|
||||
draggingItemDraggedDelta += offset.y
|
||||
|
||||
val draggingItem = draggingItemLayoutInfo ?: return
|
||||
val startOffset = draggingItem.offset + draggingItemOffset
|
||||
val endOffset = startOffset + draggingItem.size
|
||||
val middleOffset = startOffset + (endOffset - startOffset) / 2f
|
||||
|
||||
val targetItem =
|
||||
state.layoutInfo.visibleItemsInfo.find { item ->
|
||||
middleOffset.toInt() in item.offset..item.offsetEnd &&
|
||||
draggingItem.index != item.index
|
||||
}
|
||||
if (targetItem != null) {
|
||||
if (
|
||||
draggingItem.index == state.firstVisibleItemIndex ||
|
||||
targetItem.index == state.firstVisibleItemIndex
|
||||
) {
|
||||
state.requestScrollToItem(
|
||||
state.firstVisibleItemIndex,
|
||||
state.firstVisibleItemScrollOffset,
|
||||
)
|
||||
}
|
||||
onMove.invoke(draggingItem.index, targetItem.index)
|
||||
draggingItemIndex = targetItem.index
|
||||
movedToKey = targetItem.key
|
||||
} else {
|
||||
val overscroll =
|
||||
when {
|
||||
draggingItemDraggedDelta > 0 ->
|
||||
(endOffset - state.layoutInfo.viewportEndOffset).coerceAtLeast(0f)
|
||||
draggingItemDraggedDelta < 0 ->
|
||||
(startOffset - state.layoutInfo.viewportStartOffset).coerceAtMost(0f)
|
||||
else -> 0f
|
||||
}
|
||||
if (overscroll != 0f) {
|
||||
scrollChannel.trySend(overscroll)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val LazyListItemInfo.offsetEnd: Int
|
||||
get() = this.offset + this.size
|
||||
}
|
||||
|
||||
fun Modifier.dragContainer(dragDropState: DragDropState): Modifier {
|
||||
return pointerInput(dragDropState) {
|
||||
detectDragGesturesAfterLongPress(
|
||||
onDrag = { change, offset ->
|
||||
change.consume()
|
||||
dragDropState.onDrag(offset = offset)
|
||||
},
|
||||
onDragStart = { offset -> dragDropState.onDragStart(offset) },
|
||||
onDragEnd = { dragDropState.onDragEnd() },
|
||||
onDragCancel = { dragDropState.onDragInterrupted() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LazyItemScope.DraggableItem(
|
||||
dragDropState: DragDropState,
|
||||
index: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable ColumnScope.(isDragging: Boolean) -> Unit,
|
||||
) {
|
||||
val dragging = index == dragDropState.draggingItemIndex
|
||||
val draggingModifier =
|
||||
if (dragging) {
|
||||
LocalHapticFeedback.current.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
Modifier
|
||||
.zIndex(1f)
|
||||
.graphicsLayer { translationY = dragDropState.draggingItemOffset }
|
||||
} else if (index == dragDropState.previousIndexOfDraggedItem) {
|
||||
Modifier
|
||||
.zIndex(1f)
|
||||
.graphicsLayer {
|
||||
translationY = dragDropState.previousItemOffset.value
|
||||
}
|
||||
} else {
|
||||
Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||
}
|
||||
Column(modifier = modifier.then(draggingModifier)) { content(dragging) }
|
||||
}
|
||||
Reference in New Issue
Block a user