From 42a115e93bc5d06884f9be15b2a8ade5eb89545c Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Sat, 16 Sep 2023 10:30:25 +0200 Subject: [PATCH] Improved RecyclerViewHeaderDecoration to make it sticky --- .../ContactsAndSuggestionsListAdapter.kt | 12 +++- .../main/calls/fragment/StartCallFragment.kt | 2 +- .../utils/RecyclerViewHeaderDecoration.kt | 68 ++++++++++++++++++- .../main/res/layout/call_start_fragment.xml | 20 +----- .../call_suggestion_list_decoration.xml | 28 +++++--- 5 files changed, 98 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/calls/adapter/ContactsAndSuggestionsListAdapter.kt b/app/src/main/java/org/linphone/ui/main/calls/adapter/ContactsAndSuggestionsListAdapter.kt index 4e6332d85..ce749adff 100644 --- a/app/src/main/java/org/linphone/ui/main/calls/adapter/ContactsAndSuggestionsListAdapter.kt +++ b/app/src/main/java/org/linphone/ui/main/calls/adapter/ContactsAndSuggestionsListAdapter.kt @@ -13,8 +13,10 @@ import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import org.linphone.R import org.linphone.databinding.CallSuggestionListCellBinding +import org.linphone.databinding.CallSuggestionListDecorationBinding import org.linphone.databinding.ContactListCellBinding import org.linphone.ui.main.calls.model.ContactOrSuggestionModel +import org.linphone.utils.AppUtils import org.linphone.utils.Event import org.linphone.utils.HeaderAdapter @@ -43,12 +45,18 @@ class ContactsAndSuggestionsListAdapter( } val previousModel = getItem(position - 1) return previousModel.friend != null - } + } else if (position == 0) return true return false } override fun getHeaderViewForPosition(context: Context, position: Int): View { - return LayoutInflater.from(context).inflate(R.layout.call_suggestion_list_decoration, null) + val binding = CallSuggestionListDecorationBinding.inflate(LayoutInflater.from(context)) + binding.header.text = if (position == 0) { + AppUtils.getString(R.string.call_start_contacts_list_title) + } else { + AppUtils.getString(R.string.call_start_suggestions_list_title) + } + return binding.root } override fun getItemViewType(position: Int): Int { diff --git a/app/src/main/java/org/linphone/ui/main/calls/fragment/StartCallFragment.kt b/app/src/main/java/org/linphone/ui/main/calls/fragment/StartCallFragment.kt index 27463e7b4..66c31ba51 100644 --- a/app/src/main/java/org/linphone/ui/main/calls/fragment/StartCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/calls/fragment/StartCallFragment.kt @@ -108,7 +108,7 @@ class StartCallFragment : GenericFragment() { binding.contactsAndSuggestionsList.setHasFixedSize(true) binding.contactsAndSuggestionsList.adapter = adapter - val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), adapter) + val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), adapter, true) binding.contactsAndSuggestionsList.addItemDecoration(headerItemDecoration) adapter.contactClickedEvent.observe(viewLifecycleOwner) { diff --git a/app/src/main/java/org/linphone/utils/RecyclerViewHeaderDecoration.kt b/app/src/main/java/org/linphone/utils/RecyclerViewHeaderDecoration.kt index 4b598a4e7..1f98a4879 100644 --- a/app/src/main/java/org/linphone/utils/RecyclerViewHeaderDecoration.kt +++ b/app/src/main/java/org/linphone/utils/RecyclerViewHeaderDecoration.kt @@ -27,7 +27,11 @@ import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -class RecyclerViewHeaderDecoration(private val context: Context, private val adapter: HeaderAdapter) : RecyclerView.ItemDecoration() { +class RecyclerViewHeaderDecoration( + private val context: Context, + private val adapter: HeaderAdapter, + private val sticky: Boolean = false +) : RecyclerView.ItemDecoration() { private val headers: SparseArray = SparseArray() override fun getItemOffsets( @@ -74,8 +78,12 @@ class RecyclerViewHeaderDecoration(private val context: Context, private val ada } override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { - for (i in 0 until parent.childCount) { + if (sticky) return + + // Used to display the moving item decoration + for (i in 0 until parent.childCount) { // Only returns visible children val child = parent.getChildAt(i) + // Maps the visible view position to the item index in the adapter val position = parent.getChildAdapterPosition(child) if (position != RecyclerView.NO_POSITION && adapter.displayHeaderForPosition(position)) { canvas.save() @@ -89,6 +97,62 @@ class RecyclerViewHeaderDecoration(private val context: Context, private val ada } } } + + override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { + if (!sticky) return + + var latestPositionHeaderFound = -1 + var nextHeaderTopPosition = -1f + + for (index in parent.childCount downTo 0) { + val child = parent.getChildAt(index) + val position = parent.getChildAdapterPosition(child) + if (position != RecyclerView.NO_POSITION && adapter.displayHeaderForPosition(position)) { + canvas.save() + val headerView: View = headers.get(position) ?: adapter.getHeaderViewForPosition( + context, + position + ) + + val top = child.y - headerView.height + if (top >= 0) { // don't move the first header + canvas.translate(0f, top) + } + + headerView.draw(canvas) + canvas.restore() + + latestPositionHeaderFound = position + nextHeaderTopPosition = child.y + } + } + + // Makes sure at least one header is displayed + if (latestPositionHeaderFound > 0 || latestPositionHeaderFound == -1) { + // Display first item header at top + val topVisibleChild = parent.getChildAt(0) + val topVisibleChildPosition = parent.getChildAdapterPosition(topVisibleChild) + for (position in topVisibleChildPosition downTo 0) { + if (adapter.displayHeaderForPosition(position)) { + canvas.save() + val headerView: View = headers.get(position) ?: adapter.getHeaderViewForPosition( + context, + position + ) + + // Do not translate it as we want it sticky to the top unless in contact with next header + if (nextHeaderTopPosition > 0 && nextHeaderTopPosition <= (headerView.height * 2)) { + val top = nextHeaderTopPosition - (headerView.height * 2) + canvas.translate(0f, top) + } + + headerView.draw(canvas) + canvas.restore() + break + } + } + } + } } interface HeaderAdapter { diff --git a/app/src/main/res/layout/call_start_fragment.xml b/app/src/main/res/layout/call_start_fragment.xml index bf4263a20..6a1e7a89c 100644 --- a/app/src/main/res/layout/call_start_fragment.xml +++ b/app/src/main/res/layout/call_start_fragment.xml @@ -185,24 +185,6 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/no_contacts_nor_suggestion_image" /> - - diff --git a/app/src/main/res/layout/call_suggestion_list_decoration.xml b/app/src/main/res/layout/call_suggestion_list_decoration.xml index 5998a43fe..56ee6a603 100644 --- a/app/src/main/res/layout/call_suggestion_list_decoration.xml +++ b/app/src/main/res/layout/call_suggestion_list_decoration.xml @@ -1,9 +1,21 @@ - \ No newline at end of file + + + + + + + + + \ No newline at end of file