Collapsible sort group headers

pull/996/head
Alex Baker 6 years ago
parent 395cba2705
commit 8b2ed5b1e9

@ -41,7 +41,7 @@ class CaldavManualSortTaskAdapterTest : InjectingTestCase() {
private val dataSource = object : TaskAdapterDataSource {
override fun getItem(position: Int) = tasks[position]
override fun getItemCount() = tasks.size
override fun getTaskCount() = tasks.size
}
@Before

@ -36,7 +36,7 @@ class CaldavTaskAdapterTest : InjectingTestCase() {
adapter.setDataSource(object : TaskAdapterDataSource {
override fun getItem(position: Int) = tasks[position]
override fun getItemCount() = tasks.size
override fun getTaskCount() = tasks.size
})
}

@ -40,7 +40,7 @@ class GoogleTaskManualSortAdapterTest : InjectingTestCase() {
private val dataSource = object : TaskAdapterDataSource {
override fun getItem(position: Int) = tasks[position]
override fun getItemCount() = tasks.size
override fun getTaskCount() = tasks.size
}
@Test

@ -404,6 +404,7 @@ class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandl
}
override fun sortChanged(reload: Boolean) {
taskListFragment?.clearCollapsed()
localBroadcastManager.broadcastRefresh()
if (reload) {
openTaskListFragment(filter, true)

@ -167,7 +167,7 @@ class TaskListFragment : InjectingFragment(), OnRefreshListener, Toolbar.OnMenuI
if (savedInstanceState != null) {
val longArray = savedInstanceState.getLongArray(EXTRA_SELECTED_TASK_IDS)
if (longArray?.isNotEmpty() == true) {
taskAdapter.setSelected(*longArray)
taskAdapter.setSelected(longArray.toList())
startActionMode()
}
}
@ -185,6 +185,7 @@ class TaskListFragment : InjectingFragment(), OnRefreshListener, Toolbar.OnMenuI
val selectedTaskIds: List<Long> = taskAdapter.getSelected()
outState.putLongArray(EXTRA_SELECTED_TASK_IDS, selectedTaskIds.toLongArray())
outState.putString(EXTRA_SEARCH, searchQuery)
outState.putLongArray(EXTRA_COLLAPSED, taskAdapter.getCollapsed().toLongArray())
}
override fun onCreateView(
@ -197,6 +198,7 @@ class TaskListFragment : InjectingFragment(), OnRefreshListener, Toolbar.OnMenuI
// set up list adapters
taskAdapter = taskAdapterProvider.createTaskAdapter(filter)
taskAdapter.setCollapsed(savedInstanceState?.getLongArray(EXTRA_COLLAPSED))
taskListViewModel = ViewModelProvider(requireActivity()).get(TaskListViewModel::class.java)
if (savedInstanceState != null) {
searchQuery = savedInstanceState.getString(EXTRA_SEARCH)
@ -231,13 +233,13 @@ class TaskListFragment : InjectingFragment(), OnRefreshListener, Toolbar.OnMenuI
if (recyclerAdapter !is PagedListRecyclerAdapter) {
setAdapter(
PagedListRecyclerAdapter(
taskAdapter, recyclerView, viewHolderFactory, this, tasks, taskDao))
taskAdapter, recyclerView, viewHolderFactory, this, tasks, taskDao, preferences))
return
}
} else if (recyclerAdapter !is DragAndDropRecyclerAdapter) {
setAdapter(
DragAndDropRecyclerAdapter(
taskAdapter, recyclerView, viewHolderFactory, this, tasks as MutableList, taskDao))
taskAdapter, recyclerView, viewHolderFactory, this, tasks as MutableList, taskDao, preferences))
return
}
recyclerAdapter!!.submitList(tasks)
@ -740,6 +742,8 @@ class TaskListFragment : InjectingFragment(), OnRefreshListener, Toolbar.OnMenuI
makeSnackbar(R.string.copy_multiple_tasks_confirmation, duplicates.size.toString()).show()
}
fun clearCollapsed() = taskAdapter.clearCollapsed()
companion object {
const val TAGS_METADATA_JOIN = "for_tags" // $NON-NLS-1$
const val GTASK_METADATA_JOIN = "googletask" // $NON-NLS-1$
@ -748,6 +752,7 @@ class TaskListFragment : InjectingFragment(), OnRefreshListener, Toolbar.OnMenuI
const val ACTION_DELETED = "action_deleted"
private const val EXTRA_SELECTED_TASK_IDS = "extra_selected_task_ids"
private const val EXTRA_SEARCH = "extra_search"
private const val EXTRA_COLLAPSED = "extra_collapsed"
private const val VOICE_RECOGNITION_REQUEST_CODE = 1234
private const val EXTRA_FILTER = "extra_filter"
private const val FRAG_TAG_REMOTE_LIST_PICKER = "frag_tag_remote_list_picker"

@ -12,6 +12,9 @@ open class CaldavTaskAdapter internal constructor(private val taskDao: TaskDao,
override fun minIndent(nextPosition: Int, task: TaskContainer): Int {
(nextPosition until count).forEach {
if (isHeader(it)) {
return 0
}
if (!taskIsChild(task, it)) {
return getTask(it).indent
}
@ -79,6 +82,9 @@ open class CaldavTaskAdapter internal constructor(private val taskDao: TaskDao,
private fun taskIsChild(source: TaskContainer, destinationIndex: Int): Boolean {
(destinationIndex downTo 0).forEach {
if (isHeader(it)) {
return false
}
when (getTask(it).parent) {
0L -> return false
source.parent -> return false

@ -11,10 +11,11 @@ import java.util.*
open class TaskAdapter {
private val selected = HashSet<Long>()
private val collapsed = HashSet<Long>()
private lateinit var dataSource: TaskAdapterDataSource
val count: Int
get() = dataSource.getItemCount()
get() = dataSource.getTaskCount()
fun setDataSource(dataSource: TaskAdapterDataSource) {
this.dataSource = dataSource
@ -25,15 +26,22 @@ open class TaskAdapter {
fun getSelected(): ArrayList<Long> = ArrayList(selected)
fun setSelected(vararg ids: Long) = setSelected(ids.toList())
fun setSelected(ids: Collection<Long>) {
selected.clear()
clearSelections()
selected.addAll(ids)
}
fun clearSelections() = selected.clear()
fun getCollapsed(): ArrayList<Long> = ArrayList(collapsed)
fun setCollapsed(groups: LongArray?) {
clearCollapsed()
groups?.toList()?.let(collapsed::addAll)
}
fun clearCollapsed() = collapsed.clear()
open fun getIndent(task: TaskContainer): Int = task.getIndent()
open fun canMove(source: TaskContainer, from: Int, target: TaskContainer, to: Int): Boolean = false
@ -53,12 +61,22 @@ open class TaskAdapter {
}
}
fun toggleCollapsed(group: Long) {
if (collapsed.contains(group)) {
collapsed.remove(group)
} else {
collapsed.add(group)
}
}
open fun supportsParentingOrManualSort(): Boolean = false
open fun supportsManualSorting(): Boolean = false
open fun moved(from: Int, to: Int, indent: Int) {}
fun isHeader(position: Int): Boolean = dataSource.isHeader(position)
fun getTask(position: Int): TaskContainer = dataSource.getItem(position)
fun getItemUuid(position: Int): String = getTask(position).uuid

@ -5,5 +5,7 @@ import org.tasks.data.TaskContainer
interface TaskAdapterDataSource {
fun getItem(position: Int): TaskContainer
fun getItemCount(): Int
fun getTaskCount(): Int
fun isHeader(position: Int): Boolean = false
}

@ -11,6 +11,7 @@ import static org.tasks.db.QueryUtils.showHidden;
import static org.tasks.db.QueryUtils.showRecentlyCompleted;
import android.annotation.SuppressLint;
import androidx.annotation.Nullable;
import com.todoroo.andlib.sql.Functions;
import com.todoroo.andlib.sql.Order;
import com.todoroo.astrid.data.Task;
@ -122,6 +123,21 @@ public class SortHelper {
return order;
}
public static @Nullable String getSortGroup(int sortType) {
switch (sortType) {
case SORT_DUE:
return "tasks.dueDate";
case SORT_IMPORTANCE:
return "tasks.importance";
case SORT_MODIFIED:
return "tasks.modified";
case SORT_CREATED:
return "tasks.created";
default:
return null;
}
}
public static String orderSelectForSortTypeRecursive(int sortType) {
String select;
switch (sortType) {

@ -11,6 +11,7 @@ public class TaskContainer {
@Embedded public Location location;
public String tags;
public int children;
public Long sortGroup;
public long primarySort;
public long secondarySort;
public int indent;
@ -52,6 +53,10 @@ public class TaskContainer {
return task.hasDueDate();
}
public boolean hasDueTime() {
return task.hasDueTime();
}
public boolean isOverdue() {
return task.isOverdue();
}
@ -107,14 +112,24 @@ public class TaskContainer {
&& Objects.equals(googletask, that.googletask)
&& Objects.equals(caldavTask, that.caldavTask)
&& Objects.equals(location, that.location)
&& Objects.equals(tags, that.tags);
&& Objects.equals(tags, that.tags)
&& Objects.equals(sortGroup, that.sortGroup);
}
@Override
public int hashCode() {
return Objects
.hash(task, googletask, caldavTask, location, tags, children, primarySort,
secondarySort, indent, targetIndent);
return Objects.hash(
task,
googletask,
caldavTask,
location,
tags,
children,
sortGroup,
primarySort,
secondarySort,
indent,
targetIndent);
}
@Override
@ -133,6 +148,8 @@ public class TaskContainer {
+ '\''
+ ", children="
+ children
+ ", sortGroup="
+ sortGroup
+ ", primarySort="
+ primarySort
+ ", secondarySort="

@ -22,6 +22,7 @@ internal object TaskListQueryRecursive {
TaskListQuery.FIELDS.plus(listOf(
field("(${Query.select(field("group_concat(distinct(tag_uid))")).from(Tag.TABLE).where(Task.ID.eq(Tag.TASK))} GROUP BY ${Tag.TASK})").`as`("tags"),
field("indent"),
field("sort_group").`as`("sortGroup"),
field("children"),
field("primary_sort").`as`("primarySort"),
field("secondary_sort").`as`("secondarySort"))).toTypedArray()
@ -94,10 +95,10 @@ internal object TaskListQueryRecursive {
val sortSelect = SortHelper.orderSelectForSortTypeRecursive(sortMode)
val withClause ="""
CREATE TEMPORARY TABLE `recursive_tasks` AS
WITH RECURSIVE recursive_tasks (task, parent, collapsed, hidden, indent, title, sortField, primary_sort, secondary_sort) AS (
SELECT tasks._id, 0 as parent, tasks.collapsed as collapsed, 0 as hidden, 0 AS sort_indent, UPPER(tasks.title) AS sort_title, $sortSelect, $sortField as primary_sort, NULL as secondarySort FROM tasks
WITH RECURSIVE recursive_tasks (task, parent, collapsed, hidden, indent, title, sortField, primary_sort, secondary_sort, sort_group) AS (
SELECT tasks._id, 0 as parent, tasks.collapsed as collapsed, 0 as hidden, 0 AS sort_indent, UPPER(tasks.title) AS sort_title, $sortSelect, $sortField as primary_sort, NULL as secondarySort, ${SortHelper.getSortGroup(sortMode)} FROM tasks
$parentQuery
UNION ALL SELECT tasks._id, recursive_tasks.task as parent, tasks.collapsed as collapsed, CASE WHEN recursive_tasks.collapsed > 0 OR recursive_tasks.hidden > 0 THEN 1 ELSE 0 END as hidden, recursive_tasks.indent+1 AS sort_indent, UPPER(tasks.title) AS sort_title, $sortSelect, recursive_tasks.primary_sort as primary_sort, $sortField as secondary_sort FROM tasks
UNION ALL SELECT tasks._id, recursive_tasks.task as parent, tasks.collapsed as collapsed, CASE WHEN recursive_tasks.collapsed > 0 OR recursive_tasks.hidden > 0 THEN 1 ELSE 0 END as hidden, recursive_tasks.indent+1 AS sort_indent, UPPER(tasks.title) AS sort_title, $sortSelect, recursive_tasks.primary_sort as primary_sort, $sortField as secondary_sort, recursive_tasks.sort_group FROM tasks
$subtaskQuery
ORDER BY sort_indent DESC, ${SortHelper.orderForSortTypeRecursive(sortMode, reverseSort)}
) SELECT * FROM recursive_tasks

@ -23,6 +23,10 @@ class ApplicationModule(@get:Provides @get:ForApplication val context: Context)
val locale: Locale
get() = Locale.getInstance(context)
@Provides
@ApplicationScope
fun getJavaLocale(locale: Locale): java.util.Locale = locale.locale
@Provides
@ApplicationScope
fun getNotificationDao(db: Database): NotificationDao = db.notificationDao()

@ -0,0 +1,3 @@
package org.tasks.tasklist
data class AdapterSection(var firstPosition: Int, val value: Long, var sectionedPosition: Int = 0, var collapsed: Boolean = false)

@ -2,21 +2,32 @@ package org.tasks.tasklist
import androidx.recyclerview.widget.DiffUtil
import com.todoroo.astrid.adapter.TaskAdapter
import org.tasks.data.TaskContainer
internal class DiffCallback(private val old: List<TaskContainer>, private val new: List<TaskContainer>, @Deprecated("") private val adapter: TaskAdapter) : DiffUtil.Callback() {
internal class DiffCallback(private val old: SectionedDataSource, private val new: SectionedDataSource, @Deprecated("") private val adapter: TaskAdapter) : DiffUtil.Callback() {
override fun getOldListSize() = old.size
override fun getNewListSize() = new.size
override fun areItemsTheSame(oldPosition: Int, newPosition: Int): Boolean {
return old[oldPosition].id == new[newPosition].id
val wasHeader = old.isHeader(oldPosition)
val isHeader = new.isHeader(newPosition)
if (wasHeader != isHeader) {
return false
}
return if (isHeader) {
old.sortMode == new.sortMode && old.getHeaderValue(oldPosition) == new.getHeaderValue(newPosition)
} else {
old.getItem(oldPosition).id == new.getItem(newPosition).id
}
}
override fun areContentsTheSame(oldPosition: Int, newPosition: Int): Boolean {
val oldItem = old[oldPosition]
val newItem = new[newPosition]
if (new.isHeader(newPosition)) {
return old.getSection(oldPosition).collapsed == new.getSection(newPosition).collapsed
}
val oldItem = old.getItem(oldPosition)
val newItem = new.getItem(newPosition)
return oldItem == newItem && oldItem.getIndent() == adapter.getIndent(newItem)
}
}

@ -1,6 +1,7 @@
package org.tasks.tasklist
import android.graphics.Canvas
import android.view.ViewGroup
import androidx.core.util.Pair
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ItemTouchHelper
@ -11,11 +12,13 @@ import com.todoroo.astrid.activity.TaskListFragment
import com.todoroo.astrid.adapter.TaskAdapter
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.utility.Flags
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 org.tasks.data.TaskContainer
import org.tasks.preferences.Preferences
import java.util.*
import kotlin.math.max
import kotlin.math.min
@ -25,27 +28,61 @@ class DragAndDropRecyclerAdapter(
private val recyclerView: RecyclerView,
viewHolderFactory: ViewHolderFactory,
private val taskList: TaskListFragment,
private var list: MutableList<TaskContainer>,
taskDao: TaskDao) : TaskListRecyclerAdapter(adapter, viewHolderFactory, taskList, taskDao) {
private val publishSubject = PublishSubject.create<MutableList<TaskContainer>>()
tasks: MutableList<TaskContainer>,
taskDao: TaskDao,
preferences: Preferences) : TaskListRecyclerAdapter(adapter, viewHolderFactory, taskList, taskDao, preferences) {
private var list: SectionedDataSource
private val publishSubject = PublishSubject.create<SectionedDataSource>()
private val disposables = CompositeDisposable()
private val updates: Queue<Pair<MutableList<TaskContainer>, DiffUtil.DiffResult>> = LinkedList()
private val updates: Queue<Pair<SectionedDataSource, DiffUtil.DiffResult>> = LinkedList()
private var dragging = false
private val disableHeaders: Boolean
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val viewType = getItemViewType(position)
if (viewType == 1) {
val headerSection = list.getSection(position)
(holder as HeaderViewHolder).bind(taskList.getFilter(), preferences.sortMode, headerSection)
} else {
super.onBindViewHolder(holder, position)
}
}
override fun getItemViewType(position: Int) = if (list.isHeader(position)) 1 else 0
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = if (viewType == 1) {
viewHolderFactory.newHeaderViewHolder(parent, this::toggleGroup)
} else {
super.onCreateViewHolder(parent, viewType)
}
private fun toggleGroup(group: Long) {
adapter.toggleCollapsed(group)
taskList.loadTaskListContent()
}
override fun dragAndDropEnabled() = adapter.supportsParentingOrManualSort()
override fun getItem(position: Int) = list[position]
override fun isHeader(position: Int): Boolean = list.isHeader(position)
override fun getItem(position: Int): TaskContainer = list.getItem(position)
override fun submitList(list: List<TaskContainer>) = publishSubject.onNext(list as MutableList<TaskContainer>)
override fun submitList(list: List<TaskContainer>) {
disposables.add(
Single.fromCallable { SectionedDataSource(list as MutableList, disableHeaders, preferences.sortMode, adapter.getCollapsed().toMutableSet()) }
.subscribeOn(Schedulers.computation())
.subscribe(publishSubject::onNext))
}
private fun calculateDiff(
last: Pair<MutableList<TaskContainer>, DiffUtil.DiffResult>, next: MutableList<TaskContainer>): Pair<MutableList<TaskContainer>, DiffUtil.DiffResult> {
last: Pair<SectionedDataSource, DiffUtil.DiffResult>, next: SectionedDataSource): Pair<SectionedDataSource, DiffUtil.DiffResult> {
AndroidUtilities.assertNotMainThread()
val cb = DiffCallback(last.first!!, next, adapter)
val result = DiffUtil.calculateDiff(cb, next.size < LONG_LIST_SIZE)
return Pair.create(next, result)
}
private fun applyDiff(update: Pair<MutableList<TaskContainer>, DiffUtil.DiffResult>) {
private fun applyDiff(update: Pair<SectionedDataSource, DiffUtil.DiffResult>) {
AndroidUtilities.assertMainThread()
updates.add(update)
if (!dragging) {
@ -67,9 +104,9 @@ class DragAndDropRecyclerAdapter(
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) = disposables.dispose()
override fun getItemCount(): Int {
return list.size
}
override fun getTaskCount() = list.taskCount
override fun getItemCount() = list.size
private inner class ItemTouchHelperCallback : ItemTouchHelper.Callback() {
private var from = -1
@ -85,10 +122,14 @@ class DragAndDropRecyclerAdapter(
}
}
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) = if (adapter.supportsParentingOrManualSort() && adapter.numSelected == 0) {
makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT, 0)
} else {
makeMovementFlags(0, 0)
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
return if (adapter.isHeader(viewHolder.adapterPosition)) {
makeMovementFlags(0, 0)
} else if (adapter.supportsParentingOrManualSort() && adapter.numSelected == 0) {
makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT, 0)
} else {
makeMovementFlags(0, 0)
}
}
override fun onMove(
@ -99,7 +140,8 @@ class DragAndDropRecyclerAdapter(
val fromPosition = src.adapterPosition
val toPosition = target.adapterPosition
val source = src as TaskViewHolder
if (!adapter.canMove(source.task, fromPosition, (target as TaskViewHolder).task, toPosition)) {
val isHeader = isHeader(toPosition)
if (!isHeader && !adapter.canMove(source.task, fromPosition, (target as TaskViewHolder).task, toPosition)) {
return false
}
if (from == -1) {
@ -108,14 +150,19 @@ class DragAndDropRecyclerAdapter(
}
to = toPosition
notifyItemMoved(fromPosition, toPosition)
if (isHeader) {
val offset = if (fromPosition < toPosition) -1 else 1
list.moveSection(toPosition, offset)
}
updateIndents(source, from, to)
return true
}
private fun updateIndents(source: TaskViewHolder?, from: Int, to: Int) {
val task = source!!.task
source.minIndent = if (to == 0 || to == itemCount - 1) 0 else adapter.minIndent(if (from <= to) to + 1 else to, task)
source.maxIndent = if (to == 0) 0 else adapter.maxIndent(if (from >= to) to - 1 else to, task)
val previousIsHeader = to > 0 && isHeader(to - 1)
source.minIndent = if (to == 0 || to == itemCount - 1 || previousIsHeader) 0 else adapter.minIndent(if (from <= to) to + 1 else to, task)
source.maxIndent = if (to == 0 || previousIsHeader) 0 else adapter.maxIndent(if (from >= to) to - 1 else to, task)
}
override fun onChildDraw(
@ -191,7 +238,14 @@ class DragAndDropRecyclerAdapter(
throw UnsupportedOperationException()
}
private fun moved(from: Int, to: Int, indent: Int) {
private fun moved(fromOrig: Int, to: Int, indent: Int) {
val from = if (fromOrig == to) {
to
} else if (fromOrig > to && isHeader(fromOrig)) {
from - 1
} else {
from
}
adapter.moved(from, to, indent)
val task: TaskContainer = list.removeAt(from)
list.add(if (from < to) to - 1 else to, task)
@ -204,16 +258,18 @@ class DragAndDropRecyclerAdapter(
}
init {
val filter = taskList.getFilter()
disableHeaders = !filter.supportsSorting() || preferences.isManualSort && filter.supportsManualSort()
ItemTouchHelper(ItemTouchHelperCallback()).attachToRecyclerView(recyclerView)
val initial = Pair.create<MutableList<TaskContainer>, DiffUtil.DiffResult>(list, null)
disposables.add(
publishSubject
.observeOn(Schedulers.computation())
.scan(initial, { last: Pair<MutableList<TaskContainer>, DiffUtil.DiffResult>, next: List<TaskContainer> ->
calculateDiff(last, next.toMutableList())
})
.skip(1)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { update: Pair<MutableList<TaskContainer>, DiffUtil.DiffResult> -> applyDiff(update) })
list = SectionedDataSource(tasks, disableHeaders, preferences.sortMode, adapter.getCollapsed().toMutableSet())
val initial = Pair.create<SectionedDataSource, DiffUtil.DiffResult>(list, null)
disposables.add(publishSubject
.observeOn(Schedulers.computation())
.scan(initial, { last: Pair<SectionedDataSource, DiffUtil.DiffResult>, next: SectionedDataSource ->
calculateDiff(last, next)
})
.skip(1)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { update: Pair<SectionedDataSource, DiffUtil.DiffResult> -> applyDiff(update) })
}
}

@ -0,0 +1,73 @@
package org.tasks.tasklist
import android.content.Context
import android.view.View
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.recyclerview.widget.RecyclerView
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.core.SortHelper
import org.tasks.R
import org.tasks.date.DateTimeUtils.newDateTime
import org.threeten.bp.format.FormatStyle
import java.util.*
class HeaderViewHolder(
private val context: Context,
private val locale: Locale,
view: View,
callback: (Long) -> Unit) : RecyclerView.ViewHolder(view) {
private val header: TextView = view.findViewById(R.id.header)
private var sortGroup = -1L
fun bind(filter: Filter, sortMode: Int, section: AdapterSection) {
sortGroup = section.value
val header: String? = if (filter.supportsSorting()) getHeader(sortMode, sortGroup) else null
if (header == null) {
this.header.visibility = View.GONE
} else {
this.header.visibility = View.VISIBLE
this.header.text = header
this.header.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, if (section.collapsed) R.drawable.ic_keyboard_arrow_up_black_18dp else R.drawable.ic_keyboard_arrow_down_black_18dp, 0)
this.header.setTextColor(
context.getColor(
if (sortMode == SortHelper.SORT_DUE && sortGroup > 0 && newDateTime(sortGroup).plusDays(1).startOfDay().isBeforeNow) R.color.overdue else R.color.text_secondary))
}
}
private fun getHeader(sortMode: Int, group: Long): String {
return when {
sortMode == SortHelper.SORT_IMPORTANCE -> context.getString(priorityToString(group.toInt()))
group == 0L -> context.getString(if (sortMode == SortHelper.SORT_DUE) {
R.string.no_due_date
} else {
R.string.no_date
})
sortMode == SortHelper.SORT_CREATED ->
context.getString(R.string.sort_created_group, getDateString(group))
sortMode == SortHelper.SORT_MODIFIED ->
context.getString(R.string.sort_modified_group, getDateString(group))
else -> getDateString(group, false)
}
}
private fun getDateString(value: Long, lowercase: Boolean = true) =
DateUtilities.getRelativeDay(context, value, locale, FormatStyle.FULL, lowercase)
@StringRes
private fun priorityToString(priority: Int) = when (priority) {
0 -> R.string.filter_high_priority
1 -> R.string.filter_medium_priority
2 -> R.string.filter_low_priority
else -> R.string.filter_no_priority
}
init {
header.setOnClickListener {
callback.invoke(sortGroup)
}
}
}

@ -8,6 +8,7 @@ import com.todoroo.astrid.activity.TaskListFragment
import com.todoroo.astrid.adapter.TaskAdapter
import com.todoroo.astrid.dao.TaskDao
import org.tasks.data.TaskContainer
import org.tasks.preferences.Preferences
class PagedListRecyclerAdapter(
adapter: TaskAdapter,
@ -15,7 +16,8 @@ class PagedListRecyclerAdapter(
viewHolderFactory: ViewHolderFactory,
taskList: TaskListFragment,
list: List<TaskContainer>,
taskDao: TaskDao) : TaskListRecyclerAdapter(adapter, viewHolderFactory, taskList, taskDao) {
taskDao: TaskDao,
preferences: Preferences) : TaskListRecyclerAdapter(adapter, viewHolderFactory, taskList, taskDao, preferences) {
private val differ: AsyncPagedListDiffer<TaskContainer> =
AsyncPagedListDiffer(this, AsyncDifferConfig.Builder(ItemCallback()).build())
@ -34,6 +36,8 @@ class PagedListRecyclerAdapter(
override fun getItemCount() = differ.itemCount
override fun getTaskCount() = itemCount
init {
if (list is PagedList<*>) {
differ.submitList(list as PagedList<TaskContainer>?)

@ -0,0 +1,111 @@
package org.tasks.tasklist
import android.util.SparseArray
import com.todoroo.astrid.core.SortHelper
import org.tasks.data.TaskContainer
import org.tasks.date.DateTimeUtils
import java.util.*
class SectionedDataSource constructor(tasks: List<TaskContainer>, disableHeaders: Boolean, val sortMode: Int, private val collapsed: MutableSet<Long>) {
private val tasks = tasks.toMutableList()
private val sections = if (disableHeaders) {
SparseArray()
} else {
getSections()
}
fun getItem(position: Int): TaskContainer = tasks[sectionedPositionToPosition(position)]
fun getHeaderValue(position: Int): Long = sections[position]!!.value
fun isHeader(position: Int) = sections[position] != null
private fun sectionedPositionToPosition(sectionedPosition: Int): Int {
if (isHeader(sectionedPosition)) {
return sections[sectionedPosition].firstPosition
}
var offset = 0
for (i in 0 until sections.size()) {
val section = sections.valueAt(i)
if (section.sectionedPosition > sectionedPosition) {
break
}
--offset
}
return sectionedPosition + offset
}
val taskCount: Int
get() = tasks.size
val size: Int
get() = tasks.size + sections.size()
fun getSection(position: Int): AdapterSection = sections[position]
fun add(position: Int, task: TaskContainer) = tasks.add(sectionedPositionToPosition(position), task)
fun removeAt(position: Int): TaskContainer = tasks.removeAt(sectionedPositionToPosition(position))
private fun getSections(): SparseArray<AdapterSection> {
val sections = ArrayList<AdapterSection>()
for (i in tasks.indices) {
val task = tasks[i]
val sortGroup = task.sortGroup ?: continue
val header = if (sortMode == SortHelper.SORT_IMPORTANCE || sortGroup == 0L) {
sortGroup
} else {
DateTimeUtils.newDateTime(sortGroup).startOfDay().millis
}
val isCollapsed = collapsed.contains(header)
if (i == 0) {
sections.add(AdapterSection(i, header, 0, isCollapsed))
} else {
val previous = tasks[i - 1].sortGroup
when (sortMode) {
SortHelper.SORT_IMPORTANCE -> if (header != previous) {
sections.add(AdapterSection(i, header, 0, isCollapsed))
}
else -> if (previous > 0 && header != DateTimeUtils.newDateTime(previous).startOfDay().millis) {
sections.add(AdapterSection(i, header, 0, isCollapsed))
}
}
}
}
var adjustment = 0
for (i in sections.indices) {
val section = sections[i]
section.firstPosition -= adjustment
if (section.collapsed) {
val next = sections.getOrNull(i + 1)?.firstPosition?.minus(adjustment) ?: tasks.size
tasks.subList(section.firstPosition, next).clear()
adjustment += next - section.firstPosition
}
}
return setSections(sections)
}
private fun setSections(newSections: List<AdapterSection>): SparseArray<AdapterSection> {
val sections = SparseArray<AdapterSection>()
newSections.forEachIndexed { index, section ->
section.sectionedPosition = section.firstPosition + index
sections.append(section.sectionedPosition, section)
}
return sections
}
fun moveSection(toPosition: Int, offset: Int) {
val old = sections[toPosition]
sections.remove(toPosition)
val newSectionedPosition = old.sectionedPosition + offset
val previousSection = if (isHeader(newSectionedPosition - 1)) sections[newSectionedPosition - 1] else null
val newFirstPosition = previousSection?.firstPosition ?: old.firstPosition + offset
val new = AdapterSection(newFirstPosition, old.value, newSectionedPosition, old.collapsed)
sections.append(new.sectionedPosition, new)
}
}

@ -10,22 +10,24 @@ import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.dao.TaskDao
import org.tasks.data.TaskContainer
import org.tasks.intents.TaskIntents
import org.tasks.preferences.Preferences
import org.tasks.tasklist.TaskViewHolder.ViewHolderCallbacks
abstract class TaskListRecyclerAdapter internal constructor(
private val adapter: TaskAdapter,
private val viewHolderFactory: ViewHolderFactory,
internal val viewHolderFactory: ViewHolderFactory,
private val taskList: TaskListFragment,
private val taskDao: TaskDao) : RecyclerView.Adapter<TaskViewHolder>(), ViewHolderCallbacks, ListUpdateCallback, TaskAdapterDataSource {
private val taskDao: TaskDao,
internal val preferences: Preferences)
: RecyclerView.Adapter<RecyclerView.ViewHolder>(), ViewHolderCallbacks, ListUpdateCallback, TaskAdapterDataSource {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TaskViewHolder {
return viewHolderFactory.newViewHolder(parent, this)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = viewHolderFactory.newViewHolder(parent, this)
override fun onBindViewHolder(holder: TaskViewHolder, position: Int) {
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val sortMode = preferences.sortMode
val task = getItem(position)
if (task != null) {
holder.bindView(task, taskList.getFilter())
(holder as TaskViewHolder).bindView(task, taskList.getFilter(), sortMode)
holder.isMoving = false
val indent = adapter.getIndent(task)
task.setIndent(indent)
@ -85,8 +87,6 @@ abstract class TaskListRecyclerAdapter internal constructor(
protected abstract fun dragAndDropEnabled(): Boolean
abstract override fun getItem(position: Int): TaskContainer
abstract fun submitList(list: List<TaskContainer>)
override fun onInserted(position: Int, count: Int) {

@ -1,6 +1,8 @@
package org.tasks.tasklist;
import static com.todoroo.andlib.utility.DateUtilities.getRelativeDateTime;
import static com.todoroo.andlib.utility.DateUtilities.getTimeString;
import static org.tasks.date.DateTimeUtils.newDateTime;
import android.annotation.SuppressLint;
import android.app.Activity;
@ -18,13 +20,14 @@ import butterknife.OnLongClick;
import com.google.android.material.chip.Chip;
import com.google.android.material.chip.ChipGroup;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.core.SortHelper;
import com.todoroo.astrid.service.TaskCompleter;
import com.todoroo.astrid.ui.CheckableImageView;
import java.util.List;
import java.util.Locale;
import org.tasks.R;
import org.tasks.data.TaskContainer;
import org.tasks.dialogs.Linkify;
import org.tasks.locale.Locale;
import org.tasks.preferences.Preferences;
import org.tasks.ui.CheckBoxProvider;
import org.tasks.ui.ChipProvider;
@ -42,6 +45,7 @@ public class TaskViewHolder extends RecyclerView.ViewHolder {
private final int selectedColor;
private final int rowPadding;
private final Linkify linkify;
private final Locale locale;
private final CheckBoxProvider checkBoxProvider;
private final int textColorOverdue;
private final ChipProvider chipProvider;
@ -93,7 +97,8 @@ public class TaskViewHolder extends RecyclerView.ViewHolder {
int background,
int selectedColor,
int rowPadding,
Linkify linkify) {
Linkify linkify,
Locale locale) {
super(view);
this.context = context;
this.preferences = preferences;
@ -108,6 +113,7 @@ public class TaskViewHolder extends RecyclerView.ViewHolder {
this.selectedColor = selectedColor;
this.rowPadding = rowPadding;
this.linkify = linkify;
this.locale = locale;
ButterKnife.bind(this, view);
if (preferences.getBoolean(R.string.p_fullTaskTitle, false)) {
@ -188,14 +194,14 @@ public class TaskViewHolder extends RecyclerView.ViewHolder {
return Math.round(indent * getShiftSize());
}
void bindView(TaskContainer task, Filter filter) {
void bindView(TaskContainer task, Filter filter, int sortMode) {
this.task = task;
this.indent = task.indent;
nameView.setText(task.getTitle());
hiddenIcon.setVisibility(task.isHidden() ? View.VISIBLE : View.GONE);
setupTitleAndCheckbox();
setupDueDate();
setupDueDate(sortMode);
setupChips(filter);
if (preferences.getBoolean(R.string.p_show_description, true)) {
description.setText(task.getNotes());
@ -233,16 +239,20 @@ public class TaskViewHolder extends RecyclerView.ViewHolder {
completeBox.invalidate();
}
private void setupDueDate() {
private void setupDueDate(int sortMode) {
if (task.hasDueDate()) {
if (task.isOverdue()) {
dueDate.setTextColor(textColorOverdue);
} else {
dueDate.setTextColor(textColorSecondary);
}
String dateValue =
getRelativeDateTime(
context, task.getDueDate(), Locale.getInstance().getLocale(), FormatStyle.MEDIUM);
String dateValue;
if (sortMode == SortHelper.SORT_DUE && task.sortGroup != null && newDateTime(task.sortGroup).startOfDay().equals(newDateTime(task.getDueDate()).startOfDay())) {
dateValue =
task.hasDueTime() ? getTimeString(context, newDateTime(task.getDueDate())) : null;
} else {
dateValue = getRelativeDateTime(context, task.getDueDate(), locale, FormatStyle.MEDIUM);
}
dueDate.setText(dateValue);
dueDate.setVisibility(View.VISIBLE);
} else {
@ -332,7 +342,7 @@ public class TaskViewHolder extends RecyclerView.ViewHolder {
return maxIndent;
}
interface ViewHolderCallbacks {
public interface ViewHolderCallbacks {
void onCompletedTask(TaskContainer task, boolean newState);

@ -1,79 +0,0 @@
package org.tasks.tasklist;
import static com.todoroo.andlib.utility.AndroidUtilities.convertDpToPixels;
import static org.tasks.preferences.ResourceResolver.getData;
import static org.tasks.preferences.ResourceResolver.getResourceId;
import android.app.Activity;
import android.content.Context;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import com.todoroo.astrid.service.TaskCompleter;
import javax.inject.Inject;
import org.tasks.R;
import org.tasks.dialogs.Linkify;
import org.tasks.injection.ForActivity;
import org.tasks.preferences.Preferences;
import org.tasks.ui.CheckBoxProvider;
import org.tasks.ui.ChipProvider;
public class ViewHolderFactory {
private final int textColorSecondary;
private final int textColorOverdue;
private final Context context;
private final ChipProvider chipProvider;
private final int fontSize;
private final CheckBoxProvider checkBoxProvider;
private final TaskCompleter taskCompleter;
private final DisplayMetrics metrics;
private final int background;
private final int selectedColor;
private final int rowPadding;
private final Linkify linkify;
private final Preferences preferences;
@Inject
public ViewHolderFactory(
@ForActivity Context context,
Preferences preferences,
ChipProvider chipProvider,
CheckBoxProvider checkBoxProvider,
TaskCompleter taskCompleter,
Linkify linkify) {
this.context = context;
this.chipProvider = chipProvider;
this.checkBoxProvider = checkBoxProvider;
this.taskCompleter = taskCompleter;
this.preferences = preferences;
this.linkify = linkify;
textColorSecondary = getData(context, android.R.attr.textColorSecondary);
textColorOverdue = context.getColor(R.color.overdue);
background = getResourceId(context, R.attr.selectableItemBackground);
selectedColor = getData(context, R.attr.colorControlHighlight);
fontSize = preferences.getFontSize();
metrics = context.getResources().getDisplayMetrics();
rowPadding = convertDpToPixels(metrics, preferences.getInt(R.string.p_rowPadding, 16));
}
TaskViewHolder newViewHolder(ViewGroup parent, TaskViewHolder.ViewHolderCallbacks callbacks) {
return new TaskViewHolder(
(Activity) context,
(ViewGroup)
LayoutInflater.from(context).inflate(R.layout.task_adapter_row, parent, false),
preferences,
fontSize,
chipProvider,
checkBoxProvider,
textColorOverdue,
textColorSecondary,
taskCompleter,
callbacks,
metrics,
background,
selectedColor,
rowPadding,
linkify);
}
}

@ -0,0 +1,62 @@
package org.tasks.tasklist
import android.app.Activity
import android.content.Context
import android.util.DisplayMetrics
import android.view.LayoutInflater
import android.view.ViewGroup
import com.todoroo.andlib.utility.AndroidUtilities
import com.todoroo.astrid.service.TaskCompleter
import org.tasks.R
import org.tasks.dialogs.Linkify
import org.tasks.injection.ForActivity
import org.tasks.preferences.Preferences
import org.tasks.preferences.ResourceResolver
import org.tasks.tasklist.TaskViewHolder.ViewHolderCallbacks
import org.tasks.ui.CheckBoxProvider
import org.tasks.ui.ChipProvider
import java.util.*
import javax.inject.Inject
class ViewHolderFactory @Inject constructor(
@param:ForActivity private val context: Context,
private val preferences: Preferences,
private val chipProvider: ChipProvider,
private val checkBoxProvider: CheckBoxProvider,
private val taskCompleter: TaskCompleter,
private val linkify: Linkify,
private val locale: Locale) {
private val textColorSecondary: Int = ResourceResolver.getData(context, android.R.attr.textColorSecondary)
private val textColorOverdue: Int = context.getColor(R.color.overdue)
private val fontSize: Int = preferences.fontSize
private val metrics: DisplayMetrics = context.resources.displayMetrics
private val background: Int = ResourceResolver.getResourceId(context, R.attr.selectableItemBackground)
private val selectedColor: Int = ResourceResolver.getData(context, R.attr.colorControlHighlight)
private val rowPadding: Int = AndroidUtilities.convertDpToPixels(metrics, preferences.getInt(R.string.p_rowPadding, 16))
fun newHeaderViewHolder(parent: ViewGroup?, callback: (Long) -> Unit) =
HeaderViewHolder(
context,
locale,
LayoutInflater.from(context).inflate(R.layout.task_adapter_header, parent, false),
callback)
fun newViewHolder(parent: ViewGroup?, callbacks: ViewHolderCallbacks?) =
TaskViewHolder(
context as Activity,
LayoutInflater.from(context).inflate(R.layout.task_adapter_row, parent, false) as ViewGroup,
preferences,
fontSize,
chipProvider,
checkBoxProvider,
textColorOverdue,
textColorSecondary,
taskCompleter,
callbacks,
metrics,
background,
selectedColor,
rowPadding,
linkify,
locale)
}

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/header"
style="@style/TextAppearance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:padding="@dimen/keyline_first"
android:gravity="start|center_vertical"
android:textColor="@color/text_secondary"
android:textSize="@dimen/sku_details_row_text_size"
android:drawableEnd="@drawable/ic_keyboard_arrow_down_black_18dp"
android:drawableTint="@color/text_tertiary"
app:fontFamily="sans-serif-medium" />

@ -630,4 +630,6 @@ File %1$s contained %2$s.\n\n
<string name="support_development_subscribe">Unlock additional features and support open source software</string>
<string name="no_thanks">No thanks</string>
<string name="got_it">Got it!</string>
<string name="sort_created_group">Created %s</string>
<string name="sort_modified_group">Modified %s</string>
</resources>

Loading…
Cancel
Save