studio stickyheaders recyclerview item ejemplo brandongogetap android header android-recyclerview sticky

android - stickyheaders - ¿Cómo puedo hacer encabezados adhesivos en RecyclerView?(Sin lib externo)



sticky header android (9)

Quiero arreglar mis vistas de encabezado en la parte superior de la pantalla como en la imagen a continuación y sin usar bibliotecas externas.

En mi caso, no quiero hacerlo alfabéticamente. Tengo dos tipos diferentes de vistas (encabezado y normal). Solo quiero arreglar en la parte superior, el último encabezado.


La respuesta ya ha estado aquí. Si no desea utilizar ninguna biblioteca, puede seguir estos pasos:

  1. Ordenar lista con datos por nombre
  2. Iterar a través de la lista con datos, y en su lugar cuando la primera letra del elemento actual = la primera letra del siguiente elemento, inserte el tipo de objeto "especial".
  3. Dentro de su adaptador coloque una vista especial cuando el artículo sea "especial".

Explicación:

En el onCreateViewHolder método podemos verificar viewType y, dependiendo del valor (nuestro tipo "especial"), inflamos un diseño especial.

Por ejemplo:

public static final int TITLE = 0; public static final int ITEM = 1; @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (context == null) { context = parent.getContext(); } if (viewType == TITLE) { view = LayoutInflater.from(context).inflate(R.layout.recycler_adapter_title, parent,false); return new TitleElement(view); } else if (viewType == ITEM) { view = LayoutInflater.from(context).inflate(R.layout.recycler_adapter_item, parent,false); return new ItemElement(view); } return null; }

donde class ItemElement y class TitleElement puede parecer ordinario ViewHolder :

public class ItemElement extends RecyclerView.ViewHolder { //TextView text; public ItemElement(View view) { super(view); //text = (TextView) view.findViewById(R.id.text); }

Entonces la idea de todo eso es interesante. Pero estoy interesado si es efectivo, porque necesitamos ordenar la lista de datos. Y creo que esto reducirá la velocidad. Si tienes alguna idea al respecto, escríbeme :)

Y también la pregunta abierta: es cómo mantener el diseño "especial" en la parte superior, mientras los artículos se reciclan. Quizás combine todo eso con CoordinatorLayout .


Para aquellos que puedan preocuparse. En función de la respuesta de Sevastyan, si desea que se desplace horizontalmente. Simplemente cambiar todo getBottom() a getRight() y getTop() a getLeft()


Aquí explicaré cómo hacerlo sin una biblioteca externa. Será una publicación muy larga, así que prepárate.

En primer lugar, permítanme agradecer a @tim.paetz cuya publicación me inspiró a emprender un viaje de implementación de mis propios encabezados adhesivos utilizando ItemDecoration s. Tomé prestadas algunas partes de su código en mi implementación.

Como ya habrás experimentado, si intentas hacerlo tú mismo, es muy difícil encontrar una buena explicación de CÓMO hacerlo realmente con la técnica ItemDecoration . Quiero decir, ¿cuáles son los pasos? ¿Cuál es la lógica detrás de esto? ¿Cómo hago que el encabezado se pegue en la parte superior de la lista? No saber las respuestas a estas preguntas es lo que hace que otros usen bibliotecas externas, mientras que hacerlo usted mismo con el uso de ItemDecoration es bastante fácil.

Condiciones iniciales

  1. Su conjunto de datos debe ser una list de elementos de diferente tipo (no en un sentido de "tipos Java", sino en un sentido de tipos "encabezado / elemento").
  2. Tu lista ya debería estar ordenada.
  3. Cada elemento de la lista debe ser de cierto tipo; debe haber un elemento de encabezado relacionado con él.
  4. El primer elemento de la list debe ser un elemento de encabezado.

Aquí proporciono el código completo para mi RecyclerView.ItemDecoration llamado HeaderItemDecoration . Luego explico los pasos tomados en detalle.

