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 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 ItemDecoration
té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 ItemDecoration
es bastante fácil.
Condiciones iniciales
- Su conjunto de datos debe ser
list
de elementos de diferente tipo (no en un sentido de "tipos de Java", sino en un sentido de tipos de "encabezado / elemento").
- Tu lista ya debería estar ordenada.
- Cada elemento de la lista debe ser de cierto tipo; debe haber un elemento de encabezado relacionado con él.
- El primer elemento del
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 un RecyclerView
artí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 ItemDecoration
puede proporcionarle tanto Canvas
información como información sobre elementos visibles. Con esto, aquí hay pasos básicos:
En el onDrawOver
método de RecyclerView.ItemDecoration
obtener el primer elemento (superior) que es visible para el usuario.
View topChild = parent.getChildAt(0);
Determine qué encabezado lo representa.
int topChildPosition = parent.getChildAdapterPosition(topChild);
View currentHeader = getHeaderViewForItem(topChildPosition, parent);
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".
Determine cuándo el encabezado superior "atascado" se encuentra con el nuevo próximo.
View childInContact = getChildInContact(parent, contactPoint);
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();
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 mAdapter
deben 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 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