From f85f3661448b41b782f468ec8268a46359dd8c0a Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Tue, 7 Jun 2022 19:14:54 -0600 Subject: [PATCH] ui: change adapter animations Rework the submitList animation to be less resource intensive and nicer looking (at the cost of scroll positioning) notifyDatasetChanged is slow and has no animation, but list diffing is chaotic and basically useless outside of search. However, clearing the adapter and then populating it with new items seems to work quite well, albeit with the scroll position being lost sadly. Switch to that. --- CHANGELOG.md | 1 + .../auxio/music/excluded/ExcludedDialog.kt | 31 +++++++++---------- .../org/oxycblt/auxio/ui/RecyclerFramework.kt | 29 +++++++++++++++-- 3 files changed, 42 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6004e65a4..aa2fd9356 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ #### What's New - Folders on external drives can now be excluded on Android Q+ [#134] +- Added toggle for edge-to-edge mode [#149] #### What's Improved - Genre parsing now handles multiple integer values and cover/remix indicators (May wipe playback state) diff --git a/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDialog.kt index a9442273c..c9a543230 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDialog.kt @@ -86,17 +86,19 @@ class ExcludedDialog : } } - binding.excludedRecycler.adapter = excludedAdapter - - if (savedInstanceState != null) { - val pendingDirs = savedInstanceState.getStringArrayList(KEY_PENDING_DIRS) - if (pendingDirs != null) { - updateDirectories(pendingDirs.mapNotNull(ExcludedDirectory::fromString)) - return - } + binding.excludedRecycler.apply { + adapter = excludedAdapter + itemAnimator = null } - updateDirectories(settingsManager.excludedDirs) + val dirs = + savedInstanceState + ?.getStringArrayList(KEY_PENDING_DIRS) + ?.mapNotNull(ExcludedDirectory::fromString) + ?: settingsManager.excludedDirs + + excludedAdapter.data.submitList(dirs) + requireBinding().excludedEmpty.isVisible = dirs.isEmpty() } override fun onSaveInstanceState(outState: Bundle) { @@ -111,7 +113,8 @@ class ExcludedDialog : } override fun onRemoveDirectory(dir: ExcludedDirectory) { - updateDirectories(excludedAdapter.data.currentList.toMutableList().also { it.remove(dir) }) + excludedAdapter.data.remove(dir) + requireBinding().excludedEmpty.isVisible = excludedAdapter.data.currentList.isEmpty() } private fun addDocTreePath(uri: Uri?) { @@ -123,7 +126,8 @@ class ExcludedDialog : val dir = parseExcludedUri(uri) if (dir != null) { - updateDirectories(excludedAdapter.data.currentList.toMutableList().also { it.add(dir) }) + excludedAdapter.data.add(dir) + requireBinding().excludedEmpty.isVisible = false } else { requireContext().showToast(R.string.err_bad_dir) } @@ -142,11 +146,6 @@ class ExcludedDialog : return ExcludedDirectory.fromString(treeUri) } - private fun updateDirectories(dirs: List) { - excludedAdapter.data.submitList(dirs) - requireBinding().excludedEmpty.isVisible = dirs.isEmpty() - } - private fun saveAndRestart() { settingsManager.excludedDirs = excludedAdapter.data.currentList diff --git a/app/src/main/java/org/oxycblt/auxio/ui/RecyclerFramework.kt b/app/src/main/java/org/oxycblt/auxio/ui/RecyclerFramework.kt index 315802d87..fbfe17c4c 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/RecyclerFramework.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/RecyclerFramework.kt @@ -20,6 +20,7 @@ package org.oxycblt.auxio.ui import android.content.Context import android.view.View import android.view.ViewGroup +import android.widget.Adapter import androidx.annotation.StringRes import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.DiffUtil @@ -188,6 +189,7 @@ abstract class BackingData { */ class PrimitiveBackingData(private val adapter: RecyclerView.Adapter<*>) : BackingData() { private var _currentList = mutableListOf() + /** The current list backing this adapter. */ val currentList: List get() = _currentList @@ -196,13 +198,34 @@ class PrimitiveBackingData(private val adapter: RecyclerView.Adapter<*>) : Ba override fun getItemCount(): Int = _currentList.size /** - * Update the list with a [newList]. This calls [RecyclerView.Adapter.notifyDataSetChanged] - * internally, which is inefficient but also the most reliable update callback. + * Update the list with a [newList]. This does a basic animation that removes all items and + * replaces them with the new ones. */ @Suppress("NotifyDatasetChanged") fun submitList(newList: List) { + if (_currentList.isNotEmpty()) { + val oldListSize = _currentList.size + _currentList.clear() + adapter.notifyItemRangeRemoved(0, oldListSize) + } + _currentList = newList.toMutableList() - adapter.notifyDataSetChanged() + adapter.notifyItemRangeInserted(0, _currentList.size) + } + + /** Add an item to the list. */ + fun add(item: T) { + _currentList.add(item) + adapter.notifyItemInserted(_currentList.lastIndex) + } + + /** Remove an item from the list. */ + fun remove(item: T) { + val idx = _currentList.indexOf(item) + if (idx != -1) { + _currentList.removeAt(idx) + adapter.notifyItemRemoved(idx) + } } }