Merge pull request #820 from pavelpoley/task/favorites-drag-and-drop

Allow reordering favorites by drag and drop
This commit is contained in:
Tibor Kaputa 2022-05-11 22:07:04 +02:00 committed by GitHub
commit ba31d487c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 194 additions and 20 deletions

View File

@ -63,7 +63,7 @@ android {
} }
dependencies { dependencies {
implementation 'com.github.SimpleMobileTools:Simple-Commons:b264da6cff' implementation 'com.github.SimpleMobileTools:Simple-Commons:34fdfce71c'
implementation 'com.googlecode.ez-vcard:ez-vcard:0.11.3' implementation 'com.googlecode.ez-vcard:ez-vcard:0.11.3'
implementation 'com.github.tibbi:IndicatorFastScroll:4524cd0b61' implementation 'com.github.tibbi:IndicatorFastScroll:4524cd0b61'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'

View File

@ -35,6 +35,7 @@ import com.simplemobiletools.contacts.pro.dialogs.ImportContactsDialog
import com.simplemobiletools.contacts.pro.extensions.config import com.simplemobiletools.contacts.pro.extensions.config
import com.simplemobiletools.contacts.pro.extensions.getTempFile import com.simplemobiletools.contacts.pro.extensions.getTempFile
import com.simplemobiletools.contacts.pro.extensions.handleGenericContactClick import com.simplemobiletools.contacts.pro.extensions.handleGenericContactClick
import com.simplemobiletools.contacts.pro.fragments.FavoritesFragment
import com.simplemobiletools.contacts.pro.fragments.MyViewPagerFragment import com.simplemobiletools.contacts.pro.fragments.MyViewPagerFragment
import com.simplemobiletools.contacts.pro.helpers.ALL_TABS_MASK import com.simplemobiletools.contacts.pro.helpers.ALL_TABS_MASK
import com.simplemobiletools.contacts.pro.helpers.ContactsHelper import com.simplemobiletools.contacts.pro.helpers.ContactsHelper
@ -185,8 +186,9 @@ class MainActivity : SimpleActivity(), RefreshContactsListener {
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.sort -> showSortingDialog() R.id.sort -> showSortingDialog(showCustomSorting = getCurrentFragment() is FavoritesFragment)
R.id.filter -> showFilterDialog() R.id.filter -> showFilterDialog()
R.id.dialpad -> launchDialpad() R.id.dialpad -> launchDialpad()
R.id.import_contacts -> tryImportContacts() R.id.import_contacts -> tryImportContacts()
@ -410,8 +412,8 @@ class MainActivity : SimpleActivity(), RefreshContactsListener {
} }
} }
private fun showSortingDialog() { private fun showSortingDialog(showCustomSorting: Boolean) {
ChangeSortingDialog(this) { ChangeSortingDialog(this, showCustomSorting) {
refreshContacts(TAB_CONTACTS or TAB_FAVORITES) refreshContacts(TAB_CONTACTS or TAB_FAVORITES)
} }
} }

View File

