Android 5.0 - Adicionar cabeçalho / rodapé para um RecyclerView

? MathieuMaree @ | Original: StackOverFlow
---

Passei um momento tentando descobrir uma maneira de adicionar um cabeçalho para um RecyclerView, sem sucesso . Isto é o que eu tenho até agora:

@Override
protected void onCreate(Bundle savedInstanceState)
{
    ...

    layouManager = new LinearLayoutManager(getActivity());
    recyclerView.setLayoutManager(layouManager);

    LayoutInflater inflater = (LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    headerPlaceHolder = inflater.inflate(R.layout.view_header_holder_medium, null, false);
    layouManager.addView(headerPlaceHolder, 0);

   ...
}

O LayoutManager parece ser o objecto de manuseamento a disposição dos itens RecyclerView . Como eu não poderia encontrar qualquer método addHeaderView(View view), eu decidi ir com o método de addView(View view, int position) do LayoutManager e adicionar meu ver cabeçalho na primeira posição para agir como um cabeçalho.

Aaand este é o lugar onde as coisas ficam mais feio :

java.lang.NullPointerException: Attempt to read from field 'android.support.v7.widget.RecyclerView$ViewHolder android.support.v7.widget.RecyclerView$LayoutParams.mViewHolder' on a null object reference
            at android.support.v7.widget.RecyclerView.getChildViewHolderInt(RecyclerView.java:2497)
            at android.support.v7.widget.RecyclerView$LayoutManager.addViewInt(RecyclerView.java:4807)
            at android.support.v7.widget.RecyclerView$LayoutManager.addView(RecyclerView.java:4803)
            at com.mathieumaree.showz.fragments.CategoryFragment.setRecyclerView(CategoryFragment.java:231)
            at com.mathieumaree.showz.fragments.CategoryFragment.access$200(CategoryFragment.java:47)
            at com.mathieumaree.showz.fragments.CategoryFragment$2.success(CategoryFragment.java:201)
            at com.mathieumaree.showz.fragments.CategoryFragment$2.success(CategoryFragment.java:196)
            at retrofit.CallbackRunnable$1.run(CallbackRunnable.java:41)
            at android.os.Handler.handleCallback(Handler.java:739)
            at android.os.Handler.dispatchMessage(Handler.java:95)
            at android.os.Looper.loop(Looper.java:135)
            at android.app.ActivityThread.main(ActivityThread.java:5221)
            at java.lang.reflect.Method.invoke(Native Method)
            at java.lang.reflect.Method.invoke(Method.java:372)
            at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:899)
            at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:694)

Depois de pegar vários NullPointerExceptions tentando chamar o addView(View view) em diferentes momentos da criação Atividade (também tentei adicionar o ponto de vista, uma vez que tudo estiver configurado, até mesmo os dados do adaptador ), eu percebi que eu não tenho idéia se este é o caminho certo para fazê-lo (e ele não parece ser) .

Alguém poderia me ajudar? Ou pelo menos me dar uma ideia / nova direção para explorar ?

Desde já, obrigado!

VieuMa

PS : Além disso, uma solução que pudesse lidar com a GridLayoutManager além da LinearLayoutManager seria muito apreciada !

---

Top 5 Responder

1Ian Newson @

Eu não tentei isso, mas eu simplesmente adicionar 1 (ou 2, se você quer tanto um cabeçalho e rodapé ) para o número inteiro retornado por GetItemCount em seu adaptador. Você pode então substituir getItemViewType em seu adaptador para devolver um número inteiro diferente quando i==0 : https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html#getItemViewType(int)

createViewHolder É, então, passou o número inteiro que você voltou de getItemViewType, o que lhe permite criar ou configurar o suporte de vista de forma diferente para a vista de cabeçalho: https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html#createViewHolder(android.view.ViewGroup, Int)

Não se esqueça de subtrair um do inteiro posição passado para bindViewHolder .