public class HeaderItemDecoration extends RecyclerView.ItemDecoration { private StickyHeaderInterface mListener; private int mStickyHeaderHeight; public HeaderItemDecoration(RecyclerView recyclerView, @NonNull StickyHeaderInterface listener) { mListener = listener; // On Sticky Header Click recyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() { public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent motionEvent) { if (motionEvent.getY() <= mStickyHeaderHeight) { // Handle the clicks on the header here ... return true; } return false; } public void onTouchEvent(RecyclerView recyclerView, MotionEvent motionEvent) { } public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { } }); } @Override public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { super.onDrawOver(c, parent, state); View topChild = parent.getChildAt(0); if (Util.isNull(topChild)) { return; } int topChildPosition = parent.getChildAdapterPosition(topChild); if (topChildPosition == RecyclerView.NO_POSITION) { return; } View currentHeader = getHeaderViewForItem(topChildPosition, parent); fixLayoutSize(parent, currentHeader); int contactPoint = currentHeader.getBottom(); View childInContact = getChildInContact(parent, contactPoint); if (Util.isNull(childInContact)) { return; } if (mListener.isHeader(parent.getChildAdapterPosition(childInContact))) { moveHeader(c, currentHeader, childInContact); return; } drawHeader(c, currentHeader); } private View getHeaderViewForItem(int itemPosition, RecyclerView parent) { int headerPosition = mListener.getHeaderPositionForItem(itemPosition); int layoutResId = mListener.getHeaderLayout(headerPosition); View header = LayoutInflater.from(parent.getContext()).inflate(layoutResId, parent, false); mListener.bindHeaderData(header, headerPosition); return header; } private void drawHeader(Canvas c, View header) { c.save(); c.translate(0, 0); header.draw(c); c.restore(); } private void moveHeader(Canvas c, View currentHeader, View nextHeader) { c.save(); c.translate(0, nextHeader.getTop() - currentHeader.getHeight()); currentHeader.draw(c); c.restore(); } private View getChildInContact(RecyclerView parent, int contactPoint) { View childInContact = null; for (int i = 0; i < parent.getChildCount(); i++) { View child = parent.getChildAt(i); if (child.getBottom() > contactPoint) { if (child.getTop() <= contactPoint) { // This child overlaps the contactPoint childInContact = child; break; } } } return childInContact; } /** * Properly measures and layouts the top sticky header. * @param parent ViewGroup: RecyclerView in this case. */ private void fixLayoutSize(ViewGroup parent, View view) { // Specs for parent (RecyclerView) int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY); int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED); // Specs for children (headers) int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec, parent.getPaddingLeft() + parent.getPaddingRight(), view.getLayoutParams().width); int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec, parent.getPaddingTop() + parent.getPaddingBottom(), view.getLayoutParams().height); view.measure(childWidthSpec, childHeightSpec); view.layout(0, 0, view.getMeasuredWidth(), mStickyHeaderHeight = view.getMeasuredHeight()); } public interface StickyHeaderInterface { /** * This method gets called by {@link HeaderItemDecoration} to fetch the position of the header item in the adapter * that is used for (represents) item at specified position. * @param itemPosition int. Adapter''s position of the item for which to do the search of the position of the header item. * @return int. Position of the header item in the adapter. */ int getHeaderPositionForItem(int itemPosition); /** * This method gets called by {@link HeaderItemDecoration} to get layout resource id for the header item at specified adapter''s position. * @param headerPosition int. Position of the header item in the adapter. * @return int. Layout resource id. */ int getHeaderLayout(int headerPosition); /** * This method gets called by {@link HeaderItemDecoration} to setup the header View. * @param header View. Header to set the data on. * @param headerPosition int. Position of the header item in the adapter. */ void bindHeaderData(View header, int headerPosition); /** * This method gets called by {@link HeaderItemDecoration} to verify whether the item represents a header. * @param itemPosition int. * @return true, if item at the specified adapter''s position represents a header. */ boolean isHeader(int itemPosition); } }

Lógica de negocios

Entonces, ¿cómo lo hago pegar?

Usted no No puede hacer que un elemento de RecyclerView elija elija detenerse y pegarse en la parte superior, a menos que sea un gurú de diseños personalizados y sepa de memoria más de 12,000 líneas de código para un RecyclerView . Entonces, como siempre ocurre con el diseño de la interfaz de usuario, si no puedes hacer algo, fingelo. Simplemente dibuja el encabezado encima de todo con Canvas . También debe saber qué elementos puede ver el usuario en este momento. Simplemente sucede que ItemDecoration puede proporcionarle tanto el Canvas como información sobre elementos visibles. Con esto, aquí hay pasos básicos:

