add swipe for contact picker and contacts page
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 3s

This commit is contained in:
2026-05-15 03:54:16 +05:00
parent 97ab31301e
commit deedd16ba2
6 changed files with 265 additions and 183 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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"

View File

@@ -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"

View File

@@ -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>