Allow reordering accounts in settings

This commit is contained in:
Poldi 2021-03-02 16:42:27 +11:00 committed by cketti
parent a00d69cf13
commit 3d9e9d96fc
10 changed files with 235 additions and 68 deletions

View file

@ -511,30 +511,19 @@ class AccountPreferenceSerializer(
} while (gotOne)
}
fun move(editor: StorageEditor, account: Account, storage: Storage, moveUp: Boolean) {
val uuids = storage.getString("accountUuids", "").split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val newUuids = arrayOfNulls<String>(uuids.size)
if (moveUp) {
for (i in uuids.indices) {
if (i > 0 && uuids[i] == account.uuid) {
newUuids[i] = newUuids[i - 1]
newUuids[i - 1] = account.uuid
} else {
newUuids[i] = uuids[i]
}
fun move(editor: StorageEditor, account: Account, storage: Storage, newPosition: Int) {
val accountUuids = storage.getString("accountUuids", "").split(",").filter { it.isNotEmpty() }
val oldPosition = accountUuids.indexOf(account.uuid)
if (oldPosition == -1 || oldPosition == newPosition) return
val newAccountUuidsString = accountUuids.toMutableList()
.apply {
removeAt(oldPosition)
add(newPosition, account.uuid)
}
} else {
for (i in uuids.indices.reversed()) {
if (i < uuids.size - 1 && uuids[i] == account.uuid) {
newUuids[i] = newUuids[i + 1]
newUuids[i + 1] = account.uuid
} else {
newUuids[i] = uuids[i]
}
}
}
val accountUuids = Utility.combine(newUuids, ',')
editor.putString("accountUuids", accountUuids)
.joinToString(separator = ",")
editor.putString("accountUuids", newAccountUuidsString)
}
private fun <T : Enum<T>> getEnumStringPref(storage: Storage, key: String, defaultEnum: T): T {

View file

@ -215,10 +215,10 @@ class Preferences internal constructor(
return newAccountNumber
}
fun move(account: Account, up: Boolean) {
fun move(account: Account, newPosition: Int) {
synchronized(accountLock) {
val storageEditor = createStorageEditor()
accountPreferenceSerializer.move(storageEditor, account, storage, up)
accountPreferenceSerializer.move(storageEditor, account, storage, newPosition)
storageEditor.commit()
loadAccounts()

View file

@ -39,7 +39,9 @@ dependencies {
implementation 'com.mikepenz:materialdrawer-iconics:8.3.3'
implementation 'com.mikepenz:fontawesome-typeface:5.9.0.0-kotlin@aar'
implementation 'com.github.ByteHamster:SearchPreference:v2.0.0'
implementation 'com.mikepenz:fastadapter:5.3.4'
implementation 'com.mikepenz:fastadapter:5.4.0-b01'
implementation 'com.mikepenz:fastadapter-extensions-drag:5.4.0-b01'
implementation 'com.mikepenz:fastadapter-extensions-utils:5.4.0-b01'
implementation 'de.hdodenhof:circleimageview:3.1.0'
implementation "commons-io:commons-io:${versions.commonsIo}"

View file

@ -1,24 +1,32 @@
package com.fsck.k9.ui.settings
import android.view.MotionEvent
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.fsck.k9.Account
import com.fsck.k9.ui.R
import com.mikepenz.fastadapter.FastAdapter
import com.mikepenz.fastadapter.drag.IDraggable
import com.mikepenz.fastadapter.items.AbstractItem
import com.mikepenz.fastadapter.listeners.TouchEventHook
internal class AccountItem(val account: Account) : AbstractItem<AccountItem.ViewHolder>() {
internal class AccountItem(val account: Account) : AbstractItem<AccountItem.ViewHolder>(), IDraggable {
override var identifier = 200L + account.accountNumber
override val type = R.id.settings_list_account_item
override val layoutRes = R.layout.account_list_item
override var isDraggable = true
override fun getViewHolder(v: View) = ViewHolder(v)
class ViewHolder(view: View) : FastAdapter.ViewHolder<AccountItem>(view) {
val name: TextView = view.findViewById(R.id.name)
val email: TextView = view.findViewById(R.id.email)
val dragHandle: ImageView = view.findViewById(R.id.drag_handle)
override fun bindView(item: AccountItem, payloads: List<Any>) {
name.text = item.account.description
@ -31,3 +39,24 @@ internal class AccountItem(val account: Account) : AbstractItem<AccountItem.View
}
}
}
internal class DragHandleTouchEvent(val action: (position: Int) -> Unit) : TouchEventHook<AccountItem>() {
override fun onBind(viewHolder: RecyclerView.ViewHolder): View? {
return if (viewHolder is AccountItem.ViewHolder) viewHolder.dragHandle else null
}
override fun onTouch(
v: View,
event: MotionEvent,
position: Int,
fastAdapter: FastAdapter<AccountItem>,
item: AccountItem
): Boolean {
return if (event.action == MotionEvent.ACTION_DOWN) {
action(position)
true
} else {
false
}
}
}

View file

@ -12,18 +12,24 @@ import androidx.annotation.AttrRes
import androidx.annotation.IdRes
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.fsck.k9.Account
import com.fsck.k9.Preferences
import com.fsck.k9.ui.R
import com.fsck.k9.ui.observeNotNull
import com.fsck.k9.ui.settings.account.AccountSettingsActivity
import com.fsck.k9.view.DraggableFrameLayout
import com.mikepenz.fastadapter.FastAdapter
import com.mikepenz.fastadapter.GenericItem
import com.mikepenz.fastadapter.adapters.ItemAdapter
import com.mikepenz.fastadapter.drag.ItemTouchCallback
import com.mikepenz.fastadapter.drag.SimpleDragCallback
import com.mikepenz.fastadapter.utils.DragDropUtil
import org.koin.androidx.viewmodel.ext.android.viewModel
class SettingsListFragment : Fragment() {
class SettingsListFragment : Fragment(), ItemTouchCallback {
private val viewModel: SettingsViewModel by viewModel()
private lateinit var itemAdapter: ItemAdapter<GenericItem>
@ -40,18 +46,29 @@ class SettingsListFragment : Fragment() {
private fun initializeSettingsList(recyclerView: RecyclerView) {
itemAdapter = ItemAdapter()
val touchCallBack = SimpleDragCallback(this).apply {
setIsDragEnabled(true)
}
val touchHelper = ItemTouchHelper(touchCallBack)
val settingsListAdapter = FastAdapter.with(itemAdapter).apply {
setHasStableIds(true)
onClickListener = { _, _, item, _ ->
handleItemClick(item)
true
}
addEventHook(
DragHandleTouchEvent { position ->
recyclerView.findViewHolderForAdapterPosition(position)?.let { viewHolder ->
touchHelper.startDrag(viewHolder)
}
}
)
}
with(recyclerView) {
adapter = settingsListAdapter
layoutManager = LinearLayoutManager(context)
}
recyclerView.adapter = settingsListAdapter
recyclerView.layoutManager = LinearLayoutManager(context)
touchHelper.attachToRecyclerView(recyclerView)
}
private fun populateSettingsList() {
@ -175,4 +192,35 @@ class SettingsListFragment : Fragment() {
fun toList(): List<GenericItem> = settingsList
}
override fun itemTouchStartDrag(viewHolder: RecyclerView.ViewHolder) {
(viewHolder.itemView as DraggableFrameLayout).isDragged = true
}
override fun itemTouchStopDrag(viewHolder: RecyclerView.ViewHolder) {
(viewHolder.itemView as DraggableFrameLayout).isDragged = false
}
override fun itemTouchOnMove(oldPosition: Int, newPosition: Int): Boolean {
val firstDropPosition = itemAdapter.adapterItems.indexOfFirst { it is AccountItem }
val lastDropPosition = itemAdapter.adapterItems.indexOfLast { it is AccountItem }
return if (newPosition in firstDropPosition..lastDropPosition) {
DragDropUtil.onMove(itemAdapter, oldPosition, newPosition)
true
} else {
false
}
}
override fun itemTouchDropped(oldPosition: Int, newPosition: Int) {
if (oldPosition == newPosition) return
val account = (itemAdapter.getAdapterItem(newPosition) as AccountItem).account
val firstAccountPosition = itemAdapter.adapterItems.indexOfFirst { it is AccountItem }
val newAccountPosition = newPosition - firstAccountPosition
val preferences = Preferences.getPreferences(requireContext())
preferences.move(account, newAccountPosition)
}
}

View file

@ -0,0 +1,33 @@
package com.fsck.k9.view
import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
import com.google.android.material.R
private val DRAGGED_STATE_SET = intArrayOf(
R.attr.state_dragged,
// When the item is dragged we also set the 'pressed' state so the item background is changed
android.R.attr.state_pressed
)
class DraggableFrameLayout : FrameLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
var isDragged: Boolean = false
set(value) {
field = value
refreshDrawableState()
invalidate()
}
override fun onCreateDrawableState(extraSpace: Int): IntArray? {
val drawableState = super.onCreateDrawableState(extraSpace + DRAGGED_STATE_SET.size)
if (isDragged) {
mergeDrawableStates(drawableState, DRAGGED_STATE_SET)
}
return drawableState
}
}

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copy the behavior of MaterialCardView -->
<selector xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item app:state_dragged="true">
<set>
<objectAnimator
android:duration="@integer/mtrl_card_anim_duration_ms"
android:interpolator="@interpolator/mtrl_fast_out_slow_in"
android:propertyName="translationZ"
android:startDelay="@integer/mtrl_card_anim_delay_ms"
android:valueTo="@dimen/mtrl_card_dragged_z"
android:valueType="floatType"
tools:ignore="UnusedAttribute" />
</set>
</item>
<item>
<set>
<objectAnimator
android:duration="@integer/mtrl_card_anim_duration_ms"
android:interpolator="@anim/mtrl_card_lowers_interpolator"
android:propertyName="translationZ"
android:valueTo="0dp"
android:valueType="floatType" />
</set>
</item>
</selector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M20,9H4v2h16V9zM4,15h16v-2H4V15z"/>
</vector>

View file

@ -1,49 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<com.fsck.k9.view.DraggableFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingLeft="8dp">
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:contentDescription="@string/account_settings_action"
android:padding="8dp"
app:srcCompat="?attr/iconSettingsAccount" />
android:background="?android:attr/windowBackground"
android:foreground="?attr/selectableItemBackground"
android:stateListAnimator="@animator/draggable_state_list_anim">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="24dp"
android:paddingRight="16dp">
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingLeft="8dp">
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:paddingBottom="4dp"
android:paddingTop="8dp"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceListItem"
tools:text="Personal" />
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:contentDescription="@string/account_settings_action"
android:padding="8dp"
app:srcCompat="?attr/iconSettingsAccount" />
<TextView
android:id="@+id/email"
android:layout_width="wrap_content"
<LinearLayout
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:ellipsize="end"
android:paddingBottom="8dp"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceSmall"
tools:text="test@example.org" />
android:orientation="vertical"
android:paddingLeft="24dp"
android:paddingRight="16dp">
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:fadeScrollbars="true"
android:paddingTop="8dp"
android:paddingBottom="4dp"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceListItem"
tools:text="Personal" />
<TextView
android:id="@+id/email"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:paddingBottom="8dp"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceSmall"
tools:text="test@example.org" />
</LinearLayout>
<ImageView
android:id="@+id/drag_handle"
android:layout_width="56dp"
android:scaleType="center"
android:layout_height="match_parent"
android:contentDescription="@null"
android:padding="16dp"
app:srcCompat="@drawable/ic_drag_handle" />
</LinearLayout>
</LinearLayout>
</com.fsck.k9.view.DraggableFrameLayout>

View file

@ -133,4 +133,9 @@
<attr name="highlightBackgroundColor" format="color|reference" />
</declare-styleable>
<declare-styleable name="DraggableFrameView">
<!-- State when a list item is being dragged. -->
<attr format="boolean" name="state_dragged"/>
</declare-styleable>
</resources>