  1. En el método onDrawOver de RecyclerView.ItemDecoration obtenga el primer elemento (superior) visible para el usuario.

    View topChild = parent.getChildAt(0);

  2. Determine qué encabezado lo representa.

    int topChildPosition = parent.getChildAdapterPosition(topChild); View currentHeader = getHeaderViewForItem(topChildPosition, parent);

  3. Dibuje el encabezado apropiado en la parte superior de RecyclerView utilizando el método drawHeader() .

También quiero implementar el comportamiento cuando el nuevo encabezado próximo se encuentre con el superior: debería parecer que el próximo encabezado empuja suavemente el encabezado actual superior fuera de la vista y finalmente toma su lugar.

Aquí se aplica la misma técnica de "dibujar encima de todo".

  1. Determine cuándo el encabezado superior "atascado" se encuentra con el nuevo próximo.

    View childInContact = getChildInContact(parent, contactPoint);

  2. Obtenga este punto de contacto (que es la parte inferior del encabezado adhesivo que dibujó y la parte superior del próximo encabezado).

    int contactPoint = currentHeader.getBottom();

  3. Si el elemento de la lista está traspasando este "punto de contacto", vuelva a dibujar su encabezado adhesivo para que su parte inferior esté en la parte superior del elemento de traspaso. Lo logras con el método translate() del Canvas . Como resultado, el punto de partida del encabezado superior estará fuera del área visible, y parecerá que el próximo encabezado lo está "expulsando". Cuando haya desaparecido por completo, dibuja el nuevo encabezado en la parte superior.

    if (childInContact != null) { if (mListener.isHeader(parent.getChildAdapterPosition(childInContact))) { moveHeader(c, currentHeader, childInContact); } else { drawHeader(c, currentHeader); } }

El resto se explica por comentarios y anotaciones detalladas en el código que proporcioné.

El uso es sencillo:

mRecyclerView.addItemDecoration(new HeaderItemDecoration((HeaderItemDecoration.StickyHeaderInterface) mAdapter));

Su mAdapter debe implementar StickyHeaderInterface para que funcione. La implementación depende de los datos que tenga.

Finalmente, aquí proporciono un gif con encabezados semitransparentes, para que pueda comprender la idea y realmente ver lo que sucede debajo del capó.

Aquí está la ilustración del concepto "simplemente dibuja encima de todo". Puede ver que hay dos elementos "encabezado 1": uno que dibujamos y permanece en la parte superior en una posición atascada, y el otro que proviene del conjunto de datos y se mueve con todos los elementos restantes. El usuario no verá su funcionamiento interno, ya que no tendrá encabezados semitransparentes.

Y aquí lo que sucede en la fase de "expulsión":

Espero que haya ayudado.

Editar

Aquí está mi implementación real del método getHeaderPositionForItem() en el adaptador de RecyclerView:

@Override public int getHeaderPositionForItem(int itemPosition) { int headerPosition = 0; do { if (this.isHeader(itemPosition)) { headerPosition = itemPosition; break; } itemPosition -= 1; } while (itemPosition >= 0); return headerPosition; }

Implementación ligeramente diferente en Kotlin


He hecho mi propia variación de la solución de Sevastyan anterior

class HeaderItemDecoration(recyclerView: RecyclerView, private val listener: StickyHeaderInterface) : RecyclerView.ItemDecoration() { private val headerContainer = FrameLayout(recyclerView.context) private var stickyHeaderHeight: Int = 0 private var currentHeader: View? = null private var currentHeaderPosition = 0 init { val layout = RelativeLayout(recyclerView.context) val params = recyclerView.layoutParams val parent = recyclerView.parent as ViewGroup val index = parent.indexOfChild(recyclerView) parent.addView(layout, index, params) parent.removeView(recyclerView) layout.addView(recyclerView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) layout.addView(headerContainer, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) } override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { super.onDrawOver(c, parent, state) val topChild = parent.getChildAt(0) ?: return val topChildPosition = parent.getChildAdapterPosition(topChild) if (topChildPosition == RecyclerView.NO_POSITION) { return } val currentHeader = getHeaderViewForItem(topChildPosition, parent) fixLayoutSize(parent, currentHeader) val contactPoint = currentHeader.bottom val childInContact = getChildInContact(parent, contactPoint) ?: return val nextPosition = parent.getChildAdapterPosition(childInContact) if (listener.isHeader(nextPosition)) { moveHeader(currentHeader, childInContact, topChildPosition, nextPosition) return } drawHeader(currentHeader, topChildPosition) } private fun getHeaderViewForItem(itemPosition: Int, parent: RecyclerView): View { val headerPosition = listener.getHeaderPositionForItem(itemPosition) val layoutResId = listener.getHeaderLayout(headerPosition) val header = LayoutInflater.from(parent.context).inflate(layoutResId, parent, false) listener.bindHeaderData(header, headerPosition) return header } private fun drawHeader(header: View, position: Int) { headerContainer.layoutParams.height = stickyHeaderHeight setCurrentHeader(header, position) } private fun moveHeader(currentHead: View, nextHead: View, currentPos: Int, nextPos: Int) { val marginTop = nextHead.top - currentHead.height if (currentHeaderPosition == nextPos && currentPos != nextPos) setCurrentHeader(currentHead, currentPos) val params = currentHeader?.layoutParams as? MarginLayoutParams ?: return params.setMargins(0, marginTop, 0, 0) currentHeader?.layoutParams = params headerContainer.layoutParams.height = stickyHeaderHeight + marginTop } private fun setCurrentHeader(header: View, position: Int) { currentHeader = header currentHeaderPosition = position headerContainer.removeAllViews() headerContainer.addView(currentHeader) } private fun getChildInContact(parent: RecyclerView, contactPoint: Int): View? = (0 until parent.childCount) .map { parent.getChildAt(it) } .firstOrNull { it.bottom > contactPoint && it.top <= contactPoint } private fun fixLayoutSize(parent: ViewGroup, view: View) { val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY) val heightSpec = View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED) val childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec, parent.paddingLeft + parent.paddingRight, view.layoutParams.width) val childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec, parent.paddingTop + parent.paddingBottom, view.layoutParams.height) view.measure(childWidthSpec, childHeightSpec) stickyHeaderHeight = view.measuredHeight view.layout(0, 0, view.measuredWidth, stickyHeaderHeight) } interface StickyHeaderInterface { fun getHeaderPositionForItem(itemPosition: Int): Int fun getHeaderLayout(headerPosition: Int): Int fun bindHeaderData(header: View, headerPosition: Int) fun isHeader(itemPosition: Int): Boolean } }

... y aquí está la implementación de StickyHeaderInterface (lo hice directamente en el adaptador de reciclador):

override fun getHeaderPositionForItem(itemPosition: Int): Int = (itemPosition downTo 0) .map { Pair(isHeader(it), it) } .firstOrNull { it.first }?.second ?: RecyclerView.NO_POSITION override fun getHeaderLayout(headerPosition: Int): Int { /* ... return something like R.layout.view_header or add conditions if you have different headers on different positions ... */ } override fun bindHeaderData(header: View, headerPosition: Int) { if (headerPosition == RecyclerView.NO_POSITION) header.layoutParams.height = 0 else /* ... here you get your header and can change some data on it ... */ } override fun isHeader(itemPosition: Int): Boolean { /* ... here have to be condition for checking - is item on this position header ... */ }

