add swipe for contact picker and contacts page
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 3s
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 3s
This commit is contained in:
@@ -1,26 +1,28 @@
|
||||
package sh.sar.basedbank.ui.home
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Bundle
|
||||
import android.util.Base64
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.PopupMenu
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import sh.sar.basedbank.BasedBankApp
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.api.mib.MibContactsClient
|
||||
import android.widget.PopupMenu
|
||||
import sh.sar.basedbank.databinding.SheetContactPickerBinding
|
||||
import sh.sar.basedbank.util.RecentsCache
|
||||
|
||||
@@ -30,106 +32,148 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
||||
private val binding get() = _binding!!
|
||||
private val viewModel: HomeViewModel by activityViewModels()
|
||||
|
||||
private lateinit var adapter: ContactPickerAdapter
|
||||
// null = All, RECENTS_TAG = Recents, else = category id
|
||||
private var activeCategoryId: String? = RECENTS_TAG
|
||||
private val pendingHashes = mutableSetOf<String>()
|
||||
private val profileImageHashes = mutableSetOf<String>()
|
||||
private val app get() = requireActivity().application as BasedBankApp
|
||||
private val session get() = app.mibSession
|
||||
|
||||
private var fromAccountNumber: String = ""
|
||||
private var mediator: TabLayoutMediator? = null
|
||||
private lateinit var pagerAdapter: PickerPagerAdapter
|
||||
|
||||
private data class TabDef(val tag: String?, val label: String)
|
||||
|
||||
private inner class PickerPagerAdapter(val pages: List<TabDef>) :
|
||||
RecyclerView.Adapter<PickerPagerAdapter.PageHolder>() {
|
||||
|
||||
val pageAdapters: List<ContactPickerAdapter> = pages.mapIndexed { i, page ->
|
||||
ContactPickerAdapter(
|
||||
onItemClick = { accountNumber, label -> handlePickerSelection(accountNumber, label) },
|
||||
onSameAsFrom = {},
|
||||
onImageNeeded = { hash -> fetchImage(hash) },
|
||||
onItemLongClick = { accountNumber, anchor ->
|
||||
if (page.tag == RECENTS_TAG) {
|
||||
val menu = PopupMenu(requireContext(), anchor)
|
||||
menu.menu.add(getString(R.string.recents_remove))
|
||||
menu.setOnMenuItemClickListener {
|
||||
RecentsCache.remove(requireContext(), accountNumber)
|
||||
rebuildPage(i)
|
||||
true
|
||||
}
|
||||
menu.show()
|
||||
true
|
||||
} else false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun rebuildAll() = pages.indices.forEach { rebuildPage(it) }
|
||||
|
||||
fun rebuildPage(position: Int) {
|
||||
pageAdapters[position].submitList(buildPageItems(pages[position].tag))
|
||||
}
|
||||
|
||||
fun updateImage(hash: String, bitmap: Bitmap) =
|
||||
pageAdapters.forEach { it.updateImage(hash, bitmap) }
|
||||
|
||||
inner class PageHolder(val rv: RecyclerView) : RecyclerView.ViewHolder(rv)
|
||||
|
||||
override fun getItemCount() = pages.size
|
||||
override fun getItemViewType(position: Int) = position
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PageHolder {
|
||||
val rv = RecyclerView(parent.context).apply {
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
clipToPadding = false
|
||||
val p24 = (24 * resources.displayMetrics.density).toInt()
|
||||
setPadding(0, 0, 0, p24)
|
||||
adapter = pageAdapters[viewType]
|
||||
}
|
||||
return PageHolder(rv)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: PageHolder, position: Int) {
|
||||
rebuildPage(position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = SheetContactPickerBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val selectedAccountNumber = arguments?.getString(ARG_FROM_ACCOUNT) ?: ""
|
||||
fromAccountNumber = arguments?.getString(ARG_FROM_ACCOUNT) ?: ""
|
||||
|
||||
adapter = ContactPickerAdapter(
|
||||
onItemClick = { accountNumber, label ->
|
||||
val contacts = viewModel.contacts.value ?: emptyList()
|
||||
val accounts = viewModel.accounts.value ?: emptyList()
|
||||
val contact = contacts.firstOrNull { it.benefAccount == accountNumber }
|
||||
val account = accounts.firstOrNull { it.accountNumber == accountNumber }
|
||||
val bundle = bundleOf(KEY_ACCOUNT_NUMBER to accountNumber, KEY_LABEL to label)
|
||||
when {
|
||||
account?.profileType == "BML_PREPAID" -> {
|
||||
bundle.putBoolean(KEY_SKIP_LOOKUP, true)
|
||||
bundle.putString(KEY_SUBTITLE, "${account.accountNumber} · ${account.currencyName} ${account.availableBalance}")
|
||||
bundle.putString(KEY_COLOR, "#FE860E")
|
||||
}
|
||||
contact != null && !contact.transferCyDesc.equals("MVR", ignoreCase = true) -> {
|
||||
bundle.putBoolean(KEY_SKIP_LOOKUP, true)
|
||||
bundle.putString(KEY_SUBTITLE, "${contact.benefBankName} · ${contact.benefAccount}")
|
||||
bundle.putString(KEY_COLOR, contact.bankColor)
|
||||
contact.customerImgHash?.let { bundle.putString(KEY_IMAGE_HASH, it) }
|
||||
}
|
||||
}
|
||||
setFragmentResult(REQUEST_KEY, bundle)
|
||||
dismiss()
|
||||
},
|
||||
onSameAsFrom = {},
|
||||
onImageNeeded = { hash -> fetchImage(hash) },
|
||||
onItemLongClick = { accountNumber, anchor ->
|
||||
if (activeCategoryId == RECENTS_TAG) {
|
||||
val menu = PopupMenu(requireContext(), anchor)
|
||||
menu.menu.add(getString(R.string.recents_remove))
|
||||
menu.setOnMenuItemClickListener {
|
||||
RecentsCache.remove(requireContext(), accountNumber)
|
||||
rebuildList(selectedAccountNumber)
|
||||
true
|
||||
}
|
||||
menu.show()
|
||||
true
|
||||
} else false
|
||||
}
|
||||
val initialPages = listOf(
|
||||
TabDef(RECENTS_TAG, getString(R.string.contacts_tab_recents)),
|
||||
TabDef(MY_ACCOUNTS_TAG, getString(R.string.transfer_my_accounts)),
|
||||
TabDef(null, getString(R.string.contacts_tab_all))
|
||||
)
|
||||
pagerAdapter = PickerPagerAdapter(initialPages)
|
||||
binding.viewPager.adapter = pagerAdapter
|
||||
binding.viewPager.offscreenPageLimit = 1
|
||||
attachMediator(initialPages)
|
||||
|
||||
binding.sheetRecyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.sheetRecyclerView.adapter = adapter
|
||||
|
||||
binding.etSheetSearch.addTextChangedListener { rebuildList(selectedAccountNumber) }
|
||||
|
||||
// Tabs: Recents | My Accounts | All | <categories...>
|
||||
binding.sheetCategoryTabs.addTab(
|
||||
binding.sheetCategoryTabs.newTab().setText(R.string.contacts_tab_recents).apply { tag = RECENTS_TAG }
|
||||
)
|
||||
binding.sheetCategoryTabs.addTab(
|
||||
binding.sheetCategoryTabs.newTab().setText(R.string.transfer_my_accounts).apply { tag = MY_ACCOUNTS_TAG }
|
||||
)
|
||||
binding.sheetCategoryTabs.addTab(
|
||||
binding.sheetCategoryTabs.newTab().setText(R.string.contacts_tab_all).apply { tag = null }
|
||||
)
|
||||
binding.sheetCategoryTabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||
override fun onTabSelected(tab: TabLayout.Tab) {
|
||||
activeCategoryId = tab.tag as? String
|
||||
rebuildList(selectedAccountNumber)
|
||||
}
|
||||
override fun onTabUnselected(tab: TabLayout.Tab) {}
|
||||
override fun onTabReselected(tab: TabLayout.Tab) {}
|
||||
})
|
||||
binding.etSheetSearch.addTextChangedListener { pagerAdapter.rebuildAll() }
|
||||
|
||||
viewModel.contactCategories.observe(viewLifecycleOwner) { cats ->
|
||||
// Remove all tabs after "All" (index 2) and re-add categories
|
||||
while (binding.sheetCategoryTabs.tabCount > 3) binding.sheetCategoryTabs.removeTabAt(3)
|
||||
for (cat in cats) {
|
||||
binding.sheetCategoryTabs.addTab(
|
||||
binding.sheetCategoryTabs.newTab().setText(cat.categoryName).apply { tag = cat.id }
|
||||
)
|
||||
val pages = buildList {
|
||||
add(TabDef(RECENTS_TAG, getString(R.string.contacts_tab_recents)))
|
||||
add(TabDef(MY_ACCOUNTS_TAG, getString(R.string.transfer_my_accounts)))
|
||||
add(TabDef(null, getString(R.string.contacts_tab_all)))
|
||||
cats.forEach { add(TabDef(it.id, it.categoryName)) }
|
||||
}
|
||||
rebuildList(selectedAccountNumber)
|
||||
val savedPosition = binding.viewPager.currentItem
|
||||
pagerAdapter = PickerPagerAdapter(pages)
|
||||
binding.viewPager.adapter = pagerAdapter
|
||||
attachMediator(pages)
|
||||
binding.viewPager.setCurrentItem(savedPosition.coerceIn(0, pages.size - 1), false)
|
||||
}
|
||||
|
||||
viewModel.contacts.observe(viewLifecycleOwner) { rebuildList(selectedAccountNumber) }
|
||||
viewModel.accounts.observe(viewLifecycleOwner) { rebuildList(selectedAccountNumber) }
|
||||
viewModel.contacts.observe(viewLifecycleOwner) { pagerAdapter.rebuildAll() }
|
||||
viewModel.accounts.observe(viewLifecycleOwner) { pagerAdapter.rebuildAll() }
|
||||
}
|
||||
|
||||
private fun rebuildList(fromNumber: String) {
|
||||
private fun attachMediator(pages: List<TabDef>) {
|
||||
mediator?.detach()
|
||||
mediator = TabLayoutMediator(binding.sheetCategoryTabs, binding.viewPager) { tab, position ->
|
||||
tab.text = pages[position].label
|
||||
}.also { it.attach() }
|
||||
}
|
||||
|
||||
private fun handlePickerSelection(accountNumber: String, label: String) {
|
||||
val contacts = viewModel.contacts.value ?: emptyList()
|
||||
val accounts = viewModel.accounts.value ?: emptyList()
|
||||
val contact = contacts.firstOrNull { it.benefAccount == accountNumber }
|
||||
val account = accounts.firstOrNull { it.accountNumber == accountNumber }
|
||||
val bundle = bundleOf(KEY_ACCOUNT_NUMBER to accountNumber, KEY_LABEL to label)
|
||||
when {
|
||||
account?.profileType == "BML_PREPAID" -> {
|
||||
bundle.putBoolean(KEY_SKIP_LOOKUP, true)
|
||||
bundle.putString(KEY_SUBTITLE, "${account.accountNumber} · ${account.currencyName} ${account.availableBalance}")
|
||||
bundle.putString(KEY_COLOR, "#FE860E")
|
||||
}
|
||||
contact != null && !contact.transferCyDesc.equals("MVR", ignoreCase = true) -> {
|
||||
bundle.putBoolean(KEY_SKIP_LOOKUP, true)
|
||||
bundle.putString(KEY_SUBTITLE, "${contact.benefBankName} · ${contact.benefAccount}")
|
||||
bundle.putString(KEY_COLOR, contact.bankColor)
|
||||
contact.customerImgHash?.let { bundle.putString(KEY_IMAGE_HASH, it) }
|
||||
}
|
||||
}
|
||||
setFragmentResult(REQUEST_KEY, bundle)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
private fun buildPageItems(tabTag: String?): List<ContactPickerAdapter.PickerItem> {
|
||||
val search = binding.etSheetSearch.text?.toString()?.trim() ?: ""
|
||||
val items = mutableListOf<ContactPickerAdapter.PickerItem>()
|
||||
|
||||
if (activeCategoryId == RECENTS_TAG) {
|
||||
if (tabTag == RECENTS_TAG) {
|
||||
val recents = RecentsCache.load(requireContext())
|
||||
val filtered = if (search.isBlank()) recents else recents.filter {
|
||||
it.displayName.contains(search, ignoreCase = true) || it.accountNumber.contains(search)
|
||||
@@ -138,25 +182,24 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
||||
if (r.isProfileImage && r.imageHash != null) profileImageHashes.add(r.imageHash)
|
||||
items.add(ContactPickerAdapter.PickerItem.Row(
|
||||
accountNumber = r.accountNumber,
|
||||
displayName = r.displayName,
|
||||
subtitle = r.subtitle,
|
||||
colorHex = r.colorHex,
|
||||
isSameAsFrom = r.accountNumber == fromNumber,
|
||||
imageHash = r.imageHash
|
||||
displayName = r.displayName,
|
||||
subtitle = r.subtitle,
|
||||
colorHex = r.colorHex,
|
||||
isSameAsFrom = r.accountNumber == fromAccountNumber,
|
||||
imageHash = r.imageHash
|
||||
))
|
||||
}
|
||||
adapter.submitList(items)
|
||||
return
|
||||
return items
|
||||
}
|
||||
|
||||
val accounts = viewModel.accounts.value ?: emptyList()
|
||||
val contacts = viewModel.contacts.value ?: emptyList()
|
||||
val fromAccount = accounts.find { it.accountNumber == fromNumber }
|
||||
val fromAccount = accounts.find { it.accountNumber == fromAccountNumber }
|
||||
val fromCurrency = fromAccount?.currencyName ?: ""
|
||||
val fromLoginTag = fromAccount?.loginTag ?: ""
|
||||
val fromIsCard = fromAccount?.profileType == "BML_PREPAID"
|
||||
|
||||
if (activeCategoryId == MY_ACCOUNTS_TAG) {
|
||||
if (tabTag == MY_ACCOUNTS_TAG) {
|
||||
val regularAccounts = accounts.filter { it.profileType != "BML_PREPAID" }
|
||||
val cards = accounts.filter { it.profileType == "BML_PREPAID" }
|
||||
|
||||
@@ -167,14 +210,14 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
||||
items.add(ContactPickerAdapter.PickerItem.Header(getString(R.string.accounts)))
|
||||
for (acc in filteredRegular) {
|
||||
if (acc.profileImageHash != null) profileImageHashes.add(acc.profileImageHash)
|
||||
val isSame = acc.accountNumber == fromNumber
|
||||
val isSame = acc.accountNumber == fromAccountNumber
|
||||
items.add(ContactPickerAdapter.PickerItem.Row(
|
||||
accountNumber = acc.accountNumber,
|
||||
displayName = acc.accountBriefName,
|
||||
subtitle = "${acc.accountNumber} · ${acc.currencyName} ${acc.availableBalance}",
|
||||
colorHex = "#FE860E",
|
||||
isSameAsFrom = isSame,
|
||||
imageHash = acc.profileImageHash,
|
||||
displayName = acc.accountBriefName,
|
||||
subtitle = "${acc.accountNumber} · ${acc.currencyName} ${acc.availableBalance}",
|
||||
colorHex = "#FE860E",
|
||||
isSameAsFrom = isSame,
|
||||
imageHash = acc.profileImageHash,
|
||||
inactiveReason = if (isSame) null
|
||||
else if (fromIsCard && acc.loginTag != fromLoginTag) "Cards can only be used within the same BML account"
|
||||
else currencyMismatchReason(fromCurrency, acc.currencyName)
|
||||
@@ -189,15 +232,15 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
||||
items.add(ContactPickerAdapter.PickerItem.Header(getString(R.string.cards)))
|
||||
for (acc in filteredCards) {
|
||||
if (acc.profileImageHash != null) profileImageHashes.add(acc.profileImageHash)
|
||||
val isSame = acc.accountNumber == fromNumber
|
||||
val isSame = acc.accountNumber == fromAccountNumber
|
||||
val isActive = acc.statusDesc.equals("Active", ignoreCase = true)
|
||||
items.add(ContactPickerAdapter.PickerItem.Row(
|
||||
accountNumber = acc.accountNumber,
|
||||
displayName = acc.accountBriefName,
|
||||
subtitle = "${acc.accountNumber} · ${acc.currencyName} ${acc.availableBalance}",
|
||||
colorHex = "#FE860E",
|
||||
isSameAsFrom = isSame,
|
||||
imageHash = acc.profileImageHash,
|
||||
displayName = acc.accountBriefName,
|
||||
subtitle = "${acc.accountNumber} · ${acc.currencyName} ${acc.availableBalance}",
|
||||
colorHex = "#FE860E",
|
||||
isSameAsFrom = isSame,
|
||||
imageHash = acc.profileImageHash,
|
||||
inactiveReason = if (isSame) null
|
||||
else if (!isActive) acc.statusDesc
|
||||
else if (acc.loginTag != fromLoginTag) "Cards can only be used within the same BML account"
|
||||
@@ -205,35 +248,29 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
adapter.submitList(items)
|
||||
return
|
||||
return items
|
||||
}
|
||||
|
||||
val filteredContacts = contacts.filter { contact ->
|
||||
val matchesCat = activeCategoryId == null || contact.benefCategoryId == activeCategoryId
|
||||
val matchesCat = tabTag == null || contact.benefCategoryId == tabTag
|
||||
val matchesSearch = search.isBlank() ||
|
||||
contact.benefNickName.contains(search, ignoreCase = true) ||
|
||||
contact.benefName.contains(search, ignoreCase = true) ||
|
||||
contact.benefAccount.contains(search)
|
||||
matchesCat && matchesSearch
|
||||
}
|
||||
|
||||
if (filteredContacts.isNotEmpty()) {
|
||||
for (contact in filteredContacts) {
|
||||
items.add(ContactPickerAdapter.PickerItem.Row(
|
||||
accountNumber = contact.benefAccount,
|
||||
displayName = contact.benefNickName,
|
||||
subtitle = "${contact.benefBankName} · ${contact.benefAccount}",
|
||||
colorHex = contact.bankColor,
|
||||
isSameAsFrom = contact.benefAccount == fromNumber,
|
||||
imageHash = contact.customerImgHash,
|
||||
inactiveReason = currencyMismatchReason(fromCurrency, contact.transferCyDesc)
|
||||
))
|
||||
}
|
||||
for (contact in filteredContacts) {
|
||||
items.add(ContactPickerAdapter.PickerItem.Row(
|
||||
accountNumber = contact.benefAccount,
|
||||
displayName = contact.benefNickName,
|
||||
subtitle = "${contact.benefBankName} · ${contact.benefAccount}",
|
||||
colorHex = contact.bankColor,
|
||||
isSameAsFrom = contact.benefAccount == fromAccountNumber,
|
||||
imageHash = contact.customerImgHash,
|
||||
inactiveReason = currencyMismatchReason(fromCurrency, contact.transferCyDesc)
|
||||
))
|
||||
}
|
||||
|
||||
adapter.submitList(items)
|
||||
return items
|
||||
}
|
||||
|
||||
private fun fetchImage(hash: String) {
|
||||
@@ -249,7 +286,7 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
||||
val bytes = Base64.decode(base64, Base64.DEFAULT)
|
||||
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?: return@launch
|
||||
withContext(Dispatchers.Main) {
|
||||
adapter.updateImage(hash, bitmap)
|
||||
pagerAdapter.updateImage(hash, bitmap)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
pendingHashes.remove(hash)
|
||||
|
||||
@@ -120,6 +120,8 @@ class ContactsAdapter(
|
||||
)
|
||||
}
|
||||
|
||||
binding.tvRealName.text = contact.benefName
|
||||
|
||||
val vis = if (expanded) View.VISIBLE else View.GONE
|
||||
binding.dividerExpand.visibility = vis
|
||||
binding.expandedSection.visibility = vis
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package sh.sar.basedbank.ui.home
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Bundle
|
||||
import android.util.Base64
|
||||
@@ -13,7 +14,8 @@ import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -31,14 +33,65 @@ class ContactsFragment : Fragment() {
|
||||
private var _binding: FragmentContactsBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private val viewModel: HomeViewModel by activityViewModels()
|
||||
private lateinit var adapter: ContactsAdapter
|
||||
|
||||
private val pendingHashes = mutableSetOf<String>()
|
||||
private val app get() = requireActivity().application as BasedBankApp
|
||||
private val session get() = app.mibSession
|
||||
|
||||
private var categories: List<MibBeneficiaryCategory> = emptyList()
|
||||
private var activeCategoryId: String? = null
|
||||
private var allContacts: List<MibBeneficiary> = emptyList()
|
||||
private var currentSearch: String = ""
|
||||
private var mediator: TabLayoutMediator? = null
|
||||
private lateinit var pagerAdapter: ContactsPagerAdapter
|
||||
|
||||
private data class TabPage(val categoryId: String?, val label: String)
|
||||
|
||||
private inner class ContactsPagerAdapter(val pages: List<TabPage>) :
|
||||
RecyclerView.Adapter<ContactsPagerAdapter.PageHolder>() {
|
||||
|
||||
private val density get() = resources.displayMetrics.density
|
||||
val contactAdapters: List<ContactsAdapter> = pages.map { page ->
|
||||
ContactsAdapter(
|
||||
onImageNeeded = { hash -> fetchImage(hash) },
|
||||
onDeleteClick = { contact -> confirmDelete(contact) },
|
||||
onTransferClick = { contact -> openTransfer(contact) }
|
||||
).also { a ->
|
||||
a.setFilter(page.categoryId, currentSearch)
|
||||
a.updateContacts(allContacts)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateContacts(contacts: List<MibBeneficiary>) =
|
||||
contactAdapters.forEach { it.updateContacts(contacts) }
|
||||
|
||||
fun updateSearch(query: String) =
|
||||
pages.forEachIndexed { i, page -> contactAdapters[i].setFilter(page.categoryId, query) }
|
||||
|
||||
fun updateImage(hash: String, bitmap: Bitmap) =
|
||||
contactAdapters.forEach { it.updateImage(hash, bitmap) }
|
||||
|
||||
inner class PageHolder(val rv: RecyclerView) : RecyclerView.ViewHolder(rv)
|
||||
|
||||
override fun getItemCount() = pages.size
|
||||
override fun getItemViewType(position: Int) = position
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PageHolder {
|
||||
val rv = RecyclerView(parent.context).apply {
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
clipToPadding = false
|
||||
val p4 = (4 * density).toInt()
|
||||
val p80 = (80 * density).toInt()
|
||||
setPadding(0, p4, 0, p80)
|
||||
adapter = contactAdapters[viewType]
|
||||
}
|
||||
return PageHolder(rv)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: PageHolder, position: Int) {}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = FragmentContactsBinding.inflate(inflater, container, false)
|
||||
@@ -46,74 +99,61 @@ class ContactsFragment : Fragment() {
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
adapter = ContactsAdapter(
|
||||
onImageNeeded = { hash -> fetchImage(hash) },
|
||||
onDeleteClick = { contact -> confirmDelete(contact) },
|
||||
onTransferClick = { contact ->
|
||||
val fragment = TransferFragment.newInstance(
|
||||
accountNumber = contact.benefAccount,
|
||||
displayName = contact.benefNickName,
|
||||
subtitle = "${contact.benefBankName} · ${contact.benefAccount}",
|
||||
colorHex = contact.bankColor,
|
||||
imageHash = contact.customerImgHash
|
||||
)
|
||||
(requireActivity() as HomeActivity).showWithBackStack(fragment)
|
||||
}
|
||||
)
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.recyclerView.adapter = adapter
|
||||
pagerAdapter = ContactsPagerAdapter(listOf(TabPage(null, getString(R.string.contacts_tab_all))))
|
||||
binding.viewPager.adapter = pagerAdapter
|
||||
binding.viewPager.offscreenPageLimit = 1
|
||||
|
||||
attachMediator(pagerAdapter.pages)
|
||||
|
||||
binding.etSearch.addTextChangedListener { text ->
|
||||
adapter.setFilter(activeCategoryId, text?.toString() ?: "")
|
||||
currentSearch = text?.toString() ?: ""
|
||||
pagerAdapter.updateSearch(currentSearch)
|
||||
}
|
||||
|
||||
binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||
override fun onTabSelected(tab: TabLayout.Tab) {
|
||||
activeCategoryId = tab.tag as? String
|
||||
adapter.setFilter(activeCategoryId, binding.etSearch.text?.toString() ?: "")
|
||||
}
|
||||
override fun onTabUnselected(tab: TabLayout.Tab) {}
|
||||
override fun onTabReselected(tab: TabLayout.Tab) {}
|
||||
})
|
||||
|
||||
binding.fabAddContact.setOnClickListener {
|
||||
AddContactSheetFragment().show(childFragmentManager, "add_contact")
|
||||
}
|
||||
|
||||
viewModel.contactCategories.observe(viewLifecycleOwner) { cats ->
|
||||
categories = cats
|
||||
rebuildTabs(cats)
|
||||
rebuildPager(cats)
|
||||
}
|
||||
|
||||
viewModel.contacts.observe(viewLifecycleOwner) { contacts ->
|
||||
adapter.updateContacts(contacts)
|
||||
binding.recyclerView.visibility = if (contacts.isEmpty()) View.GONE else View.VISIBLE
|
||||
allContacts = contacts
|
||||
pagerAdapter.updateContacts(contacts)
|
||||
binding.emptyView.visibility = if (contacts.isEmpty()) View.VISIBLE else View.GONE
|
||||
binding.loadingView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun rebuildTabs(cats: List<MibBeneficiaryCategory>) {
|
||||
binding.tabLayout.clearOnTabSelectedListeners()
|
||||
binding.tabLayout.removeAllTabs()
|
||||
private fun attachMediator(pages: List<TabPage>) {
|
||||
mediator?.detach()
|
||||
mediator = TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position ->
|
||||
tab.text = pages[position].label
|
||||
}.also { it.attach() }
|
||||
}
|
||||
|
||||
binding.tabLayout.addTab(
|
||||
binding.tabLayout.newTab().setText(R.string.contacts_tab_all).apply { tag = null }
|
||||
)
|
||||
for (cat in cats) {
|
||||
binding.tabLayout.addTab(
|
||||
binding.tabLayout.newTab().setText(cat.categoryName).apply { tag = cat.id }
|
||||
)
|
||||
private fun rebuildPager(cats: List<MibBeneficiaryCategory>) {
|
||||
val pages = buildList {
|
||||
add(TabPage(null, getString(R.string.contacts_tab_all)))
|
||||
cats.forEach { add(TabPage(it.id, it.categoryName)) }
|
||||
}
|
||||
val savedPosition = binding.viewPager.currentItem
|
||||
pagerAdapter = ContactsPagerAdapter(pages)
|
||||
binding.viewPager.adapter = pagerAdapter
|
||||
attachMediator(pages)
|
||||
binding.viewPager.setCurrentItem(savedPosition.coerceIn(0, pages.size - 1), false)
|
||||
}
|
||||
|
||||
binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||
override fun onTabSelected(tab: TabLayout.Tab) {
|
||||
activeCategoryId = tab.tag as? String
|
||||
adapter.setFilter(activeCategoryId, binding.etSearch.text?.toString() ?: "")
|
||||
}
|
||||
override fun onTabUnselected(tab: TabLayout.Tab) {}
|
||||
override fun onTabReselected(tab: TabLayout.Tab) {}
|
||||
})
|
||||
private fun openTransfer(contact: MibBeneficiary) {
|
||||
val fragment = TransferFragment.newInstance(
|
||||
accountNumber = contact.benefAccount,
|
||||
displayName = contact.benefNickName,
|
||||
subtitle = "${contact.benefBankName} · ${contact.benefAccount}",
|
||||
colorHex = contact.bankColor,
|
||||
imageHash = contact.customerImgHash
|
||||
)
|
||||
(requireActivity() as HomeActivity).showWithBackStack(fragment)
|
||||
}
|
||||
|
||||
private fun confirmDelete(contact: MibBeneficiary) {
|
||||
@@ -179,7 +219,7 @@ class ContactsFragment : Fragment() {
|
||||
val base64 = client.fetchProfileImageBase64(sess, hash) ?: return@launch
|
||||
val bytes = Base64.decode(base64, Base64.DEFAULT)
|
||||
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?: return@launch
|
||||
withContext(Dispatchers.Main) { adapter.updateImage(hash, bitmap) }
|
||||
withContext(Dispatchers.Main) { pagerAdapter.updateImage(hash, bitmap) }
|
||||
} catch (_: Exception) {
|
||||
pendingHashes.remove(hash)
|
||||
}
|
||||
|
||||
@@ -46,13 +46,10 @@
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/viewPager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:paddingVertical="4dp"
|
||||
android:paddingBottom="80dp" />
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/loadingView"
|
||||
|
||||
@@ -87,6 +87,14 @@
|
||||
android:paddingBottom="8dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvRealName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?attr/textAppearanceLabelMedium"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:paddingBottom="8dp" />
|
||||
|
||||
<!-- Action buttons -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -37,11 +37,9 @@
|
||||
app:tabMode="scrollable"
|
||||
app:tabGravity="start" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/sheetRecyclerView"
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/viewPager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="400dp"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="24dp" />
|
||||
android:layout_height="400dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
Reference in New Issue
Block a user