Como criar cabeçalhos fixos no RecyclerView? (Sem lib externo)

120

Quero corrigir minhas visualizações de cabeçalho na parte superior da tela, como na imagem abaixo e sem usar bibliotecas externas.

insira a descrição da imagem aqui

No meu caso, não quero fazer isso alfabeticamente. Eu tenho dois tipos diferentes de visualizações (cabeçalho e normal). Eu só quero consertar o topo, o último cabeçalho.

Jaume Colom
fonte
17
a pergunta era sobre RecyclerView, esta ^ lib é baseada em ListView
Max Ch

Respostas:

319

Aqui vou explicar como fazê-lo sem uma biblioteca externa. Será um post muito longo, então prepare-se.

Antes de tudo, permita-me reconhecer @ tim.paetz cuja postagem me inspirou a iniciar uma jornada de implementação de meus próprios cabeçalhos persistentes usando ItemDecorations. Peguei emprestado algumas partes do código dele na minha implementação.

Como você já deve ter experimentado, se você tentou fazer isso sozinho, é muito difícil encontrar uma boa explicação de COMO realmente fazer isso com a ItemDecorationtécnica. Quero dizer, quais são os passos? Qual é a lógica por trás disso? Como coloco o cabeçalho no topo da lista? Não saber as respostas para essas perguntas é o que leva os outros a usar bibliotecas externas, enquanto faz isso sozinho com o uso de ItemDecorationé bastante fácil.

Condições iniciais

  1. O conjunto de dados deve ser um listitem de tipo diferente (não no sentido "tipos Java", mas no sentido "tipos cabeçalho / item").
  2. Sua lista já deve estar classificada.
  3. Cada item da lista deve ser de determinado tipo - deve haver um item de cabeçalho relacionado a ele.
  4. O primeiro item no listdeve ser um item de cabeçalho.

Aqui eu forneço o código completo para o meu RecyclerView.ItemDecorationchamado HeaderItemDecoration. Depois explico os passos dados em detalhes.

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

Logíca de negócios

Então, como faço para ficar?

Você não Você não pode fazer com que RecyclerViewo item de sua escolha pare e fique por cima, a menos que você seja um guru de layouts personalizados e conheça mais de 12.000 linhas de código de RecyclerViewcor. Portanto, como sempre acontece com o design da interface do usuário, se você não pode fazer algo, falsifique-o. Você apenas desenha o cabeçalho em cima de tudo usando Canvas. Você também deve saber quais itens o usuário pode ver no momento. Acontece que isso ItemDecorationpode lhe fornecer Canvasinformações sobre itens visíveis. Com isso, aqui estão as etapas básicas:

  1. No onDrawOvermétodo de RecyclerView.ItemDecorationobter o primeiro item (superior) visível para o usuário.

        View topChild = parent.getChildAt(0);
  2. Determine qual cabeçalho o representa.

            int topChildPosition = parent.getChildAdapterPosition(topChild);
        View currentHeader = getHeaderViewForItem(topChildPosition, parent);
  3. Desenhe o cabeçalho apropriado na parte superior do RecyclerView usando o drawHeader()método

Também quero implementar o comportamento quando o novo cabeçalho próximo encontrar o superior: deve parecer que o cabeçalho próximo empurra suavemente o cabeçalho atual superior para fora da visualização e, eventualmente, substitui-o.

A mesma técnica de "desenhar em cima de tudo" se aplica aqui.

  1. Determine quando o cabeçalho superior "preso" encontra o novo próximo.

            View childInContact = getChildInContact(parent, contactPoint);
  2. Obtenha esse ponto de contato (que é a parte inferior do cabeçalho pegajoso que você desenhou e a parte superior do próximo cabeçalho).

            int contactPoint = currentHeader.getBottom();
  3. Se o item da lista estiver ultrapassando esse "ponto de contato", redesenhe o cabeçalho adesivo para que sua parte inferior fique na parte superior do item invasor. Você consegue isso com o translate()método da Canvas. Como resultado, o ponto de partida do cabeçalho superior ficará fora da área visível e parecerá "sendo empurrado pelo próximo cabeçalho". Quando acabar, desenhe o novo cabeçalho no topo.

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

O restante é explicado por comentários e anotações completas no trecho de código que forneci.

O uso é direto:

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

Você mAdapterdeve implementar StickyHeaderInterfacepara que ele funcione. A implementação depende dos dados que você possui.

Finalmente, aqui eu forneço um gif com cabeçalhos semitransparentes, para que você possa entender a idéia e realmente ver o que está acontecendo sob o capô.

Aqui está a ilustração do conceito "basta desenhar em cima de tudo". Você pode ver que existem dois itens "cabeçalho 1" - um que desenhamos e permanece no topo em uma posição travada, e o outro que vem do conjunto de dados e se move com todos os demais itens. O usuário não verá o funcionamento interno, porque você não terá cabeçalhos semitransparentes.

conceito "basta desenhar em cima de tudo"

E aqui o que acontece na fase "empurrando":

fase "empurrando"

Espero que tenha ajudado.

Editar

Aqui está minha implementação real do getHeaderPositionForItem()método no adaptador do 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;
}

Implementação ligeiramente diferente no Kotlin

Sevastyan Savanyuk
fonte
4
@Sevastyan Just shiny! Gostei muito da maneira como você resolveu esse desafio. Nada a dizer, exceto talvez uma pergunta: existe uma maneira de definir um OnClickListener no "cabeçalho fixo" ou pelo menos consumir o clique, impedindo o usuário de clicar nele?
Denis
17
Seria ótimo se você colocar adaptador exemplo dessa aplicação
Solid Snake
1
Finalmente consegui trabalhar com alguns ajustes aqui e ali. embora se você adicionar algum preenchimento aos itens, ele continuará piscando sempre que você rolar para a área preenchida. a solução no layout do item cria um layout pai com preenchimento 0 e um layout filho com o preenchimento desejado.
Solid Snake
8
Obrigado. Solução interessante, mas um pouco cara para aumentar a exibição do cabeçalho em todos os eventos de rolagem. Acabei de alterar a lógica e usar o ViewHolder e mantê-los em um HashMap de WeakReferences para reutilizar visualizações já infladas.
Michael
4
@Sevastyan, ótimo trabalho. Eu tenho uma sugestão. Para evitar a criação de novos cabeçalhos sempre. Apenas salve o cabeçalho e altere-o apenas quando ele mudar. 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

A maneira mais fácil é criar apenas uma decoração de itens para o seu 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 seu cabeçalho em 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"
/>

E, finalmente, para adicionar o item Decoration ao seu 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);

Com essa decoração de itens, você pode tornar o cabeçalho fixado / pegajoso ou não com apenas um booleano ao criar a decoração de itens.

Você pode encontrar um exemplo de trabalho completo no github: https://github.com/paetztm/recycler_view_headers

tim.paetz
fonte
Obrigado. isso funcionou para mim, no entanto, esse cabeçalho se sobrepõe à revisão de reciclagem. você pode ajudar?
Kashyap jimuliya
Não sei ao certo o que você quer dizer com sobrepõe-se ao RecyclerView. Para o booleano "pegajoso", se você definir como false, ele colocará a decoração do item entre as linhas e não permanecerá na parte superior do RecyclerView.
tim.paetz
configurá-lo como "pegajoso" para falso coloca o cabeçalho entre as linhas, mas isso não fica preso (o que eu não quero) no topo. enquanto definindo-o como verdadeiro, ele permanece preso na parte superior, mas este se sobrepõe a primeira linha na recyclerview
kashyap jimuliya
Percebo que, como potencialmente dois problemas, um é o retorno de chamada da seção, você não está definindo o primeiro item (posição 0) para isSection como true. A outra é que você está passando na altura errada. A altura do xml para a exibição de texto deve ter a mesma altura que você passa para o construtor da decoração do item de seção.
tim.paetz
3
Uma coisa que gostaria de acrescentar é que, se o layout do cabeçalho tiver a visualização do texto do título dimensionada dinamicamente (por exemplo wrap_content), você desejaria executar fixLayoutSizedepois de definir o texto do título também.
Copolii
6

Eu fiz minha própria variação da solução de Sevastyan acima

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

... e aqui está a implementação do StickyHeaderInterface (eu fiz isso diretamente no adaptador da recicladora):

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
    ... */
}

Portanto, nesse caso, o cabeçalho não é apenas desenhar na tela, mas visualizar com seletor ou ondulação, listener de cliques etc.

Andrey Turkovsky
fonte
Obrigado por compartilhar! Por que você acabou envolvendo o RecyclerView em um novo RelativeLayout?
tmm1
Porque a minha versão do cabeçalho pegajosa é View, que eu coloquei para este RelativeLayout acima RecyclerView (no campo headerContainer)
Andrey Turkovsky
Você pode mostrar sua implementação no arquivo de classe? Como você passou o objeto de ouvinte que é implementado no adaptador.
Dipali s.
recyclerView.addItemDecoration(HeaderItemDecoration(recyclerView, adapter)). Desculpe, não consigo encontrar um exemplo de implementação que eu usei. Eu editei a resposta - adicionei algum texto aos comentários
Andrey Turkovsky
6

para quem procura uma solução para o problema de cintilação / piscada quando você já tem DividerItemDecoration. eu pareço ter resolvido assim:

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(...)
}

isso parece estar funcionando, mas alguém pode confirmar que eu não quebrei mais nada?

or_dvir
fonte
Obrigado, também resolveu o problema de piscar para mim.
Yamashiro Rion 26/02
3

Você pode verificar e executar a implementação da classe StickyHeaderHelperno meu projeto FlexibleAdapter e adaptá-la ao seu caso de uso.

Porém, sugiro usar a biblioteca, pois ela simplifica e reorganiza a maneira como você geralmente implementa os Adaptadores para o RecyclerView: Não reinvente a roda.

Eu diria também, não use Decoradores ou bibliotecas obsoletas, nem use bibliotecas que fazem apenas 1 ou 3 coisas; você precisará mesclar implementações de outras bibliotecas por conta própria.

Davideas
fonte
Passei 2 dias para ler o wiki e a amostra, mas ainda não sei como criar uma lista recolhível usando sua lib. A amostra é bastante complexo para o novato
Nguyen Minh Binh
1
Por que você é contra o uso de Decorators?
Sevastyan Savanyuk
1
@ Sevastyan, porque chegaremos ao ponto em que precisamos clicar no ouvinte e nas visualizações de filhos também. Nós Decorator você simplesmente não pode por definição.
Davideas
@ David, você quer dizer que deseja definir ouvintes de clique nos cabeçalhos no futuro? Se assim for, faz sentido. Ainda assim, se você fornecer seus cabeçalhos como itens do conjunto de dados, não haverá problemas. Até Yigit Boyar recomenda o uso de decoradores.
Sevastyan Savanyuk
@ Sevastyan, sim, na minha biblioteca, o cabeçalho é um item como outros da lista, para que os usuários possam manipulá-lo. Em um futuro distante, um gerente de layout personalizado substituirá o auxiliar atual.
Davideas 29/05
3

Outra solução, com base no ouvinte de rolagem. As condições iniciais são as mesmas da resposta 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);
}

Layout para ViewHolder e cabeçalho aderente.

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

Layout para o 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>

Classe para HeaderItem.

public class HeaderItem implements Item {

    private String title;

    public HeaderItem(String title) {
        this.title = title;
    }

    public String getTitle() {
        return title;
    }

}

É tudo útil. A implementação do adaptador, ViewHolder e outras coisas, não é interessante para nós.

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());
    }
}

Interface para a visualização do cabeçalho de ligação.

public interface StickyHeaderViewWrapper<T> {

    void bindView(T data);
}
Anrimian
fonte
Eu gosto desta solução. Erro de digitação pequeno em findNearestHeader: for (int i = position; position >= 0; i--){ //should be i >= 0
Konstantin
3

Ei,

É assim que se faz, se você deseja apenas um tipo de suporte quando ele começa a sair da tela (não estamos preocupados com nenhuma seção). Só existe uma maneira de quebrar a lógica interna do RecyclerView de itens de reciclagem e aumentar a exibição adicional sobre o item de cabeçalho do recyclerView e passar dados para ele. Vou deixar o código falar.

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

}

E então você apenas faz isso no seu adaptador:

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

Onde YOUR_STICKY_VIEW_HOLDER_TYPE é viewType do seu suposto suporte pegajoso.

Stanislav Kinzl
fonte
2

Para aqueles que podem se preocupar. Com base na resposta de Sevastyan, você deve fazer a rolagem horizontal. Simplesmente mude tudo getBottom()para getRight()e getTop()paragetLeft()

Guster
fonte
-1

A resposta já está aqui. Se você não quiser usar nenhuma biblioteca, siga estas etapas:

  1. Classificar lista com dados por nome
  2. Itere via lista com dados e no lugar quando a primeira letra do item atual! = Primeira letra do próximo item, insira o tipo de objeto "especial".
  3. Dentro do seu adaptador, coloque uma visualização especial quando o item for "especial".

Explicação:

No onCreateViewHoldermétodo, podemos verificarviewType e, dependendo do valor (nosso tipo "especial"), inflar um layout especial.

Por exemplo:

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;
}

onde class ItemElemente class TitleElementpode parecer comum ViewHolder:

public class ItemElement extends RecyclerView.ViewHolder {
//TextView text;

public ItemElement(View view) {
    super(view);
   //text = (TextView) view.findViewById(R.id.text);

}

Portanto, a ideia de tudo isso é interessante. Mas estou interessado se for efetivamente, porque precisamos classificar a lista de dados. E acho que isso diminuirá a velocidade. Se alguma idéia sobre isso, por favor escreva-me :)

E também a pergunta em aberto: é como manter o layout "especial" na parte superior, enquanto os itens estão em reciclagem. Talvez combine tudo isso com CoordinatorLayout.

valeria
fonte
é possível fazê-lo com CursorAdapter
M.Yogeshwaran
10
esta solução não diz nada sobre PEGAJOSOS cabeçalhos que é o ponto principal deste post
Siavash