Skip to content

Commit

Permalink
add suggestions order by drag and drop
Browse files Browse the repository at this point in the history
  • Loading branch information
Razeeman committed Jan 8, 2025
1 parent f588e3e commit 71edda7
Show file tree
Hide file tree
Showing 9 changed files with 151 additions and 35 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ Simple app that helps track how much time you spend on all the useless activitie

**Android App**

Copyright (C) 2020-2024
Copyright (C) 2020-2025
Anton Razinkov [email protected]

This program is free software: you can redistribute it and/or modify
Expand All @@ -128,7 +128,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.

**Wear OS App**

Copyright (C) 2023-2024
Copyright (C) 2023-2025
Joseph Hale https://jhale.dev, [@kantahrek](https://github.com/kantahrek), Anton Razinkov [email protected]

This Source Code Form is subject to the terms of the Mozilla Public
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,52 @@ package com.example.util.simpletimetracker.core.extension
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.example.util.simpletimetracker.feature_base_adapter.BaseRecyclerAdapter
import com.example.util.simpletimetracker.feature_base_adapter.ViewHolderType
import java.util.Collections

fun RecyclerView.onItemMoved(
getIsSelectable: (RecyclerView.ViewHolder?) -> Boolean = { true },
getSelectablePositions: ((RecyclerView.ViewHolder?) -> Pair<Int, Int>)? = null,
onSelected: (RecyclerView.ViewHolder?) -> Unit = {},
onClear: (RecyclerView.ViewHolder) -> Unit = {},
onMoved: (Int, Int) -> Unit = { _, _ -> },
onMoved: (items: List<ViewHolderType>, from: Int, to: Int) -> Unit = { _, _, _ -> },
) {
val dragDirections =
ItemTouchHelper.DOWN or ItemTouchHelper.UP or ItemTouchHelper.START or ItemTouchHelper.END
val dragDirections = ItemTouchHelper.DOWN or ItemTouchHelper.UP or
ItemTouchHelper.START or ItemTouchHelper.END

fun getNewItems(
adapter: BaseRecyclerAdapter,
fromPosition: Int,
toPosition: Int,
): List<ViewHolderType> {
val newList = adapter.currentList.toList()

if (fromPosition < toPosition) {
for (i in fromPosition until toPosition) {
Collections.swap(newList, i, i + 1)
}
} else {
for (i in fromPosition downTo toPosition + 1) {
Collections.swap(newList, i, i - 1)
}
}

return newList
}

val helper = object : ItemTouchHelper.SimpleCallback(0, 0) {

override fun getDragDirs(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
): Int {
return if (getIsSelectable(viewHolder)) {
dragDirections
} else {
0
}
}

ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(dragDirections, 0) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
Expand All @@ -21,9 +57,15 @@ fun RecyclerView.onItemMoved(
val fromPosition = viewHolder.adapterPosition
val toPosition = target.adapterPosition

onMoved(fromPosition, toPosition)
(adapter as? BaseRecyclerAdapter)?.apply {
onMove(fromPosition, toPosition)
getSelectablePositions?.invoke(viewHolder)?.let { (start, end) ->
if (toPosition < start) return false
if (toPosition > end) return false
}

(adapter as? BaseRecyclerAdapter)?.let { adapter ->
val newItems = getNewItems(adapter, fromPosition, toPosition)
adapter.submitList(newItems)
onMoved(newItems, fromPosition, toPosition)
}

return true
Expand All @@ -42,5 +84,7 @@ fun RecyclerView.onItemMoved(
super.clearView(recyclerView, viewHolder)
onClear(viewHolder)
}
}).attachToRecyclerView(this)
}

(ItemTouchHelper(helper)).attachToRecyclerView(this)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.example.util.simpletimetracker.core.utils

const val ITEM_ALPHA_SELECTED = 0.7f
const val ITEM_ALPHA_DEFAULT = 1.0f
const val ITEM_SCALE_SELECTED = 1.1f
const val ITEM_SCALE_DEFAULT = 1.0f
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.ListAdapter
import com.example.util.simpletimetracker.feature_base_adapter.loader.createLoaderAdapterDelegate
import timber.log.Timber
import java.util.Collections

class BaseRecyclerAdapter(
vararg delegatesList: RecyclerAdapterDelegate,
Expand Down Expand Up @@ -37,22 +36,6 @@ class BaseRecyclerAdapter(
fun getItemByPosition(position: Int): ViewHolderType? =
currentList.getOrNull(position)

fun onMove(fromPosition: Int, toPosition: Int) {
val newList = currentList.toList()

if (fromPosition < toPosition) {
for (i in fromPosition until toPosition) {
Collections.swap(newList, i, i + 1)
}
} else {
for (i in fromPosition downTo toPosition + 1) {
Collections.swap(newList, i, i - 1)
}
}

submitList(newList)
}

fun replace(newItems: List<ViewHolderType>) {
submitList(newItems)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import com.example.util.simpletimetracker.feature_base_adapter.databinding.ItemB

// TODO remove ripple from icon background if background is transparent.
// TODO SUG add backup tests
// TODO SUG add reorder
// TODO SUG add reorder hint
// TODO GOAL add backup tests, raise test file version
fun createButtonAdapterDelegate(
onClick: (ViewData) -> Unit,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import com.example.util.simpletimetracker.core.base.BaseBottomSheetFragment
import com.example.util.simpletimetracker.core.extension.onItemMoved
import com.example.util.simpletimetracker.core.extension.setFullScreen
import com.example.util.simpletimetracker.core.extension.setSkipCollapsed
import com.example.util.simpletimetracker.core.utils.ITEM_ALPHA_DEFAULT
import com.example.util.simpletimetracker.core.utils.ITEM_ALPHA_SELECTED
import com.example.util.simpletimetracker.core.utils.ITEM_SCALE_DEFAULT
import com.example.util.simpletimetracker.core.utils.ITEM_SCALE_SELECTED
import com.example.util.simpletimetracker.core.utils.fragmentArgumentDelegate
import com.example.util.simpletimetracker.feature_base_adapter.BaseRecyclerAdapter
import com.example.util.simpletimetracker.feature_base_adapter.category.createCategoryAdapterDelegate
Expand Down Expand Up @@ -91,12 +95,6 @@ class CardOrderDialogFragment : BaseBottomSheetFragment<Binding>() {
}

companion object {
private const val ITEM_ALPHA_SELECTED = 0.7f
private const val ITEM_ALPHA_DEFAULT = 1.0f

private const val ITEM_SCALE_SELECTED = 1.1f
private const val ITEM_SCALE_DEFAULT = 1.0f

private const val ARGS_PARAMS = "args_card_order_params"

fun createBundle(data: CardOrderDialogParams): Bundle = Bundle().apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class ActivitySuggestionsCalculateInteractor @Inject constructor(
): List<Result> {
// TODO selectable range and number of suggestions?
val range = timeMapper.getRangeStartAndEnd(
rangeLength = RangeLength.Year,
rangeLength = RangeLength.Last(365),
shift = 0,
firstDayOfWeek = prefsInteractor.getFirstDayOfWeek(),
startOfDayShift = prefsInteractor.getStartOfDayShift(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,26 @@ package com.example.util.simpletimetracker.feature_suggestions.view
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.RecyclerView
import com.example.util.simpletimetracker.core.base.BaseFragment
import com.example.util.simpletimetracker.core.dialog.TypesSelectionDialogListener
import com.example.util.simpletimetracker.core.extension.onItemMoved
import com.example.util.simpletimetracker.core.utils.ITEM_ALPHA_DEFAULT
import com.example.util.simpletimetracker.core.utils.ITEM_ALPHA_SELECTED
import com.example.util.simpletimetracker.core.utils.ITEM_SCALE_DEFAULT
import com.example.util.simpletimetracker.core.utils.ITEM_SCALE_SELECTED
import com.example.util.simpletimetracker.core.utils.InsetConfiguration
import com.example.util.simpletimetracker.domain.extension.orFalse
import com.example.util.simpletimetracker.domain.extension.orZero
import com.example.util.simpletimetracker.feature_base_adapter.BaseRecyclerAdapter
import com.example.util.simpletimetracker.feature_base_adapter.ViewHolderType
import com.example.util.simpletimetracker.feature_base_adapter.button.createButtonAdapterDelegate
import com.example.util.simpletimetracker.feature_base_adapter.divider.createDividerAdapterDelegate
import com.example.util.simpletimetracker.feature_base_adapter.emptySpace.createEmptySpaceAdapterDelegate
import com.example.util.simpletimetracker.feature_base_adapter.hint.createHintAdapterDelegate
import com.example.util.simpletimetracker.feature_base_adapter.loader.createLoaderAdapterDelegate
import com.example.util.simpletimetracker.feature_base_adapter.recordType.createRecordTypeAdapterDelegate
import com.example.util.simpletimetracker.feature_suggestions.adapter.ActivitySuggestionListViewData
import com.example.util.simpletimetracker.feature_suggestions.adapter.createActivitySuggestionListAdapterDelegate
import com.example.util.simpletimetracker.feature_suggestions.adapter.createActivitySuggestionSpecialAdapterDelegate
import com.example.util.simpletimetracker.feature_suggestions.viewModel.ActivitySuggestionsViewModel
Expand Down Expand Up @@ -64,6 +74,7 @@ class ActivitySuggestionsFragment :

override fun initUx() = with(binding) {
btnActivitySuggestionsSave.setOnClick(throttle(viewModel::onSaveClick))
initOnItemMoved()
}

override fun initViewModel(): Unit = with(viewModel) {
Expand All @@ -73,4 +84,59 @@ class ActivitySuggestionsFragment :
override fun onDataSelected(dataIds: List<Long>, tag: String?) {
viewModel.onTypesSelected(dataIds, tag)
}

private fun initOnItemMoved() = with(binding) {
fun ViewHolderType.isSelectable(): Boolean {
return this is ActivitySuggestionListViewData
}

rvActivitySuggestionsList.onItemMoved(
getIsSelectable = { viewHolder ->
viewHolder?.adapterPosition
?.let { viewDataAdapter.getItemByPosition(it) }
?.isSelectable()
.orFalse()
},
getSelectablePositions = { viewHolder ->
val items = viewDataAdapter.currentList
val itemPosition = viewHolder?.adapterPosition.orZero()

val from = items.indexOfLast {
val index = items.indexOf(it)
val prevItem = items.getOrNull(index - 1)
it.isSelectable() &&
index <= itemPosition &&
(prevItem != null && !prevItem.isSelectable())
}
val to = items.indexOfFirst {
val index = items.indexOf(it)
val nextItem = items.getOrNull(index + 1)
it.isSelectable() &&
index >= itemPosition &&
(nextItem != null && !nextItem.isSelectable())
}
from to to
},
onSelected = ::setItemSelected,
onClear = ::setItemUnselected,
onMoved = { list, _, to ->
viewModel.onItemMoved(
items = list,
toPosition = to,
)
},
)
}

private fun setItemSelected(viewHolder: RecyclerView.ViewHolder?) = viewHolder?.run {
itemView.alpha = ITEM_ALPHA_SELECTED
itemView.scaleX = ITEM_SCALE_SELECTED
itemView.scaleY = ITEM_SCALE_SELECTED
} ?: Unit

private fun setItemUnselected(viewHolder: RecyclerView.ViewHolder) = viewHolder.run {
itemView.alpha = ITEM_ALPHA_DEFAULT
itemView.scaleX = ITEM_SCALE_DEFAULT
itemView.scaleY = ITEM_SCALE_DEFAULT
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.example.util.simpletimetracker.feature_base_adapter.button.ButtonView
import com.example.util.simpletimetracker.feature_base_adapter.loader.LoaderViewData
import com.example.util.simpletimetracker.feature_suggestions.viewData.ActivitySuggestionsButtonViewData
import com.example.util.simpletimetracker.feature_suggestions.R
import com.example.util.simpletimetracker.feature_suggestions.adapter.ActivitySuggestionListViewData
import com.example.util.simpletimetracker.feature_suggestions.adapter.ActivitySuggestionSpecialViewData
import com.example.util.simpletimetracker.feature_suggestions.interactor.ActivitySuggestionsCalculateInteractor
import com.example.util.simpletimetracker.feature_suggestions.interactor.ActivitySuggestionsViewDataInteractor
Expand Down Expand Up @@ -117,6 +118,24 @@ class ActivitySuggestionsViewModel @Inject constructor(
}
}

fun onItemMoved(
items: List<ViewHolderType>,
toPosition: Int,
) {
val movedItem = items.getOrNull(toPosition)
as? ActivitySuggestionListViewData ?: return
val forTypeId = movedItem.id.forTypeId
val newOrder = items
.filterIsInstance<ActivitySuggestionListViewData>()
.filter { it.id.forTypeId == forTypeId }
val newSuggestions = suggestions[forTypeId].orEmpty().sortedBy { suggestionId ->
newOrder.indexOfFirst { it.id.suggestionTypeId == suggestionId }
}
suggestions = suggestions.toMutableMap().apply {
put(forTypeId, newSuggestions)
}
}

fun onSaveClick() {
viewModelScope.launch {
// Remove all.
Expand Down

0 comments on commit 71edda7

Please sign in to comment.