Improved RecyclerViewHeaderDecoration to make it sticky

This commit is contained in:
Sylvain Berfini 2023-09-16 10:30:25 +02:00
parent 9c9391c95b
commit 42a115e93b
5 changed files with 98 additions and 32 deletions

View file

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

View file

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

View file

@ -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<View> = 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 {

View file

@ -185,24 +185,6 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/no_contacts_nor_suggestion_image" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/section_header_style"
android:id="@+id/contacts_and_suggestions_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:padding="5dp"
android:text="@string/call_start_contacts_list_title"
android:visibility="@{viewModel.contactsAndSuggestionsList.size() == 0 ? View.GONE : View.VISIBLE}"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintVertical_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/group_call_icon"
app:layout_constraintBottom_toTopOf="@id/contacts_and_suggestions_list"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/contacts_and_suggestions_list"
android:layout_width="0dp"
@ -211,7 +193,7 @@
android:visibility="@{viewModel.contactsAndSuggestionsList.size() == 0 ? View.GONE : View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/contacts_and_suggestions_label"
app:layout_constraintTop_toBottomOf="@id/group_call_icon"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,9 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.AppCompatTextView xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/section_header_style"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="21dp"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:text="@string/call_start_suggestions_list_title"/>
<layout
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="android.view.View" />
</data>
<androidx.appcompat.widget.AppCompatTextView
style="@style/section_header_style"
android:id="@+id/header"
android:background="@color/white"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="21dp"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:text="@string/call_start_suggestions_list_title"
android:gravity="center_vertical"/>
</layout>