@ -10,11 +10,14 @@ import android.graphics.drawable.Icon
import android.graphics.drawable.LayerDrawable import android.graphics.drawable.LayerDrawable
import android.util.TypedValue import android.util.TypedValue
import android.view.Menu import android.view.Menu
import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
@ -25,6 +28,9 @@ import com.simplemobiletools.commons.dialogs.ConfirmationDialog
import com.simplemobiletools.commons.dialogs.RadioGroupDialog import com.simplemobiletools.commons.dialogs.RadioGroupDialog
import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.* import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.commons.interfaces.ItemMoveCallback
import com.simplemobiletools.commons.interfaces.ItemTouchHelperContract
import com.simplemobiletools.commons.interfaces.StartReorderDragListener
import com.simplemobiletools.commons.models.RadioItem import com.simplemobiletools.commons.models.RadioItem
import com.simplemobiletools.commons.views.MyRecyclerView import com.simplemobiletools.commons.views.MyRecyclerView
import com.simplemobiletools.contacts.pro.R import com.simplemobiletools.contacts.pro.R
@ -36,12 +42,19 @@ import com.simplemobiletools.contacts.pro.helpers.*
import com.simplemobiletools.contacts.pro.interfaces.RefreshContactsListener import com.simplemobiletools.contacts.pro.interfaces.RefreshContactsListener
import com.simplemobiletools.contacts.pro.interfaces.RemoveFromGroupListener import com.simplemobiletools.contacts.pro.interfaces.RemoveFromGroupListener
import com.simplemobiletools.contacts.pro.models.Contact import com.simplemobiletools.contacts.pro.models.Contact
import java.util.*
class ContactsAdapter( class ContactsAdapter(
activity: SimpleActivity, var contactItems: ArrayList<Contact>, private val refreshListener: RefreshContactsListener?, activity: SimpleActivity,
private val location: Int, private val removeListener: RemoveFromGroupListener?, recyclerView: MyRecyclerView, var contactItems: ArrayList<Contact>,
highlightText: String = "", itemClick: (Any) -> Unit private val refreshListener: RefreshContactsListener?,
) : MyRecyclerViewAdapter(activity, recyclerView, itemClick), RecyclerViewFastScroller.OnPopupTextUpdate { private val location: Int,
private val removeListener: RemoveFromGroupListener?,
recyclerView: MyRecyclerView,
highlightText: String = "",
private val enableDrag: Boolean = false,
itemClick: (Any) -> Unit
) : MyRecyclerViewAdapter(activity, recyclerView, itemClick), RecyclerViewFastScroller.OnPopupTextUpdate, ItemTouchHelperContract {
private val NEW_GROUP_ID = -1 private val NEW_GROUP_ID = -1
private var config = activity.config private var config = activity.config
@ -54,8 +67,23 @@ class ContactsAdapter(
private val itemLayout = if (showPhoneNumbers) R.layout.item_contact_with_number else R.layout.item_contact_without_number private val itemLayout = if (showPhoneNumbers) R.layout.item_contact_with_number else R.layout.item_contact_without_number
private var touchHelper: ItemTouchHelper? = null
private var startReorderDragListener: StartReorderDragListener? = null
var onDragEndListener: (() -> Unit)? = null
init { init {
setupDragListener(true) setupDragListener(true)
if (enableDrag) {
touchHelper = ItemTouchHelper(ItemMoveCallback(this))
touchHelper!!.attachToRecyclerView(recyclerView)
startReorderDragListener = object : StartReorderDragListener {
override fun requestDrag(viewHolder: RecyclerView.ViewHolder) {
touchHelper?.startDrag(viewHolder)
}
}
}
} }
override fun getActionMenuId() = R.menu.cab override fun getActionMenuId() = R.menu.cab
@ -107,9 +135,13 @@ class ContactsAdapter(
override fun getItemKeyPosition(key: Int) = contactItems.indexOfFirst { it.id == key } override fun getItemKeyPosition(key: Int) = contactItems.indexOfFirst { it.id == key }
override fun onActionModeCreated() {} override fun onActionModeCreated() {
notifyDataSetChanged()
}
override fun onActionModeDestroyed() {} override fun onActionModeDestroyed() {
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = createViewHolder(itemLayout, parent) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = createViewHolder(itemLayout, parent)
@ -117,7 +149,7 @@ class ContactsAdapter(
val contact = contactItems[position] val contact = contactItems[position]
val allowLongClick = location != LOCATION_INSERT_OR_EDIT val allowLongClick = location != LOCATION_INSERT_OR_EDIT
holder.bindView(contact, true, allowLongClick) { itemView, layoutPosition -> holder.bindView(contact, true, allowLongClick) { itemView, layoutPosition ->
setupView(itemView, contact) setupView(itemView, contact, holder)
} }
bindViewHolder(holder) bindViewHolder(holder)
} }
@ -335,7 +367,7 @@ class ContactsAdapter(
} }
} }
private fun setupView(view: View, contact: Contact) { private fun setupView(view: View, contact: Contact, holder: ViewHolder) {
view.apply { view.apply {
findViewById<FrameLayout>(R.id.item_contact_frame)?.isSelected = selectedKeys.contains(contact.id) findViewById<FrameLayout>(R.id.item_contact_frame)?.isSelected = selectedKeys.contains(contact.id)
val fullName = contact.getNameToDisplay() val fullName = contact.getNameToDisplay()
@ -393,8 +425,49 @@ class ContactsAdapter(
.into(findViewById(R.id.item_contact_image)) .into(findViewById(R.id.item_contact_image))
} }
} }
val dragIcon = findViewById<ImageView>(R.id.drag_handle_icon)
if (enableDrag && textToHighlight.isEmpty()) {
dragIcon.apply {
beVisibleIf(selectedKeys.isNotEmpty())
applyColorFilter(textColor)
setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_DOWN) {
startReorderDragListener?.requestDrag(holder)
}
false
}
}
} else {
dragIcon.apply {
beGone()
setOnTouchListener(null)
}
}
} }
} }
override fun onChange(position: Int) = contactItems.getOrNull(position)?.getBubbleText() ?: "" override fun onChange(position: Int) = contactItems.getOrNull(position)?.getBubbleText() ?: ""
override fun onRowMoved(fromPosition: Int, toPosition: Int) {
activity.config.isCustomOrderSelected = true
if (fromPosition < toPosition) {
for (i in fromPosition until toPosition) {
Collections.swap(contactItems, i, i + 1)
}
} else {
for (i in fromPosition downTo toPosition + 1) {
Collections.swap(contactItems, i, i - 1)
}
}
notifyItemMoved(fromPosition, toPosition)
}
override fun onRowSelected(myViewHolder: ViewHolder?) { }
override fun onRowClear(myViewHolder: ViewHolder?) {
onDragEndListener?.invoke()
}
} }

View File

@ -2,13 +2,14 @@ package com.simplemobiletools.contacts.pro.dialogs
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.activities.BaseSimpleActivity import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.beGoneIf
import com.simplemobiletools.commons.extensions.setupDialogStuff import com.simplemobiletools.commons.extensions.setupDialogStuff
import com.simplemobiletools.commons.helpers.* import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.contacts.pro.R import com.simplemobiletools.contacts.pro.R
import com.simplemobiletools.contacts.pro.extensions.config import com.simplemobiletools.contacts.pro.extensions.config
import kotlinx.android.synthetic.main.dialog_change_sorting.view.* import kotlinx.android.synthetic.main.dialog_change_sorting.view.*
class ChangeSortingDialog(val activity: BaseSimpleActivity, private val callback: () -> Unit) { class ChangeSortingDialog(val activity: BaseSimpleActivity, private val showCustomSorting: Boolean = false, private val callback: () -> Unit) {
private var currSorting = 0 private var currSorting = 0
private var config = activity.config private var config = activity.config
private var view = activity.layoutInflater.inflate(R.layout.dialog_change_sorting, null) private var view = activity.layoutInflater.inflate(R.layout.dialog_change_sorting, null)
@ -21,21 +22,39 @@ class ChangeSortingDialog(val activity: BaseSimpleActivity, private val callback
activity.setupDialogStuff(view, this, R.string.sort_by) activity.setupDialogStuff(view, this, R.string.sort_by)
} }
currSorting = config.sorting currSorting = if (showCustomSorting && config.isCustomOrderSelected) {
SORT_BY_CUSTOM
} else {
config.sorting
}
setupSortRadio() setupSortRadio()
setupOrderRadio() setupOrderRadio()
} }
private fun setupSortRadio() { private fun setupSortRadio() {
val sortingRadio = view.sorting_dialog_radio_sorting val sortingRadio = view.sorting_dialog_radio_sorting
sortingRadio.setOnCheckedChangeListener { group, checkedId ->
val isCustomSorting = checkedId == sortingRadio.sorting_dialog_radio_custom.id
view.sorting_dialog_radio_order.beGoneIf(isCustomSorting)
view.divider.beGoneIf(isCustomSorting)
}
val sortBtn = when { val sortBtn = when {
currSorting and SORT_BY_FIRST_NAME != 0 -> sortingRadio.sorting_dialog_radio_first_name currSorting and SORT_BY_FIRST_NAME != 0 -> sortingRadio.sorting_dialog_radio_first_name
currSorting and SORT_BY_MIDDLE_NAME != 0 -> sortingRadio.sorting_dialog_radio_middle_name currSorting and SORT_BY_MIDDLE_NAME != 0 -> sortingRadio.sorting_dialog_radio_middle_name
currSorting and SORT_BY_SURNAME != 0 -> sortingRadio.sorting_dialog_radio_surname currSorting and SORT_BY_SURNAME != 0 -> sortingRadio.sorting_dialog_radio_surname
currSorting and SORT_BY_FULL_NAME != 0 -> sortingRadio.sorting_dialog_radio_full_name currSorting and SORT_BY_FULL_NAME != 0 -> sortingRadio.sorting_dialog_radio_full_name
currSorting and SORT_BY_CUSTOM != 0 -> sortingRadio.sorting_dialog_radio_custom
else -> sortingRadio.sorting_dialog_radio_date_created else -> sortingRadio.sorting_dialog_radio_date_created
} }
sortBtn.isChecked = true sortBtn.isChecked = true
if (showCustomSorting) {
sortingRadio.sorting_dialog_radio_custom.isChecked = config.isCustomOrderSelected
}
view.sorting_dialog_radio_custom.beGoneIf(!showCustomSorting)
} }
private fun setupOrderRadio() { private fun setupOrderRadio() {
@ -55,14 +74,25 @@ class ChangeSortingDialog(val activity: BaseSimpleActivity, private val callback
R.id.sorting_dialog_radio_middle_name -> SORT_BY_MIDDLE_NAME R.id.sorting_dialog_radio_middle_name -> SORT_BY_MIDDLE_NAME
R.id.sorting_dialog_radio_surname -> SORT_BY_SURNAME R.id.sorting_dialog_radio_surname -> SORT_BY_SURNAME
R.id.sorting_dialog_radio_full_name -> SORT_BY_FULL_NAME R.id.sorting_dialog_radio_full_name -> SORT_BY_FULL_NAME
R.id.sorting_dialog_radio_custom -> SORT_BY_CUSTOM
else -> SORT_BY_DATE_CREATED else -> SORT_BY_DATE_CREATED
} }
if (view.sorting_dialog_radio_order.checkedRadioButtonId == R.id.sorting_dialog_radio_descending) { if (sorting != SORT_BY_CUSTOM && view.sorting_dialog_radio_order.checkedRadioButtonId == R.id.sorting_dialog_radio_descending) {
sorting = sorting or SORT_DESCENDING sorting = sorting or SORT_DESCENDING
} }
if (showCustomSorting) {
if (sorting == SORT_BY_CUSTOM) {
config.isCustomOrderSelected = true
} else {
config.isCustomOrderSelected = false
config.sorting = sorting config.sorting = sorting
}
} else {
config.sorting = sorting
}
callback() callback()
} }
} }

View File

@ -5,6 +5,7 @@ import android.content.Intent
import android.util.AttributeSet import android.util.AttributeSet
import android.view.ViewGroup import android.view.ViewGroup
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.gson.Gson
import com.reddit.indicatorfastscroll.FastScrollItemIndicator import com.reddit.indicatorfastscroll.FastScrollItemIndicator
import com.simplemobiletools.commons.adapters.MyRecyclerViewAdapter import com.simplemobiletools.commons.adapters.MyRecyclerViewAdapter
import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.extensions.*
@ -30,7 +31,6 @@ import kotlinx.android.synthetic.main.fragment_layout.view.fragment_placeholder_
import kotlinx.android.synthetic.main.fragment_layout.view.fragment_wrapper import kotlinx.android.synthetic.main.fragment_layout.view.fragment_wrapper
import kotlinx.android.synthetic.main.fragment_letters_layout.view.* import kotlinx.android.synthetic.main.fragment_letters_layout.view.*
import java.util.* import java.util.*
import kotlin.collections.ArrayList
abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet) : CoordinatorLayout(context, attributeSet) { abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet) : CoordinatorLayout(context, attributeSet) {
protected var activity: SimpleActivity? = null protected var activity: SimpleActivity? = null
@ -125,7 +125,15 @@ abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet)
val filtered = when { val filtered = when {
this is GroupsFragment -> contacts this is GroupsFragment -> contacts
this is FavoritesFragment -> contacts.filter { it.starred == 1 } as ArrayList<Contact> this is FavoritesFragment -> {
val favorites = contacts.filter { it.starred == 1 } as ArrayList<Contact>
if (activity!!.config.isCustomOrderSelected) {
sortByCustomOrder(favorites)
} else {
favorites
}
}
else -> { else -> {
val contactSources = activity!!.getVisibleContactSources() val contactSources = activity!!.getVisibleContactSources()
contacts.filter { contactSources.contains(it.source) } as ArrayList<Contact> contacts.filter { contactSources.contains(it.source) } as ArrayList<Contact>
@ -153,6 +161,20 @@ abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet)
} }
} }
private fun sortByCustomOrder(starred: List<Contact>): ArrayList<Contact> {
val favoritesOrder = activity!!.config.favoritesContactsOrder
if (favoritesOrder.isEmpty()) {
return ArrayList(starred)
}
val orderList = Converters().jsonToStringList(favoritesOrder)
val map = orderList.withIndex().associate { it.value to it.index }
val sorted = starred.sortedBy { map[it.id.toString()] }
return ArrayList(sorted)
}
private fun setupContacts(contacts: ArrayList<Contact>) { private fun setupContacts(contacts: ArrayList<Contact>) {
if (this is GroupsFragment) { if (this is GroupsFragment) {
setupGroupsAdapter(contacts) { setupGroupsAdapter(contacts) {
@ -219,10 +241,29 @@ abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet)
else -> LOCATION_CONTACTS_TAB else -> LOCATION_CONTACTS_TAB
} }
ContactsAdapter(activity as SimpleActivity, contacts, activity as RefreshContactsListener, location, null, fragment_list) { val enableDragReorder = this is FavoritesFragment
ContactsAdapter(
activity = activity as SimpleActivity,
contactItems = contacts,
refreshListener = activity as RefreshContactsListener,
location = location,
removeListener = null,
recyclerView = fragment_list,
enableDrag = enableDragReorder,
) {
(activity as RefreshContactsListener).contactClicked(it as Contact) (activity as RefreshContactsListener).contactClicked(it as Contact)
}.apply { }.apply {
fragment_list.adapter = this fragment_list.adapter = this
if (enableDragReorder) {
onDragEndListener = {
val adapter = fragment_list?.adapter
if (adapter is ContactsAdapter) {
val items = adapter.contactItems
saveCustomOrderToPrefs(items)
setupLetterFastscroller(items)
}
}
}
} }
if (context.areSystemAnimationsEnabled) { if (context.areSystemAnimationsEnabled) {
@ -238,6 +279,14 @@ abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet)
} }
} }
private fun saveCustomOrderToPrefs(items: ArrayList<Contact>) {
activity?.apply {
val orderIds = items.map { it.id }
val orderGsonString = Gson().toJson(orderIds)
config.favoritesContactsOrder = orderGsonString
}
}
fun showContactThumbnailsChanged(showThumbnails: Boolean) { fun showContactThumbnailsChanged(showThumbnails: Boolean) {
if (this is GroupsFragment) { if (this is GroupsFragment) {
(fragment_list.adapter as? GroupsAdapter)?.apply { (fragment_list.adapter as? GroupsAdapter)?.apply {

View File

@ -66,4 +66,12 @@ class Config(context: Context) : BaseConfig(context) {
var mergeDuplicateContacts: Boolean var mergeDuplicateContacts: Boolean
get() = prefs.getBoolean(MERGE_DUPLICATE_CONTACTS, true) get() = prefs.getBoolean(MERGE_DUPLICATE_CONTACTS, true)
set(mergeDuplicateContacts) = prefs.edit().putBoolean(MERGE_DUPLICATE_CONTACTS, mergeDuplicateContacts).apply() set(mergeDuplicateContacts) = prefs.edit().putBoolean(MERGE_DUPLICATE_CONTACTS, mergeDuplicateContacts).apply()
var favoritesContactsOrder: String
get() = prefs.getString(FAVORITES_CONTACTS_ORDER, "")!!
set(order) = prefs.edit().putString(FAVORITES_CONTACTS_ORDER, order).apply()
var isCustomOrderSelected: Boolean
get() = prefs.getBoolean(FAVORITES_CUSTOM_ORDER_SELECTED, false)
set(selected) = prefs.edit().putBoolean(FAVORITES_CUSTOM_ORDER_SELECTED, selected).apply()
} }

View File

@ -22,6 +22,8 @@ const val LAST_EXPORT_PATH = "last_export_path"
const val WAS_LOCAL_ACCOUNT_INITIALIZED = "was_local_account_initialized" const val WAS_LOCAL_ACCOUNT_INITIALIZED = "was_local_account_initialized"
const val SHOW_PRIVATE_CONTACTS = "show_private_contacts" const val SHOW_PRIVATE_CONTACTS = "show_private_contacts"
const val MERGE_DUPLICATE_CONTACTS = "merge_duplicate_contacts" const val MERGE_DUPLICATE_CONTACTS = "merge_duplicate_contacts"
const val FAVORITES_CONTACTS_ORDER = "favorites_contacts_order"
const val FAVORITES_CUSTOM_ORDER_SELECTED = "favorites_custom_order_selected"
const val SMT_PRIVATE = "smt_private" // used at the contact source of local contacts hidden from other apps const val SMT_PRIVATE = "smt_private" // used at the contact source of local contacts hidden from other apps
const val GROUP = "group" const val GROUP = "group"

View File

@ -59,9 +59,19 @@
android:paddingBottom="@dimen/medium_margin" android:paddingBottom="@dimen/medium_margin"
android:text="@string/date_created" /> android:text="@string/date_created" />
<com.simplemobiletools.commons.views.MyCompatRadioButton
android:id="@+id/sorting_dialog_radio_custom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/medium_margin"
android:paddingBottom="@dimen/medium_margin"
android:text="@string/custom" />
</RadioGroup> </RadioGroup>
<include layout="@layout/divider" /> <include
android:id="@+id/divider"
layout="@layout/divider" />
<RadioGroup <RadioGroup
android:id="@+id/sorting_dialog_radio_order" android:id="@+id/sorting_dialog_radio_order"