2seb @

Eu tive o mesmo problema e criou duas abordagens para quebrar o adaptador. Um é muito fácil de usar, mas eu não tenho certeza de como ele vai se comportar com um conjunto de dados em mudança. Porque envolve seu adaptador e você precisa fazer-se esqueça de chamar métodos como notifyDataSetChanged no adaptador - objeto certo .

A outra não deve ter tais problemas . Apenas deixe o seu adaptador normal estender a classe, implementar os métodos abstratos e você deve estar pronto . E aqui estão elas:

gists

https://gist.github.com/sebnapi/a2596ec3d1730ea47986 usage new HeaderRecyclerViewAdapterV1(new RegularAdapter()); https://gist.github.com/sebnapi/fde648c17616d9d3bcde usage RegularAdapter extends HeaderRecyclerViewAdapterV2

HeaderRecyclerViewAdapterV1

import android.support.v7.widget.RecyclerView;
import android.view.ViewGroup;

/**
 * Created by sebnapi on 08.11.14.
 * <p/>
 * This is a Plug-and-Play Approach for adding a Header or Footer to
 * a RecyclerView backed list
 * <p/>
 * Just wrap your regular adapter like this
 * <p/>
 * new HeaderRecyclerViewAdapterV1(new RegularAdapter())
 * <p/>
 * Let RegularAdapter implement HeaderRecyclerView, FooterRecyclerView or both
 * and you are ready to go.
 * <p/>
 * I'm absolutely not sure how this will behave with changes in the dataset.
 * You can always wrap a fresh adapter and make sure to not change the old one or
 * use my other approach.
 * <p/>
 * With the other approach you need to let your Adapter extend HeaderRecyclerViewAdapterV2
 * (and therefore change potentially more code) but possible omit these shortcomings.
 * <p/>
 * TOTALLY UNTESTED - USE WITH CARE - HAVE FUN :)
 */
public class HeaderRecyclerViewAdapterV1 extends RecyclerView.Adapter {
    private static final int TYPE_HEADER = Integer.MIN_VALUE;
    private static final int TYPE_FOOTER = Integer.MIN_VALUE + 1;
    private static final int TYPE_ADAPTEE_OFFSET = 2;

    private final RecyclerView.Adapter mAdaptee;


    public HeaderRecyclerViewAdapterV1(RecyclerView.Adapter adaptee) {
        mAdaptee = adaptee;
    }

    public RecyclerView.Adapter getAdaptee() {
        return mAdaptee;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType == TYPE_HEADER && mAdaptee instanceof HeaderRecyclerView) {
            return ((HeaderRecyclerView) mAdaptee).onCreateHeaderViewHolder(parent, viewType);
        } else if (viewType == TYPE_FOOTER && mAdaptee instanceof FooterRecyclerView) {
            return ((FooterRecyclerView) mAdaptee).onCreateFooterViewHolder(parent, viewType);
        }
        return mAdaptee.onCreateViewHolder(parent, viewType - TYPE_ADAPTEE_OFFSET);
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (position == 0 && holder.getItemViewType() == TYPE_HEADER && useHeader()) {
            ((HeaderRecyclerView) mAdaptee).onBindHeaderView(holder, position);
        } else if (position == mAdaptee.getItemCount() && holder.getItemViewType() == TYPE_FOOTER && useFooter()) {
            ((FooterRecyclerView) mAdaptee).onBindFooterView(holder, position);
        } else {
            mAdaptee.onBindViewHolder(holder, position - (useHeader() ? 1 : 0));
        }
    }

    @Override
    public int getItemCount() {
        int itemCount = mAdaptee.getItemCount();
        if (useHeader()) {
            itemCount += 1;
        }
        if (useFooter()) {
            itemCount += 1;
        }
        return itemCount;
    }

    private boolean useHeader() {
        if (mAdaptee instanceof HeaderRecyclerView) {
            return true;
        }
        return false;
    }

    private boolean useFooter() {
        if (mAdaptee instanceof FooterRecyclerView) {
            return true;
        }
        return false;
    }

    @Override
    public int getItemViewType(int position) {
        if (position == 0 && useHeader()) {
            return TYPE_HEADER;
        }
        if (position == mAdaptee.getItemCount() && useFooter()) {
            return TYPE_FOOTER;
        }
        if (mAdaptee.getItemCount() >= Integer.MAX_VALUE - TYPE_ADAPTEE_OFFSET) {
            new IllegalStateException("HeaderRecyclerViewAdapter offsets your BasicItemType by " + TYPE_ADAPTEE_OFFSET + ".");
        }
        return mAdaptee.getItemViewType(position) + TYPE_ADAPTEE_OFFSET;
    }


    public static interface HeaderRecyclerView {
        public RecyclerView.ViewHolder onCreateHeaderViewHolder(ViewGroup parent, int viewType);

        public void onBindHeaderView(RecyclerView.ViewHolder holder, int position);
    }

    public static interface FooterRecyclerView {
        public RecyclerView.ViewHolder onCreateFooterViewHolder(ViewGroup parent, int viewType);

        public void onBindFooterView(RecyclerView.ViewHolder holder, int position);
    }

}

