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 ItemDecoration
s. 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 ItemDecoration
té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
- O conjunto de dados deve ser um
list
item de tipo diferente (não no sentido "tipos Java", mas no sentido "tipos cabeçalho / item").
- Sua lista já deve estar classificada.
- Cada item da lista deve ser de determinado tipo - deve haver um item de cabeçalho relacionado a ele.
- O primeiro item no
list
deve ser um item de cabeçalho.
Aqui eu forneço o código completo para o meu RecyclerView.ItemDecoration
chamado 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 RecyclerView
o 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 RecyclerView
cor. 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 ItemDecoration
pode lhe fornecer Canvas
informações sobre itens visíveis. Com isso, aqui estão as etapas básicas:
No onDrawOver
método de RecyclerView.ItemDecoration
obter o primeiro item (superior) visível para o usuário.
View topChild = parent.getChildAt(0);
Determine qual cabeçalho o representa.
int topChildPosition = parent.getChildAdapterPosition(topChild);
View currentHeader = getHeaderViewForItem(topChildPosition, parent);
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.
Determine quando o cabeçalho superior "preso" encontra o novo próximo.
View childInContact = getChildInContact(parent, contactPoint);
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();
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ê mAdapter
deve implementar StickyHeaderInterface
para 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.
E aqui o que acontece na 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