mirror of https://github.com/tasks/tasks
Drag and drop to rearrange drawer
parent
e297ed4bd2
commit
745c17fbc5
@ -0,0 +1,66 @@
|
||||
package org.tasks.activities
|
||||
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListUpdateCallback
|
||||
import com.todoroo.andlib.utility.AndroidUtilities
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import io.reactivex.subjects.PublishSubject
|
||||
import java.util.*
|
||||
|
||||
interface DragAndDropDiffer<T, R> : ListUpdateCallback {
|
||||
val publishSubject: PublishSubject<R>
|
||||
val updates: Queue<Pair<R, DiffUtil.DiffResult?>>
|
||||
val disposables: CompositeDisposable
|
||||
var items: R
|
||||
var dragging: Boolean
|
||||
|
||||
fun submitList(list: List<T>) {
|
||||
disposables.add(
|
||||
Single.fromCallable { transform(list) }
|
||||
.subscribeOn(Schedulers.computation())
|
||||
.subscribe(publishSubject::onNext))
|
||||
}
|
||||
|
||||
fun calculateDiff(last: Pair<R, DiffUtil.DiffResult?>, next: R): Pair<R, DiffUtil.DiffResult?> {
|
||||
AndroidUtilities.assertNotMainThread()
|
||||
return Pair(next, diff(last.first!!, next))
|
||||
}
|
||||
|
||||
fun applyDiff(update: Pair<R, DiffUtil.DiffResult?>) {
|
||||
AndroidUtilities.assertMainThread()
|
||||
updates.add(update)
|
||||
if (!dragging) {
|
||||
drainQueue()
|
||||
}
|
||||
}
|
||||
|
||||
fun drainQueue() {
|
||||
AndroidUtilities.assertMainThread()
|
||||
var update = updates.poll()
|
||||
while (update != null) {
|
||||
items = update.first
|
||||
update.second?.dispatchUpdatesTo(this as ListUpdateCallback)
|
||||
update = updates.poll()
|
||||
}
|
||||
}
|
||||
|
||||
fun initializeDiffer(list: List<T>): R {
|
||||
val initial = transform(list)
|
||||
disposables.add(publishSubject
|
||||
.observeOn(Schedulers.computation())
|
||||
.scan(Pair(initial, null), { last: Pair<R, DiffUtil.DiffResult?>, next: R ->
|
||||
calculateDiff(last, next)
|
||||
})
|
||||
.skip(1)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::applyDiff))
|
||||
return initial
|
||||
}
|
||||
|
||||
fun transform(list: List<T>): R
|
||||
|
||||
fun diff(last: R, next: R): DiffUtil.DiffResult
|
||||
}
|
@ -0,0 +1,266 @@
|
||||
package org.tasks.activities
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.ItemTouchHelper.*
|
||||
import androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.todoroo.astrid.adapter.FilterViewHolder
|
||||
import com.todoroo.astrid.adapter.NavigationDrawerAdapter
|
||||
import com.todoroo.astrid.api.*
|
||||
import com.todoroo.astrid.api.FilterListItem.Type.ITEM
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import org.tasks.LocalBroadcastManager
|
||||
import org.tasks.R
|
||||
import org.tasks.caldav.BaseCaldavCalendarSettingsActivity
|
||||
import org.tasks.data.*
|
||||
import org.tasks.databinding.ActivityTagOrganizerBinding
|
||||
import org.tasks.dialogs.NewFilterDialog.Companion.newFilterDialog
|
||||
import org.tasks.filters.FilterProvider
|
||||
import org.tasks.filters.NavigationDrawerAction
|
||||
import org.tasks.filters.PlaceFilter
|
||||
import org.tasks.injection.ActivityComponent
|
||||
import org.tasks.injection.ThemedInjectingAppCompatActivity
|
||||
import org.tasks.preferences.Preferences
|
||||
import org.tasks.ui.NavigationDrawerFragment.Companion.REQUEST_NEW_FILTER
|
||||
import javax.inject.Inject
|
||||
|
||||
class NavigationDrawerCustomization : ThemedInjectingAppCompatActivity(), Toolbar.OnMenuItemClickListener {
|
||||
|
||||
@Inject lateinit var filterProvider: FilterProvider
|
||||
@Inject lateinit var adapter: NavigationDrawerAdapter
|
||||
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
|
||||
@Inject lateinit var preferences: Preferences
|
||||
@Inject lateinit var tagDataDao: TagDataDao
|
||||
@Inject lateinit var googleTaskListDao: GoogleTaskListDao
|
||||
@Inject lateinit var filterDao: FilterDao
|
||||
@Inject lateinit var caldavDao: CaldavDao
|
||||
@Inject lateinit var locationDao: LocationDao
|
||||
|
||||
private lateinit var binding: ActivityTagOrganizerBinding
|
||||
private lateinit var toolbar: Toolbar
|
||||
private var disposables: CompositeDisposable? = null
|
||||
private val refreshReceiver = RefreshReceiver()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivityTagOrganizerBinding.inflate(layoutInflater)
|
||||
|
||||
setContentView(binding.root)
|
||||
|
||||
toolbar = binding.toolbar.toolbar
|
||||
|
||||
toolbar.title = getString(R.string.manage_lists)
|
||||
toolbar.navigationIcon = getDrawable(R.drawable.ic_outline_arrow_back_24px)
|
||||
toolbar.setNavigationOnClickListener { finish() }
|
||||
toolbar.setOnMenuItemClickListener(this)
|
||||
toolbar.inflateMenu(R.menu.menu_nav_drawer_customization)
|
||||
themeColor.apply(toolbar)
|
||||
themeColor.applyToSystemBars(this)
|
||||
|
||||
adapter.setOnClick(this::onClick)
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
binding.recyclerView.adapter = adapter
|
||||
|
||||
val itemTouchHelper = ItemTouchHelper(ItemTouchHelperCallback())
|
||||
itemTouchHelper.attachToRecyclerView(binding.recyclerView)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
localBroadcastManager.unregisterReceiver(refreshReceiver)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
disposables = CompositeDisposable()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
disposables?.dispose()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
localBroadcastManager.registerRefreshListReceiver(refreshReceiver)
|
||||
updateFilters()
|
||||
}
|
||||
|
||||
private fun updateFilters() =
|
||||
disposables?.add(
|
||||
Single.fromCallable {
|
||||
filterProvider.drawerCustomizationItems.apply {
|
||||
forEach { f -> f.count = 0 }
|
||||
}
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(adapter::submitList))
|
||||
|
||||
private fun onClick(item: FilterListItem?) {
|
||||
if (item is NavigationDrawerAction) {
|
||||
when (item.requestCode) {
|
||||
REQUEST_NEW_FILTER ->
|
||||
newFilterDialog().show(supportFragmentManager, FRAG_TAG_NEW_FILTER)
|
||||
else -> startActivity(item.intent)
|
||||
}
|
||||
} else {
|
||||
when (item) {
|
||||
is GtasksFilter ->
|
||||
Intent(this, GoogleTaskListSettingsActivity::class.java)
|
||||
.putExtra(GoogleTaskListSettingsActivity.EXTRA_STORE_DATA, item.list)
|
||||
.apply(this::startActivity)
|
||||
is CaldavFilter ->
|
||||
caldavDao.getAccountByUuid(item.account)?.let {
|
||||
Intent(this, it.listSettingsClass())
|
||||
.putExtra(BaseCaldavCalendarSettingsActivity.EXTRA_CALDAV_CALENDAR, item.calendar)
|
||||
.apply(this::startActivity)
|
||||
}
|
||||
is CustomFilter ->
|
||||
Intent(this, FilterSettingsActivity::class.java)
|
||||
.putExtra(FilterSettingsActivity.TOKEN_FILTER, item)
|
||||
.apply(this::startActivity)
|
||||
is TagFilter ->
|
||||
Intent(this, TagSettingsActivity::class.java)
|
||||
.putExtra(TagSettingsActivity.EXTRA_TAG_DATA, item.tagData)
|
||||
.apply(this::startActivity)
|
||||
is PlaceFilter ->
|
||||
Intent(this, PlaceSettingsActivity::class.java)
|
||||
.putExtra(PlaceSettingsActivity.EXTRA_PLACE, item.place as Parcelable)
|
||||
.apply(this::startActivity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun inject(component: ActivityComponent) = component.inject(this)
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
return if (item.itemId == R.id.reset_sort) {
|
||||
filterDao.resetOrders()
|
||||
caldavDao.resetOrders()
|
||||
googleTaskListDao.resetOrders()
|
||||
tagDataDao.resetOrders()
|
||||
locationDao.resetOrders()
|
||||
updateFilters()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private inner class RefreshReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
val action = intent?.action
|
||||
if (LocalBroadcastManager.REFRESH == action || LocalBroadcastManager.REFRESH_LIST == action) {
|
||||
updateFilters()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class ItemTouchHelperCallback : ItemTouchHelper.Callback() {
|
||||
private var from = -1
|
||||
private var to = -1
|
||||
|
||||
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) =
|
||||
if (viewHolder.itemViewType == ITEM.ordinal) ALLOW_DRAGGING else NO_MOVEMENT
|
||||
|
||||
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
|
||||
super.onSelectedChanged(viewHolder, actionState)
|
||||
if (actionState == ACTION_STATE_DRAG) {
|
||||
adapter.dragging = true
|
||||
(viewHolder as FilterViewHolder).setMoving(true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
|
||||
if (target !is FilterViewHolder) {
|
||||
return false
|
||||
}
|
||||
val sourceFilter = (viewHolder as FilterViewHolder).filter
|
||||
val targetFilter = target.filter
|
||||
if (sourceFilter::class.java != targetFilter::class.java) {
|
||||
return false
|
||||
}
|
||||
if (sourceFilter is GtasksFilter && targetFilter is GtasksFilter) {
|
||||
if (sourceFilter.account != targetFilter.account) {
|
||||
return false
|
||||
}
|
||||
} else if (sourceFilter is CaldavFilter && targetFilter is CaldavFilter) {
|
||||
if (sourceFilter.account != targetFilter.account) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
val sourcePosition = viewHolder.adapterPosition
|
||||
if (from == -1) {
|
||||
from = sourcePosition
|
||||
}
|
||||
to = target.adapterPosition
|
||||
|
||||
adapter.notifyItemMoved(sourcePosition, to)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
|
||||
super.clearView(recyclerView, viewHolder)
|
||||
|
||||
(viewHolder as FilterViewHolder).setMoving(false)
|
||||
|
||||
if (from != to) {
|
||||
viewHolder.filter.order = to
|
||||
adapter.items
|
||||
.apply {
|
||||
removeAt(from)
|
||||
add(to, viewHolder.filter)
|
||||
}
|
||||
.filter(getPredicate(viewHolder.filter))
|
||||
.forEachIndexed { order, filter ->
|
||||
filter.order = order
|
||||
setOrder(order, filter)
|
||||
}
|
||||
updateFilters()
|
||||
}
|
||||
|
||||
adapter.dragging = false
|
||||
from = -1
|
||||
to = -1
|
||||
}
|
||||
|
||||
private fun getPredicate(item: FilterListItem): (FilterListItem) -> Boolean = { f ->
|
||||
item::class.java == f::class.java && when (item) {
|
||||
is GtasksFilter -> item.account == (f as GtasksFilter).account
|
||||
is CaldavFilter -> item.account == (f as CaldavFilter).account
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
private fun setOrder(order: Int, filter: FilterListItem) {
|
||||
when (filter) {
|
||||
is GtasksFilter -> googleTaskListDao.setOrder(filter.list.id, order)
|
||||
is CaldavFilter -> caldavDao.setOrder(filter.calendar.id, order)
|
||||
is TagFilter -> tagDataDao.setOrder(filter.tagData.id!!, order)
|
||||
is CustomFilter -> filterDao.setOrder(filter.id, order)
|
||||
is PlaceFilter -> locationDao.setOrder(filter.place.id, order)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val NO_MOVEMENT = makeMovementFlags(0, 0)
|
||||
private val ALLOW_DRAGGING = makeMovementFlags(UP or DOWN, 0)
|
||||
private const val FRAG_TAG_NEW_FILTER = "frag_tag_new_filter"
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include
|
||||
android:id="@+id/toolbar"
|
||||
layout="@layout/toolbar" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/recycler_view" />
|
||||
|
||||
</LinearLayout>
|
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/reset_sort"
|
||||
android:title="@string/reset_sort_order"
|
||||
app:showAsAction="never" />
|
||||
|
||||
</menu>
|
Loading…
Reference in New Issue