HeaderRecyclerViewAdapterV2

import android.support.v7.widget.RecyclerView;
import android.view.ViewGroup;

/**
 * Created by sebnapi on 08.11.14.
 * <p/>
 * If you extend this Adapter you are able to add a Header, a Footer or both
 * by a similar ViewHolder pattern as in RecyclerView.
 * <p/>
 * If you want to omit changes to your class hierarchy you can try the Plug-and-Play
 * approach HeaderRecyclerViewAdapterV1.
 * <p/>
 * Don't override (Be careful while overriding)
 * - onCreateViewHolder
 * - onBindViewHolder
 * - getItemCount
 * - getItemViewType
 * <p/>
 * You need to override the abstract methods introduced by this class. This class
 * is not using generics as RecyclerView.Adapter make yourself sure to cast right.
 * <p/>
 * TOTALLY UNTESTED - USE WITH CARE - HAVE FUN :)
 */
public abstract class HeaderRecyclerViewAdapterV2 extends RecyclerView.Adapter {
    private static final int TYPE_HEADER = Integer.MIN_VALUE;
    private static final int TYPE_FOOTER = Integer.MIN_VALUE + 1;
    private static final int TYPE_ADAPTEE_OFFSET = 2;

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType == TYPE_HEADER) {
            return onCreateHeaderViewHolder(parent, viewType);
        } else if (viewType == TYPE_FOOTER) {
            return onCreateFooterViewHolder(parent, viewType);
        }
        return onCreateBasicItemViewHolder(parent, viewType - TYPE_ADAPTEE_OFFSET);
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (position == 0 && holder.getItemViewType() == TYPE_HEADER) {
            onBindHeaderView(holder, position);
        } else if (position == getBasicItemCount() && holder.getItemViewType() == TYPE_FOOTER) {
            onBindFooterView(holder, position);
        } else {
            onBindBasicItemView(holder, position - (useHeader() ? 1 : 0));
        }
    }

    @Override
    public int getItemCount() {
        int itemCount = getBasicItemCount();
        if (useHeader()) {
            itemCount += 1;
        }
        if (useFooter()) {
            itemCount += 1;
        }
        return itemCount;
    }

    @Override
    public int getItemViewType(int position) {
        if (position == 0 && useHeader()) {
            return TYPE_HEADER;
        }
        if (position == getBasicItemCount() && useFooter()) {
            return TYPE_FOOTER;
        }
        if (getBasicItemType(position) >= Integer.MAX_VALUE - TYPE_ADAPTEE_OFFSET) {
            new IllegalStateException("HeaderRecyclerViewAdapter offsets your BasicItemType by " + TYPE_ADAPTEE_OFFSET + ".");
        }
        return getBasicItemType(position) + TYPE_ADAPTEE_OFFSET;
    }

    public abstract boolean useHeader();

    public abstract RecyclerView.ViewHolder onCreateHeaderViewHolder(ViewGroup parent, int viewType);

    public abstract void onBindHeaderView(RecyclerView.ViewHolder holder, int position);

    public abstract boolean useFooter();

    public abstract RecyclerView.ViewHolder onCreateFooterViewHolder(ViewGroup parent, int viewType);

    public abstract void onBindFooterView(RecyclerView.ViewHolder holder, int position);

    public abstract RecyclerView.ViewHolder onCreateBasicItemViewHolder(ViewGroup parent, int viewType);

    public abstract void onBindBasicItemView(RecyclerView.ViewHolder holder, int position);

    public abstract int getBasicItemCount();

    /**
     * make sure you don't use [Integer.MAX_VALUE-1, Integer.MAX_VALUE] as BasicItemViewType
     *
     * @param position
     * @return
     */
    public abstract int getBasicItemType(int position);

}

Feedback e garfos apreciado. Vou usar HeaderRecyclerViewAdapterV2 por minha auto e evoluir, testar e publicar as mudanças no futuro .

EDIT: OvidiuLatcu Sim, eu tive alguns problemas. Na verdade, eu parei de compensar o Header implicitamente por position - (useHeader() ? 1 : 0) e em vez disso criou um método público int offsetPosition(int position) para ele. Porque se você definir um OnItemTouchListener na Recyclerview, você pode interceptar o toque, obtenha as coordenadas x, y do toque, encontrar o ponto de vista da criança de acordo e, em seguida, chamar de recyclerView.getChildPosition(...) e você sempre terá a posição não- offsetted no adaptador ! Este é um shortcomming no Código RecyclerView, não vejo um método fácil para superar isso. É por isso que agora compensar as posições explícitas quando eu preciso por meu próprio código .

3darnmason @

Acabei por implementar o meu próprio adaptador para embrulhar qualquer outro adaptador e fornecer métodos para adicionar pontos de vista de cabeçalho e rodapé .

Criado uma essência aqui: https://gist.github.com/darnmason/7bbf8beae24fe7296c8a

A principal característica que eu queria era uma interface semelhante a um ListView, então eu queria ser capaz de inflar os pontos de vista em minha Fragment e adicioná-los à RecyclerView in onCreateView . Isto é feito através da criação de um HeaderViewRecyclerAdapter passando o adaptador para ser acondicionada, e chamando de addHeaderView e addFooterView que passam suas opiniões inflacionados. Em seguida, defina o HeaderViewRecyclerAdapter instância como o adaptador no RecyclerView .

Um requisito adicional foi que eu precisava para ser capaz de trocar facilmente fora adaptadores, mantendo os cabeçalhos e rodapés, eu não queria ter vários adaptadores com múltiplas instâncias desses cabeçalhos e rodapés. Assim, você pode chamar de setAdapter para mudar o adaptador envolto deixando os cabeçalhos e rodapés intacto, com o RecyclerView ser notificado sobre a alteração .

4mato @

Com base na solução de @ seb, eu criei uma subclasse de RecyclerView.Adapter que suporta um número arbitrário de cabeçalhos e rodapés.

https://gist.github.com/mheras/0908873267def75dc746

Embora pareça ser uma solução, eu também acho que essa coisa deve ser gerido pelo LayoutManager . Infelizmente, eu preciso dele agora e eu não tenho tempo para implementar uma StaggeredGridLayoutManager a partir do zero ( nem mesmo estender a partir dele ) .

Eu ainda estou testando, mas você pode experimentá-lo, se quiser. Por favor, deixe-me saber se você encontrar quaisquer problemas com ele.