Allow reordering accounts in settings
This commit is contained in:
parent
a00d69cf13
commit
3d9e9d96fc
10 changed files with 235 additions and 68 deletions
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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}"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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>
|
10
app/ui/legacy/src/main/res/drawable/ic_drag_handle.xml
Normal file
10
app/ui/legacy/src/main/res/drawable/ic_drag_handle.xml
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue