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

120

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.

ingrese la descripción de la imagen aquí

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.

Jaume Colom
fuente
17
la pregunta era sobre RecyclerView, este ^ lib está basado en ListView
Max Ch

Respuestas:

319

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

En primer lugar, permítanme reconocer a @ tim.paetz cuya publicación me inspiró a emprender un viaje de implementación de mis propios encabezados adhesivos usando ItemDecorations. 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 ItemDecorationtécnica. 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 ItemDecorationes bastante fácil.

Condiciones iniciales

  1. Su conjunto de datos debe ser listde elementos de diferente tipo (no en un sentido de "tipos de Java", sino en un sentido de tipos de "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 del listdebe ser un elemento de encabezado.

Aquí proporciono el código completo para mi RecyclerView.ItemDecorationllamado 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 un RecyclerViewartículo de su elección, simplemente deténgase y quédese encima, a menos que sea un gurú de los diseños personalizados y sepa de memoria más de 12,000 líneas de código RecyclerView. Entonces, como siempre ocurre con el diseño de la interfaz de usuario, si no puedes hacer algo, fingelo. Usted acaba de dibujar el encabezado en la parte superior de todo el uso Canvas. También debe saber qué elementos puede ver el usuario en este momento. Simplemente sucede que eso ItemDecorationpuede proporcionarle tanto Canvasinformación como información sobre elementos visibles. Con esto, aquí hay pasos básicos:

  1. En el onDrawOvermétodo de RecyclerView.ItemDecorationobtener el primer elemento (superior) que es 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 mediante el drawHeader()método.

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 sobre 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 (es decir, 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 translate()método de 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 mAdapterdeben implementar StickyHeaderInterfacepara 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.

concepto "solo dibuja sobre todo"

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

fase de "expulsión"

Espero que haya ayudado.

Editar

Aquí está mi implementación real del getHeaderPositionForItem()método en el adaptador 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

Sevastyan Savanyuk
fuente
44
@Sevastyan ¡Simplemente genial! Realmente me gustó la forma en que resolviste este desafío. Nada que decir, excepto tal vez una pregunta: ¿hay alguna manera de establecer un OnClickListener en el "encabezado adhesivo", o al menos consumir el clic para evitar que el usuario haga clic en él?
Denis
17
Sería genial si pones un ejemplo de adaptador de esta implementación
SolidSnake
1
Finalmente logré trabajar con algunos ajustes aquí y allá. aunque si agrega algún relleno a sus elementos, seguirá parpadeando cada vez que se desplace al área acolchada. la solución en el diseño de su elemento cree un diseño primario con 0 relleno y un diseño secundario con el relleno que desee.
SolidSnake
8
Gracias. Solución interesante, pero un poco costosa para inflar la vista de encabezado en cada evento de desplazamiento. Acabo de cambiar la lógica y uso ViewHolder y los mantengo en un HashMap de WeakReferences para reutilizar las vistas ya infladas.
Michael
44
@Sevastyan, buen trabajo. Tengo una sugerencia. Para evitar crear nuevos encabezados cada vez. Simplemente guarde el encabezado y cámbielo solo cuando cambie. private View getHeaderViewForItem(int itemPosition, RecyclerView parent) { int headerPosition = mListener.getHeaderPositionForItem(itemPosition); if(headerPosition != mCurrentHeaderIndex) { mCurrentHeader = mListener.createHeaderView(headerPosition, parent); mCurrentHeaderIndex = headerPosition; } return mCurrentHeader; }
Vera Rivotti
27

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 / adhesivo 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

tim.paetz
fuente
Gracias. esto funcionó para mí, sin embargo, este encabezado se superpone a la vista del reciclador. ¿puede usted ayudar?
kashyap jimuliya
No estoy seguro de lo que quieres decir con superposiciones de RecyclerView. Para el booleano "pegajoso", si lo configura como falso, colocará la decoración del elemento entre las filas y no permanecerá en la parte superior de RecyclerView.
tim.paetz
configurarlo como "pegajoso" en falso pone el encabezado entre las filas, pero eso no se queda atascado (lo cual no quiero) en la parte superior. mientras lo establece en verdadero, permanece atascado en la parte superior pero se superpone a la primera fila en la vista del reciclador
kashyap jimuliya
Puedo ver que, como potencialmente dos problemas, uno es la devolución de llamada de la sección, no está configurando el primer elemento (posición 0) para isSection en verdadero. El otro es que estás pasando a la altura incorrecta. La altura del xml para la vista de texto debe ser la misma altura que la altura que pasa al constructor de la decoración del elemento de sección.
tim.paetz
3
Una cosa que agregaría es que si su diseño de encabezado tiene la vista de texto del título de tamaño dinámico (por ejemplo wrap_content), también querrá ejecutar fixLayoutSizedespués de configurar el texto del título.
copolii
6

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 ondulación, clicklistener, etc.

Andrey Turkovsky
fuente
¡Gracias por compartir! ¿Por qué terminaste envolviendo RecyclerView en un nuevo RelativeLayout?
tmm1
Debido a que mi versión del encabezado fijo es View, que puse en este RelativeLayout sobre RecyclerView (en el campo headerContainer)
Andrey Turkovsky
¿Puedes mostrar tu implementación en el archivo de clase? Cómo pasó el objeto de escucha que se implementa en el adaptador.
Dipali Shah
recyclerView.addItemDecoration(HeaderItemDecoration(recyclerView, adapter)). Lo siento, no puedo encontrar un ejemplo de implementación, que utilicé. He editado la respuesta - agregué un texto a los comentarios
Andrey Turkovsky
6

a cualquiera que busque una solución al problema de parpadeo / parpadeo cuando ya lo haya hecho 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?

or_dvir
fuente
Gracias, también resolvió el problema del parpadeo para mí.
Yamashiro Rion
3

Puede verificar y tomar la implementación de la clase StickyHeaderHelperen 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.

Davideas
fuente
Pasé 2 días para leer el wiki y la muestra, pero aún no sé cómo crear una lista plegable usando tu lib. La muestra es bastante compleja para los novatos
Nguyen Minh Binh
1
¿Por qué estás en contra de usar Decorators?
Sevastyan Savanyuk
1
@Sevastyan, porque llegaremos al punto en el que necesitamos escuchar clics en él y también en las vistas secundarias. Nosotros Decorador simplemente no puedes por definición.
Davideas
@Davidea, ¿quiere decir que desea configurar escuchas de clics en los encabezados en el futuro? Si es así, tiene sentido. Pero aún así, si proporciona sus encabezados como elementos del conjunto de datos, no habrá problemas. Incluso Yigit Boyar recomienda usar decoradores.
Sevastyan Savanyuk
@Sevastyan, sí, en mi biblioteca, el encabezado es un elemento como otros en la lista, por lo que los usuarios pueden manipularlo. En un futuro lejano, un administrador de diseño personalizado reemplazará al ayudante actual.
Davideas
3

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);
}
Anrimiano
fuente
Me gusta esta solución Pequeño error tipográfico en findNearestHeader: for (int i = position; position >= 0; i--){ //should be i >= 0
Konstantin
3

Yo,

Así es como lo hace si desea un solo tipo de soporte cuando comienza a salir de la pantalla (no nos interesan las secciones). Solo hay una manera 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.

Stanislav Kinzl
fuente
2

Para aquellos que puedan preocuparse. Según la respuesta de Sevastyan, si desea que sea horizontal, desplácese. Simplemente cambiar todo getBottom()a getRight()y getTop()agetLeft()

Guster
fuente
-1

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 = primera letra del siguiente elemento, inserte un tipo de objeto "especial".
  3. Dentro de su adaptador coloque una vista especial cuando el artículo sea "especial".

Explicación:

En el onCreateViewHoldermétodo podemos verificar viewTypey, 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 ItemElementy class TitleElementpuede 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. Tal vez combine todo eso con CoordinatorLayout.

Valeria
fuente
¿es posible hacerlo con cursoradapter
M.Yogeshwaran
10
esta solución no dice nada sobre los encabezados STICKY, que es el punto principal de esta publicación
Siavash