Entonces, en este caso, el encabezado no es solo dibujar en el lienzo, sino ver con selector o rizado, clicklistener, etc.


La forma más fácil es crear una Decoración de artículos para su RecyclerView.

import android.graphics.Canvas; import android.graphics.Rect; import android.support.annotation.NonNull; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; public class RecyclerSectionItemDecoration extends RecyclerView.ItemDecoration { private final int headerOffset; private final boolean sticky; private final SectionCallback sectionCallback; private View headerView; private TextView header; public RecyclerSectionItemDecoration(int headerHeight, boolean sticky, @NonNull SectionCallback sectionCallback) { headerOffset = headerHeight; this.sticky = sticky; this.sectionCallback = sectionCallback; } @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { super.getItemOffsets(outRect, view, parent, state); int pos = parent.getChildAdapterPosition(view); if (sectionCallback.isSection(pos)) { outRect.top = headerOffset; } } @Override public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { super.onDrawOver(c, parent, state); if (headerView == null) { headerView = inflateHeaderView(parent); header = (TextView) headerView.findViewById(R.id.list_item_section_text); fixLayoutSize(headerView, parent); } CharSequence previousHeader = ""; for (int i = 0; i < parent.getChildCount(); i++) { View child = parent.getChildAt(i); final int position = parent.getChildAdapterPosition(child); CharSequence title = sectionCallback.getSectionHeader(position); header.setText(title); if (!previousHeader.equals(title) || sectionCallback.isSection(position)) { drawHeader(c, child, headerView); previousHeader = title; } } } private void drawHeader(Canvas c, View child, View headerView) { c.save(); if (sticky) { c.translate(0, Math.max(0, child.getTop() - headerView.getHeight())); } else { c.translate(0, child.getTop() - headerView.getHeight()); } headerView.draw(c); c.restore(); } private View inflateHeaderView(RecyclerView parent) { return LayoutInflater.from(parent.getContext()) .inflate(R.layout.recycler_section_header, parent, false); } /** * Measures the header view to make sure its size is greater than 0 and will be drawn * https://yoda.entelect.co.za/view/9627/how-to-android-recyclerview-item-decorations */ private void fixLayoutSize(View view, ViewGroup parent) { int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY); int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED); int childWidth = ViewGroup.getChildMeasureSpec(widthSpec, parent.getPaddingLeft() + parent.getPaddingRight(), view.getLayoutParams().width); int childHeight = ViewGroup.getChildMeasureSpec(heightSpec, parent.getPaddingTop() + parent.getPaddingBottom(), view.getLayoutParams().height); view.measure(childWidth, childHeight); view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight()); } public interface SectionCallback { boolean isSection(int position); CharSequence getSectionHeader(int position); }

}

XML para su encabezado en recycler_section_header.xml:

<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/list_item_section_text" android:layout_width="match_parent" android:layout_height="@dimen/recycler_section_header_height" android:background="@android:color/black" android:paddingLeft="10dp" android:paddingRight="10dp" android:textColor="@android:color/white" android:textSize="14sp" />

Y finalmente para agregar la Decoración del artículo a su RecyclerView:

