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 {
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.github.tibbi:IndicatorFastScroll:4524cd0b61'
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.getTempFile
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.helpers.ALL_TABS_MASK
import com.simplemobiletools.contacts.pro.helpers.ContactsHelper
@ -185,8 +186,9 @@ class MainActivity : SimpleActivity(), RefreshContactsListener {
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.sort -> showSortingDialog()
R.id.sort -> showSortingDialog(showCustomSorting = getCurrentFragment() is FavoritesFragment)
R.id.filter -> showFilterDialog()
R.id.dialpad -> launchDialpad()
R.id.import_contacts -> tryImportContacts()
@ -410,8 +412,8 @@ class MainActivity : SimpleActivity(), RefreshContactsListener {
}
}
private fun showSortingDialog() {
ChangeSortingDialog(this) {
private fun showSortingDialog(showCustomSorting: Boolean) {
ChangeSortingDialog(this, showCustomSorting) {
refreshContacts(TAB_CONTACTS or TAB_FAVORITES)
}
}

View File

@ -10,11 +10,14 @@ import android.graphics.drawable.Icon
import android.graphics.drawable.LayerDrawable
import android.util.TypedValue
import android.view.Menu
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
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.extensions.*
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.views.MyRecyclerView
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.RemoveFromGroupListener
import com.simplemobiletools.contacts.pro.models.Contact
import java.util.*
class ContactsAdapter(
activity: SimpleActivity, var contactItems: ArrayList<Contact>, private val refreshListener: RefreshContactsListener?,
private val location: Int, private val removeListener: RemoveFromGroupListener?, recyclerView: MyRecyclerView,
highlightText: String = "", itemClick: (Any) -> Unit
) : MyRecyclerViewAdapter(activity, recyclerView, itemClick), RecyclerViewFastScroller.OnPopupTextUpdate {
activity: SimpleActivity,
var contactItems: ArrayList<Contact>,
private val refreshListener: RefreshContactsListener?,
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 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 var touchHelper: ItemTouchHelper? = null
private var startReorderDragListener: StartReorderDragListener? = null
var onDragEndListener: (() -> Unit)? = null
init {
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
@ -107,9 +135,13 @@ class ContactsAdapter(
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)
@ -117,7 +149,7 @@ class ContactsAdapter(
val contact = contactItems[position]
val allowLongClick = location != LOCATION_INSERT_OR_EDIT
holder.bindView(contact, true, allowLongClick) { itemView, layoutPosition ->
setupView(itemView, contact)
setupView(itemView, contact, 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 {
findViewById<FrameLayout>(R.id.item_contact_frame)?.isSelected = selectedKeys.contains(contact.id)
val fullName = contact.getNameToDisplay()
@ -393,8 +425,49 @@ class ContactsAdapter(
.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 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 com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.beGoneIf
import com.simplemobiletools.commons.extensions.setupDialogStuff
import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.contacts.pro.R
import com.simplemobiletools.contacts.pro.extensions.config
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 config = activity.config
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)
}
currSorting = config.sorting
currSorting = if (showCustomSorting && config.isCustomOrderSelected) {
SORT_BY_CUSTOM
} else {
config.sorting
}
setupSortRadio()
setupOrderRadio()
}
private fun setupSortRadio() {
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 {
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_SURNAME != 0 -> sortingRadio.sorting_dialog_radio_surname
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
}
sortBtn.isChecked = true
if (showCustomSorting) {
sortingRadio.sorting_dialog_radio_custom.isChecked = config.isCustomOrderSelected
}
view.sorting_dialog_radio_custom.beGoneIf(!showCustomSorting)
}
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_surname -> SORT_BY_SURNAME
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
}
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
}
config.sorting = sorting
if (showCustomSorting) {
if (sorting == SORT_BY_CUSTOM) {
config.isCustomOrderSelected = true
} else {
config.isCustomOrderSelected = false
config.sorting = sorting
}
} else {
config.sorting = sorting
}
callback()
}
}

View File

@ -5,6 +5,7 @@ import android.content.Intent
import android.util.AttributeSet
import android.view.ViewGroup
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.gson.Gson
import com.reddit.indicatorfastscroll.FastScrollItemIndicator
import com.simplemobiletools.commons.adapters.MyRecyclerViewAdapter
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_letters_layout.view.*
import java.util.*
import kotlin.collections.ArrayList
abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet) : CoordinatorLayout(context, attributeSet) {
protected var activity: SimpleActivity? = null
@ -125,7 +125,15 @@ abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet)
val filtered = when {
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 -> {
val contactSources = activity!!.getVisibleContactSources()
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>) {
if (this is GroupsFragment) {
setupGroupsAdapter(contacts) {
@ -219,10 +241,29 @@ abstract class MyViewPagerFragment(context: Context, attributeSet: AttributeSet)
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)
}.apply {
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) {
@ -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) {
if (this is GroupsFragment) {
(fragment_list.adapter as? GroupsAdapter)?.apply {

View File

@ -66,4 +66,12 @@ class Config(context: Context) : BaseConfig(context) {
var mergeDuplicateContacts: Boolean
get() = prefs.getBoolean(MERGE_DUPLICATE_CONTACTS, true)
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 SHOW_PRIVATE_CONTACTS = "show_private_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 GROUP = "group"

View File

@ -59,9 +59,19 @@
android:paddingBottom="@dimen/medium_margin"
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>
<include layout="@layout/divider" />
<include
android:id="@+id/divider"
layout="@layout/divider" />
<RadioGroup
android:id="@+id/sorting_dialog_radio_order"