From 4bfa83ae147a85f361948e449ff100c00e8cd233 Mon Sep 17 00:00:00 2001 From: Brayan Oliveira <69634269+brayandso@users.noreply.github.com> Date: Fri, 15 May 2026 07:52:03 -0300 Subject: [PATCH 1/2] fix: menu actions quick drag only the initial and final elements were swapped. On a quick move, intermediate elements wouldn't get correctly moved. so now, swap each element sequentially --- .../browser/BrowserColumnSelectionAdapter.kt | 4 ++-- .../reviewer/ReviewerMenuSettingsAdapter.kt | 2 +- .../reviewer/ReviewerMenuSettingsFragment.kt | 2 +- ...ReviewerMenuSettingsTouchHelperCallback.kt | 6 ++--- .../com/ichi2/anki/utils/ext/MutableList.kt | 24 +++++++++++++++++++ 5 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 AnkiDroid/src/main/java/com/ichi2/anki/utils/ext/MutableList.kt diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/browser/BrowserColumnSelectionAdapter.kt b/AnkiDroid/src/main/java/com/ichi2/anki/browser/BrowserColumnSelectionAdapter.kt index 64fe9f9a8e4d..a04c678dcd08 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/browser/BrowserColumnSelectionAdapter.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/browser/BrowserColumnSelectionAdapter.kt @@ -28,7 +28,7 @@ import com.ichi2.anki.browser.BrowserColumnSelectionRecyclerItem.UsageItem import com.ichi2.anki.browser.ColumnUsage.AVAILABLE import com.ichi2.anki.databinding.ItemBrowserColumnsEntryBinding import com.ichi2.anki.databinding.ItemBrowserColumnsHeadingBinding -import java.util.Collections +import com.ichi2.anki.utils.ext.swapPositions class BrowserColumnSelectionAdapter( val items: MutableList, @@ -202,7 +202,7 @@ open class BrowserColumnSelectionTouchHelperCallback( // `Available` should always be the first element, so don't allow moving above it if (toPosition == 0) return false - Collections.swap(items, fromPosition, toPosition) + items.swapPositions(fromPosition, toPosition) recyclerView.adapter?.notifyItemMoved(fromPosition, toPosition) return true } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/reviewer/ReviewerMenuSettingsAdapter.kt b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/reviewer/ReviewerMenuSettingsAdapter.kt index 642867247033..0662ec118fc8 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/reviewer/ReviewerMenuSettingsAdapter.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/reviewer/ReviewerMenuSettingsAdapter.kt @@ -37,7 +37,7 @@ import com.ichi2.anki.databinding.ItemReviewerMenuDisplayTypeBinding * @see ReviewerMenuSettingsRecyclerItem */ class ReviewerMenuSettingsAdapter( - private val items: List, + private val items: MutableList, ) : RecyclerView.Adapter() { override fun onCreateViewHolder( parent: ViewGroup, diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/reviewer/ReviewerMenuSettingsFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/reviewer/ReviewerMenuSettingsFragment.kt index bd603b8d55df..ea8905c49fa1 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/reviewer/ReviewerMenuSettingsFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/reviewer/ReviewerMenuSettingsFragment.kt @@ -59,7 +59,7 @@ class ReviewerMenuSettingsFragment : listOf(ReviewerMenuSettingsRecyclerItem.DisplayType(displayType)) + menuItems.getValue(displayType).map { ReviewerMenuSettingsRecyclerItem.Action(it) } - val recyclerViewItems = MenuDisplayType.entries.flatMap { section(it) } + val recyclerViewItems = MenuDisplayType.entries.flatMap { section(it) }.toMutableList() val callback = ReviewerMenuSettingsTouchHelperCallback(recyclerViewItems) callback.setOnClearViewListener(this) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/reviewer/ReviewerMenuSettingsTouchHelperCallback.kt b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/reviewer/ReviewerMenuSettingsTouchHelperCallback.kt index ea0322e099fb..0a07e2c65bc1 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/reviewer/ReviewerMenuSettingsTouchHelperCallback.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/reviewer/ReviewerMenuSettingsTouchHelperCallback.kt @@ -17,7 +17,7 @@ package com.ichi2.anki.preferences.reviewer import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView -import java.util.Collections +import com.ichi2.anki.utils.ext.swapPositions /** * A [ItemTouchHelper.Callback] for the [ReviewerMenuSettingsAdapter]. @@ -29,7 +29,7 @@ import java.util.Collections * (see [clearView]). */ class ReviewerMenuSettingsTouchHelperCallback( - private val items: List, + private val items: MutableList, ) : ItemTouchHelper.Callback() { private val movementFlags = makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0) @@ -54,7 +54,7 @@ class ReviewerMenuSettingsTouchHelperCallback( // `Always show` should always be the first element, so don't allow moving above it if (toPosition == 0) return false - Collections.swap(items, fromPosition, toPosition) + items.swapPositions(fromPosition, toPosition) recyclerView.adapter?.notifyItemMoved(fromPosition, toPosition) return true } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/utils/ext/MutableList.kt b/AnkiDroid/src/main/java/com/ichi2/anki/utils/ext/MutableList.kt new file mode 100644 index 000000000000..60ee2b528afc --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/anki/utils/ext/MutableList.kt @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2026 Brayan Oliveira +// SPDX-License-Identifier: GPL-3.0-or-later +package com.ichi2.anki.utils.ext + +import java.util.Collections + +/** + * Moves an item within a MutableList by swapping adjacent elements. This is particularly + * optimized for [androidx.recyclerview.widget.ItemTouchHelper] drag-and-drop operations. + */ +fun MutableList.swapPositions( + fromPosition: Int, + toPosition: Int, +) { + if (fromPosition < toPosition) { + for (i in fromPosition until toPosition) { + Collections.swap(this, i, i + 1) + } + } else { + for (i in fromPosition downTo toPosition + 1) { + Collections.swap(this, i, i - 1) + } + } +} From e5ec2ef93ce7a84421bc074cdf2c1e7403994637 Mon Sep 17 00:00:00 2001 From: Brayan Oliveira <69634269+brayandso@users.noreply.github.com> Date: Fri, 15 May 2026 08:10:00 -0300 Subject: [PATCH 2/2] perf: use stable IDs in reviewer menu settings adapter the items list is fixed, so it should improve the recycler view inner performance --- .../reviewer/ReviewerMenuSettingsAdapter.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/reviewer/ReviewerMenuSettingsAdapter.kt b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/reviewer/ReviewerMenuSettingsAdapter.kt index 0662ec118fc8..b8d9dfe52dfa 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/reviewer/ReviewerMenuSettingsAdapter.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/reviewer/ReviewerMenuSettingsAdapter.kt @@ -22,6 +22,7 @@ import androidx.recyclerview.widget.RecyclerView import com.ichi2.anki.R import com.ichi2.anki.databinding.ItemReviewerMenuBinding import com.ichi2.anki.databinding.ItemReviewerMenuDisplayTypeBinding +import java.util.Objects /** * Provides bindings from menu items and display types (headings) to [RecyclerView] views @@ -39,6 +40,10 @@ import com.ichi2.anki.databinding.ItemReviewerMenuDisplayTypeBinding class ReviewerMenuSettingsAdapter( private val items: MutableList, ) : RecyclerView.Adapter() { + init { + setHasStableIds(true) + } + override fun onCreateViewHolder( parent: ViewGroup, viewType: Int, @@ -72,6 +77,11 @@ class ReviewerMenuSettingsAdapter( override fun getItemViewType(position: Int): Int = items[position].viewType + override fun getItemId(position: Int): Long { + val item = items[position] + return Objects.hash(item.viewType, item).toLong() + } + private var onDragHandleTouchedListener: ((RecyclerView.ViewHolder) -> Unit)? = null fun setOnDragHandleTouchedListener(listener: (RecyclerView.ViewHolder) -> Unit) {