RecyclerSectionItemDecoration sectionItemDecoration = new RecyclerSectionItemDecoration(getResources().getDimensionPixelSize(R.dimen.recycler_section_header_height), true, // true for sticky, false for not new RecyclerSectionItemDecoration.SectionCallback() { @Override public boolean isSection(int position) { return position == 0 || people.get(position) .getLastName() .charAt(0) != people.get(position - 1) .getLastName() .charAt(0); } @Override public CharSequence getSectionHeader(int position) { return people.get(position) .getLastName() .subSequence(0, 1); } }); recyclerView.addItemDecoration(sectionItemDecoration);

Con esta Decoración del artículo, puede hacer que el encabezado quede fijo / pegajoso o no con solo un booleano al crear la Decoración del artículo.

Puede encontrar un ejemplo de trabajo completo en github: https://github.com/paetztm/recycler_view_headers


Otra solución, basada en el oyente scroll. Las condiciones iniciales son las mismas que en la respuesta de Sevastyan

RecyclerView recyclerView; TextView tvTitle; //sticky header view //... onCreate, initialize, etc... public void bindList(List<Item> items) { //All data in adapter. Item - just interface for different item types adapter = new YourAdapter(items); recyclerView.setAdapter(adapter); StickyHeaderViewManager<HeaderItem> stickyHeaderViewManager = new StickyHeaderViewManager<>( tvTitle, recyclerView, HeaderItem.class, //HeaderItem - subclass of Item, used to detect headers in list data -> { // bind function for sticky header view tvTitle.setText(data.getTitle()); }); stickyHeaderViewManager.attach(items); }

Diseño para ViewHolder y encabezado adhesivo.

item_header.xml

<TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/tv_title" android:layout_width="match_parent" android:layout_height="wrap_content"/>

Diseño para RecyclerView

<FrameLayout android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.v7.widget.RecyclerView android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent"/> <!--it can be any view, but order important, draw over recyclerView--> <include layout="@layout/item_header"/> </FrameLayout>

Clase para HeaderItem.

public class HeaderItem implements Item { private String title; public HeaderItem(String title) { this.title = title; } public String getTitle() { return title; } }

Es todo uso. La implementación del adaptador, ViewHolder y otras cosas, no es interesante para nosotros.

public class StickyHeaderViewManager<T> { @Nonnull private View headerView; @Nonnull private RecyclerView recyclerView; @Nonnull private StickyHeaderViewWrapper<T> viewWrapper; @Nonnull private Class<T> headerDataClass; private List<?> items; public StickyHeaderViewManager(@Nonnull View headerView, @Nonnull RecyclerView recyclerView, @Nonnull Class<T> headerDataClass, @Nonnull StickyHeaderViewWrapper<T> viewWrapper) { this.headerView = headerView; this.viewWrapper = viewWrapper; this.recyclerView = recyclerView; this.headerDataClass = headerDataClass; } public void attach(@Nonnull List<?> items) { this.items = items; if (ViewCompat.isLaidOut(headerView)) { bindHeader(recyclerView); } else { headerView.post(() -> bindHeader(recyclerView)); } recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); bindHeader(recyclerView); } }); } private void bindHeader(RecyclerView recyclerView) { if (items.isEmpty()) { headerView.setVisibility(View.GONE); return; } else { headerView.setVisibility(View.VISIBLE); } View topView = recyclerView.getChildAt(0); if (topView == null) { return; } int topPosition = recyclerView.getChildAdapterPosition(topView); if (!isValidPosition(topPosition)) { return; } if (topPosition == 0 && topView.getTop() == recyclerView.getTop()) { headerView.setVisibility(View.GONE); return; } else { headerView.setVisibility(View.VISIBLE); } T stickyItem; Object firstItem = items.get(topPosition); if (headerDataClass.isInstance(firstItem)) { stickyItem = headerDataClass.cast(firstItem); headerView.setTranslationY(0); } else { stickyItem = findNearestHeader(topPosition); int secondPosition = topPosition + 1; if (isValidPosition(secondPosition)) { Object secondItem = items.get(secondPosition); if (headerDataClass.isInstance(secondItem)) { View secondView = recyclerView.getChildAt(1); if (secondView != null) { moveViewFor(secondView); } } else { headerView.setTranslationY(0); } } } if (stickyItem != null) { viewWrapper.bindView(stickyItem); } } private void moveViewFor(View secondView) { if (secondView.getTop() <= headerView.getBottom()) { headerView.setTranslationY(secondView.getTop() - headerView.getHeight()); } else { headerView.setTranslationY(0); } } private T findNearestHeader(int position) { for (int i = position; position >= 0; i--) { Object item = items.get(i); if (headerDataClass.isInstance(item)) { return headerDataClass.cast(item); } } return null; } private boolean isValidPosition(int position) { return !(position == RecyclerView.NO_POSITION || position >= items.size()); } }

Interfaz para la vista de encabezado de enlace.

public interface StickyHeaderViewWrapper<T> { void bindView(T data); }


Puede verificar y tomar la implementación de la clase StickyHeaderHelper en mi proyecto FlexibleAdapter y adaptarla a su caso de uso.

Pero, sugiero usar la biblioteca, ya que simplifica y reorganiza la forma en que normalmente implementa los adaptadores para RecyclerView: no reinvente la rueda.

También diría que no use decoradores o bibliotecas obsoletas, así como no use bibliotecas que solo hagan 1 o 3 cosas, tendrá que fusionar las implementaciones de otras bibliotecas usted mismo.


Yo,

Así es como lo hace si desea solo un tipo de soporte cuando comienza a salir de la pantalla (no nos preocupamos por ninguna sección). Solo hay una forma sin romper la lógica interna de RecyclerView de reciclar elementos y es inflar una vista adicional en la parte superior del elemento de encabezado de recyclerView y pasar datos al mismo. Dejaré hablar el código.

import android.graphics.Canvas import android.graphics.Rect import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.annotation.LayoutRes import androidx.recyclerview.widget.RecyclerView class StickyHeaderItemDecoration(@LayoutRes private val headerId: Int, private val HEADER_TYPE: Int) : RecyclerView.ItemDecoration() { private lateinit var stickyHeaderView: View private lateinit var headerView: View private var sticked = false // executes on each bind and sets the stickyHeaderView override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { super.getItemOffsets(outRect, view, parent, state) val position = parent.getChildAdapterPosition(view) val adapter = parent.adapter ?: return val viewType = adapter.getItemViewType(position) if (viewType == HEADER_TYPE) { headerView = view } } override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { super.onDrawOver(c, parent, state) if (::headerView.isInitialized) { if (headerView.y <= 0 && !sticked) { stickyHeaderView = createHeaderView(parent) fixLayoutSize(parent, stickyHeaderView) sticked = true } if (headerView.y > 0 && sticked) { sticked = false } if (sticked) { drawStickedHeader(c) } } } private fun createHeaderView(parent: RecyclerView) = LayoutInflater.from(parent.context).inflate(headerId, parent, false) private fun drawStickedHeader(c: Canvas) { c.save() c.translate(0f, Math.max(0f, stickyHeaderView.top.toFloat() - stickyHeaderView.height.toFloat())) headerView.draw(c) c.restore() } private fun fixLayoutSize(parent: ViewGroup, view: View) { // Specs for parent (RecyclerView) val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY) val heightSpec = View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED) // Specs for children (headers) val childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec, parent.paddingLeft + parent.paddingRight, view.getLayoutParams().width) val childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec, parent.paddingTop + parent.paddingBottom, view.getLayoutParams().height) view.measure(childWidthSpec, childHeightSpec) view.layout(0, 0, view.measuredWidth, view.measuredHeight) } }

Y luego solo haces esto en tu adaptador:

override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { super.onAttachedToRecyclerView(recyclerView) recyclerView.addItemDecoration(StickyHeaderItemDecoration(R.layout.item_time_filter, YOUR_STICKY_VIEW_HOLDER_TYPE)) }

Donde YOUR_STICKY_VIEW_HOLDER_TYPE es viewType de lo que se supone que es un soporte adhesivo.


a cualquiera que busque una solución al problema de parpadeo / parpadeo cuando ya tiene DividerItemDecoration . Parece que lo he resuelto así:

override fun onDrawOver(...) { //code from before //do NOT return on null val childInContact = getChildInContact(recyclerView, currentHeader.bottom) //add null check if (childInContact != null && mHeaderListener.isHeader(recyclerView.getChildAdapterPosition(childInContact))) { moveHeader(...) return } drawHeader(...) }

Esto parece estar funcionando, pero ¿alguien puede confirmar que no rompí nada más?