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

122

Passei um momento tentando descobrir uma maneira de adicionar um cabeçalho a RecyclerView, sem êxito.

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

   ...
}

A LayoutManagerparece ser o objeto de manipulação a disposição dos RecyclerViewitens. Como eu não poderia encontrar qualquer addHeaderView(View view)método, eu decidi ir com o LayoutManager's addView(View view, int position)método e adicionar minha opinião cabeçalho na primeira posição para atuar como um cabeçalho.

E é aqui que as coisas ficam mais feias:

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 várias NullPointerExceptionstentativas de chamar o addView(View view)em diferentes momentos da criação da atividade (também tentei adicionar a visualização depois que tudo estiver configurado, até os dados do adaptador), percebi que não fazia ideia se esse é o caminho certo para fazê-lo (e não parece ser).

PS: Além disso, uma solução que poderia lidar com o GridLayoutManageralém do LinearLayoutManagerseria muito apreciada!

MathieuMaree
fonte
dê uma olhada neste stackoverflow.com/a/26573338/2127203
EC84B4
O problema está no código do adaptador. Isso significa que, de alguma forma, você está retornando um viewholder nulo na função onCreateViewHolder.
Neo
Existe uma boa maneira de adicionar cabeçalho ao StaggeredGridLayout stackoverflow.com/questions/42202735/…
Aleksey Timoshchenko

Respostas:

120

Eu tive que adicionar um rodapé ao meu RecyclerViewe aqui estou compartilhando meu snippet de código, pois achei que poderia ser útil. Verifique os comentários dentro do código para entender melhor o fluxo geral.

import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import java.util.ArrayList;

public class RecyclerViewWithFooterAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    private static final int FOOTER_VIEW = 1;
    private ArrayList<String> data; // Take any list that matches your requirement.
    private Context context;

    // Define a constructor
    public RecyclerViewWithFooterAdapter(Context context, ArrayList<String> data) {
        this.context = context;
        this.data = data;
    }

    // Define a ViewHolder for Footer view
    public class FooterViewHolder extends ViewHolder {
        public FooterViewHolder(View itemView) {
            super(itemView);
            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    // Do whatever you want on clicking the item
                }
            });
        }
    }

    // Now define the ViewHolder for Normal list item
    public class NormalViewHolder extends ViewHolder {
        public NormalViewHolder(View itemView) {
            super(itemView);

            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    // Do whatever you want on clicking the normal items
                }
            });
        }
    }

    // And now in onCreateViewHolder you have to pass the correct view
    // while populating the list item.

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

        View v;

        if (viewType == FOOTER_VIEW) {
            v = LayoutInflater.from(context).inflate(R.layout.list_item_footer, parent, false);
            FooterViewHolder vh = new FooterViewHolder(v);
            return vh;
        }

        v = LayoutInflater.from(context).inflate(R.layout.list_item_normal, parent, false);

        NormalViewHolder vh = new NormalViewHolder(v);

        return vh;
    }

    // Now bind the ViewHolder in onBindViewHolder
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {

        try {
            if (holder instanceof NormalViewHolder) {
                NormalViewHolder vh = (NormalViewHolder) holder;

                vh.bindView(position);
            } else if (holder instanceof FooterViewHolder) {
                FooterViewHolder vh = (FooterViewHolder) holder;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // Now the critical part. You have return the exact item count of your list
    // I've only one footer. So I returned data.size() + 1
    // If you've multiple headers and footers, you've to return total count
    // like, headers.size() + data.size() + footers.size()

    @Override
    public int getItemCount() {
        if (data == null) {
            return 0;
        }

        if (data.size() == 0) {
            //Return 1 here to show nothing
            return 1;
        }

        // Add extra view to show the footer view
        return data.size() + 1;
    }

    // Now define getItemViewType of your own.

    @Override
    public int getItemViewType(int position) {
        if (position == data.size()) {
            // This is where we'll add footer.
            return FOOTER_VIEW;
        }

        return super.getItemViewType(position);
    }

    // So you're done with adding a footer and its action on onClick.
    // Now set the default ViewHolder for NormalViewHolder

    public class ViewHolder extends RecyclerView.ViewHolder {
        // Define elements of a row here
        public ViewHolder(View itemView) {
            super(itemView);
            // Find view by ID and initialize here
        }

        public void bindView(int position) {
            // bindView() method to implement actions
        }
    }
}

O trecho de código acima adiciona um rodapé ao arquivo RecyclerView. Você pode verificar este repositório do GitHub para verificar a implementação de adicionar cabeçalho e rodapé.

Reaz Murshed
fonte
2
Funciona bem. Isso deve ser marcado como resposta correta.
Naga Mallesh Maddali
1
Foi exatamente o que eu fiz. Mas e se eu quisesse que o meu RecyclerView adaptasse a lista escalonada? O primeiro elemento (O cabeçalho) também será escalonado. :(
Neon Warge
Este é um tutorial sobre como você pode preencher seu RecyclerViewdinamicamente. Você pode ter controle sobre cada um dos elementos do seu RecyclerView. Por favor, olhe a seção de código para um projeto de trabalho. Espero que ajude. github.com/comeondude/dynamic-recyclerview/wiki
Reaz Murshed
2
int getItemViewType (int position)- Retorne o tipo de exibição do item na posição para fins de reciclagem de exibição. A implementação padrão desse método retorna 0, assumindo um único tipo de visualização para o adaptador. Diferentemente dos ListViewadaptadores, os tipos não precisam ser contíguos. Considere usar recursos de identificação para identificar exclusivamente os tipos de exibição de item. - Da documentação. developer.android.com/reference/android/support/v7/widget/…
Reaz Murshed
3
Tanto trabalho manual para algo que as pessoas sempre querem fazer. Eu não posso acreditar ...
JohnyTex
28

Muito simples de resolver !!

Eu não gosto de ter a lógica dentro do adaptador como um tipo de exibição diferente, porque toda vez que ele verifica o tipo de exibição antes de retornar a exibição. A solução abaixo evita verificações extras.

Basta adicionar a visualização de cabeçalho LinearLayout (vertical) + visualização de reciclagem + visualização de rodapé no android.support.v4.widget.NestedScrollView .

Veja isso:

 <android.support.v4.widget.NestedScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

       <View
            android:id="@+id/header"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>

        <android.support.v7.widget.RecyclerView
            android:id="@+id/list"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layoutManager="LinearLayoutManager"/>

        <View
            android:id="@+id/footer"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>
    </LinearLayout>
</android.support.v4.widget.NestedScrollView>

Adicione esta linha de código para rolagem suave

RecyclerView v = (RecyclerView) findViewById(...);
v.setNestedScrollingEnabled(false);

Isso perderá todo o desempenho do RV e o RV tentará exibir todos os detentores de visualização, independentemente layout_heightdo RV

Uso recomendado para a lista de tamanhos pequenos, como gaveta Nav ou configurações etc.

Nishant Shah
fonte
1
Trabalhou para mim bastante simples
Hitesh Sahu
1
Essa é uma maneira muito simples de adicionar cabeçalhos e rodapés a uma exibição do reciclador quando os cabeçalhos e rodapés não precisam ser repetidos em uma lista.
user1841702
34
Essa é uma maneira muito simples de perder todas as vantagens que RecyclerViewvocê traz - você perde a reciclagem real e a otimização que ela traz.
Marcin Koziński
1
Eu tentei este código, a rolagem não está funcionando corretamente ... tornou-se a rolagem muito lento .. plz sugerir se poderia fazer algo para que
Nibha Jain
2
o uso da rolagem aninhada fará com que a recyclerview armazene em cache toda a exibição, e se o tamanho da exibição do reciclador for maior, o tempo de carregamento e rolagem aumentará. Eu não sugiro usar este código
Tushar Saha
25

Eu tive o mesmo problema no Lollipop e criei duas abordagens para envolver o Recyclerviewadaptador. Um é bastante fácil de usar, mas não tenho certeza de como ele se comportará com um conjunto de dados alterado. Porque envolve seu adaptador e você precisa chamar métodos como notifyDataSetChangedo objeto-adaptador correto.

O outro não deve ter esses problemas. Apenas deixe seu adaptador regular estender a classe, implementar os métodos abstratos e você deverá estar pronto. E aqui estão eles:

gists

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

}

Comentários e garfos apreciados. Vou usar HeaderRecyclerViewAdapterV2por mim mesmo e evoluir, testar e publicar as mudanças no futuro.

EDIT : @OvidiuLatcu Sim, tive alguns problemas. Na verdade, parei de compensar implicitamente o cabeçalho position - (useHeader() ? 1 : 0)e, em vez disso, criei um método público int offsetPosition(int position)para ele. Como se você definir um OnItemTouchListenerna Reciclerview, poderá interceptar o toque, obter as coordenadas x, y do toque, encontrar a visão filho correspondente e, em seguida, ligar recyclerView.getChildPosition(...)e sempre obterá a posição não compensada no adaptador! Isso é uma falha no código do RecyclerView, não vejo um método fácil de superar isso. É por isso que agora compenso as posições explícitas quando preciso pelo meu próprio código.

seb
fonte
parece bom ! tendo algum problema com isso? ou podemos usá-lo com segurança? : D
Ovidiu Latcu
1
@OvidiuLatcu ver post
seb
Nessas implementações, parece que você assumiu que o número de cabeçalhos e rodapés era de apenas 1 cada?
rishabhmhjn
@seb Versão 2 funciona como charme !! a única coisa que eu precisava modificar é a condição para obter o rodapé nos métodos onBindViewHolder e getItemViewType. O problema é que, se você obtiver a posição usando position == getBasicItemCount (), ele não retornará true para a última posição real, mas para a última posição - 1. Acabou colocando o FooterView lá (não na parte inferior). Corrigimos a alteração da condição para position == getBasicItemCount () + 1 e funcionou muito bem!
mmark
@seb versão 2 funciona tão bem. Muito obrigado. eu estou usando isso. uma coisa pequena que sugiro é adicionar a palavra-chave 'final' para a função de substituição.
RyanShao
10

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

createViewHolderé passado o número inteiro do qual você retornou getItemViewType, permitindo que você crie ou configure o suporte da visualização de maneira diferente para a visualização do 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 da posição inteira passada para bindViewHolder.

Ian Newson
fonte
Eu não acho que esse seja o trabalho da reciclagem. O trabalho de Reciclerviews é simplesmente reciclar visualizações. Escrever um gerenciador de layout com uma implementação de cabeçalho ou rodapé é o caminho a seguir.
IZI_Shadow_IZI 19/10/2014
Obrigado @IanNewson pela sua resposta. Primeiro, esta solução parece estar funcionando, mesmo usando apenas getItemViewType(int position) { return position == 0 ? 0 : 1; }( RecyclerViewnão possui um getViewTypeCount()método). Por outro lado, concordo com @IZI_Shadow_IZI, realmente sinto que o LayoutManager deve ser o responsável por esse tipo de coisa. Alguma outra ideia?
MathieuMaree
@VieuMa, você provavelmente está certo, mas atualmente não sei como fazer isso e tinha certeza de que minha solução funcionaria. Uma solução abaixo do ideal é melhor que nenhuma solução, que é o que você tinha antes.
21714 Ian Newson
O @VieuMa também, abstrair o cabeçalho e o rodapé no adaptador significa que ele deve lidar com os dois tipos de layout solicitados, escrever seu próprio gerenciador de layout significa reimplementar os dois tipos de layout.
18713 Ian Newson
basta criar um adaptador que agrupe qualquer adaptador e inclua suporte para a exibição de cabeçalho no índice 0. O HeaderView no listview cria muitos casos de borda e o valor agregado é mínimo, pois é um problema fácil de resolver usando um adaptador de wrapper.
yigit
9

Você pode usar esta biblioteca do GitHub , permitindo adicionar cabeçalho e / ou rodapé no seu RecyclerView da maneira mais simples possível.

Você precisa adicionar a biblioteca HFRecyclerView ao seu projeto ou também pode obtê- la em Gradle:

compile 'com.mikhaellopez:hfrecyclerview:1.0.0'

Este é um resultado na imagem:

Pré-visualização

EDITAR:

Se você deseja adicionar uma margem na parte superior e / ou inferior desta biblioteca: SimpleItemDecoration :

int offsetPx = 10;
recyclerView.addItemDecoration(new StartOffsetItemDecoration(offsetPx));
recyclerView.addItemDecoration(new EndOffsetItemDecoration(offsetPx));
lopez.mikhael
fonte
Esta biblioteca adiciona a exibição corretamente no cabeçalho no LinearLayoutManager, mas quero definir a exibição como cabeçalho no GridLayoutManager, que ocorre em toda a largura da tela. É possível com esta biblioteca.
ved
Não, esta biblioteca permite alterar o primeiro e o último elemento de um recyclerView na adaptação (RecyclerView.Adapter). Você pode usar este adaptador para um GridView sem problemas. Então, acho que essa biblioteca permite fazer o que você deseja.
precisa saber é o seguinte
6

Acabei implementando meu próprio adaptador para agrupar qualquer outro adaptador e fornecer métodos para adicionar visualizações de cabeçalho e rodapé.

Criou uma essência aqui: HeaderViewRecyclerAdapter.java

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 no meu Fragmento e adicioná-los ao RecyclerViewno onCreateView. Isso é feito criando uma HeaderViewRecyclerAdapterpassagem do adaptador a ser empacotado e chamando addHeaderViewe addFooterViewpassando suas visualizações infladas. Em seguida, defina a HeaderViewRecyclerAdapterinstância como o adaptador no RecyclerView.

Um requisito extra era que eu precisava poder trocar facilmente adaptadores enquanto mantinha os cabeçalhos e rodapés. Eu não queria ter vários adaptadores com várias instâncias desses cabeçalhos e rodapés. Assim, você pode ligar setAdapterpara alterar o adaptador acondicionado, deixando os cabeçalhos e rodapés intactos, RecyclerViewsendo notificado da alteração.

darnmason
fonte
3

minha maneira "mantenha as coisas simples e estúpidas" ... desperdice alguns recursos, eu sei, mas não me importo, pois meu código é simples, então ... 1) adicione rodapé com visibilidade GONE ao seu item_layout

  <LinearLayout
        android:id="@+id/footer"
        android:layout_width="match_parent"
        android:layout_height="80dp"
        android:orientation="vertical"
        android:visibility="gone">
    </LinearLayout>

2) depois configure-o no último item

public void onBindViewHolder(ChannelAdapter.MyViewHolder holder, int position) {
        boolean last = position==data.size()-1;
        //....
        holder.footer.setVisibility(View.GONE);
        if (last && showFooter){
            holder.footer.setVisibility(View.VISIBLE);
        }
    }

faça o oposto do cabeçalho

Luca Rocchi
fonte
1

Com base na solução da @ seb, 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, também acho que isso deve ser gerenciado pelo LayoutManager. Infelizmente, eu preciso dele agora e não tenho tempo para implementar um StaggeredGridLayoutManager do zero (nem mesmo estender a partir dele).

Ainda estou testando, mas você pode experimentá-lo, se quiser. Entre em contato se encontrar algum problema.

mato
fonte
1

Você pode usar o viewtype para resolver esse problema. Aqui está minha demonstração: https://github.com/yefengfreedom/RecyclerViewWithHeaderFooterLoadingEmptyViewErrorView

  1. você pode definir o modo de exibição da exibição do reciclador:

    final estático público int MODE_DATA = 0, MODE_LOADING = 1, MODE_ERROR = 2, MODE_EMPTY = 3, MODE_HEADER_VIEW = 4, MODE_FOOTER_VIEW = 5;

2. substituir o método getItemViewType

 @Override
public int getItemViewType(int position) {
    if (mMode == RecyclerViewMode.MODE_LOADING) {
        return RecyclerViewMode.MODE_LOADING;
    }
    if (mMode == RecyclerViewMode.MODE_ERROR) {
        return RecyclerViewMode.MODE_ERROR;
    }
    if (mMode == RecyclerViewMode.MODE_EMPTY) {
        return RecyclerViewMode.MODE_EMPTY;
    }
    //check what type our position is, based on the assumption that the order is headers > items > footers
    if (position < mHeaders.size()) {
        return RecyclerViewMode.MODE_HEADER_VIEW;
    } else if (position >= mHeaders.size() + mData.size()) {
        return RecyclerViewMode.MODE_FOOTER_VIEW;
    }
    return RecyclerViewMode.MODE_DATA;
}

3. substitua o método getItemCount

@Override
public int getItemCount() {
    if (mMode == RecyclerViewMode.MODE_DATA) {
        return mData.size() + mHeaders.size() + mFooters.size();
    } else {
        return 1;
    }
}

4. substitua o método onCreateViewHolder. criar titular da vista por viewType

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    if (viewType == RecyclerViewMode.MODE_LOADING) {
        RecyclerView.ViewHolder loadingViewHolder = onCreateLoadingViewHolder(parent);
        loadingViewHolder.itemView.setLayoutParams(
                new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, parent.getHeight() - mToolBarHeight)
        );
        return loadingViewHolder;
    }
    if (viewType == RecyclerViewMode.MODE_ERROR) {
        RecyclerView.ViewHolder errorViewHolder = onCreateErrorViewHolder(parent);
        errorViewHolder.itemView.setLayoutParams(
                new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, parent.getHeight() - mToolBarHeight)
        );
        errorViewHolder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(final View v) {
                if (null != mOnErrorViewClickListener) {
                    new Handler().postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            mOnErrorViewClickListener.onErrorViewClick(v);
                        }
                    }, 200);
                }
            }
        });
        return errorViewHolder;
    }
    if (viewType == RecyclerViewMode.MODE_EMPTY) {
        RecyclerView.ViewHolder emptyViewHolder = onCreateEmptyViewHolder(parent);
        emptyViewHolder.itemView.setLayoutParams(
                new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, parent.getHeight() - mToolBarHeight)
        );
        emptyViewHolder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(final View v) {
                if (null != mOnEmptyViewClickListener) {
                    new Handler().postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            mOnEmptyViewClickListener.onEmptyViewClick(v);
                        }
                    }, 200);
                }
            }
        });
        return emptyViewHolder;
    }
    if (viewType == RecyclerViewMode.MODE_HEADER_VIEW) {
        RecyclerView.ViewHolder headerViewHolder = onCreateHeaderViewHolder(parent);
        headerViewHolder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(final View v) {
                if (null != mOnHeaderViewClickListener) {
                    new Handler().postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            mOnHeaderViewClickListener.onHeaderViewClick(v, v.getTag());
                        }
                    }, 200);
                }
            }
        });
        return headerViewHolder;
    }
    if (viewType == RecyclerViewMode.MODE_FOOTER_VIEW) {
        RecyclerView.ViewHolder footerViewHolder = onCreateFooterViewHolder(parent);
        footerViewHolder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(final View v) {
                if (null != mOnFooterViewClickListener) {
                    new Handler().postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            mOnFooterViewClickListener.onFooterViewClick(v, v.getTag());
                        }
                    }, 200);
                }
            }
        });
        return footerViewHolder;
    }
    RecyclerView.ViewHolder dataViewHolder = onCreateDataViewHolder(parent);
    dataViewHolder.itemView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(final View v) {
            if (null != mOnItemClickListener) {
                new Handler().postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        mOnItemClickListener.onItemClick(v, v.getTag());
                    }
                }, 200);
            }
        }
    });
    dataViewHolder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
        @Override
        public boolean onLongClick(final View v) {
            if (null != mOnItemLongClickListener) {
                new Handler().postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        mOnItemLongClickListener.onItemLongClick(v, v.getTag());
                    }
                }, 200);
                return true;
            }
            return false;
        }
    });
    return dataViewHolder;
}

5. Substitua o método onBindViewHolder. vincular dados por viewType

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    if (mMode == RecyclerViewMode.MODE_LOADING) {
        onBindLoadingViewHolder(holder, position);
    } else if (mMode == RecyclerViewMode.MODE_ERROR) {
        onBindErrorViewHolder(holder, position);
    } else if (mMode == RecyclerViewMode.MODE_EMPTY) {
        onBindEmptyViewHolder(holder, position);
    } else {
        if (position < mHeaders.size()) {
            if (mHeaders.size() > 0) {
                onBindHeaderViewHolder(holder, position);
            }
        } else if (position >= mHeaders.size() + mData.size()) {
            if (mFooters.size() > 0) {
                onBindFooterViewHolder(holder, position - mHeaders.size() - mData.size());
            }
        } else {
            onBindDataViewHolder(holder, position - mHeaders.size());
        }
    }
}
yefeng
fonte
e se o seu link for quebrado no futuro?
Gopal Singh Sirvi
Boa pergunta. Vou editar minha resposta e postar meu código aqui.
yefeng
1

Você pode usar a biblioteca SectionedRecyclerViewAdapter para agrupar seus itens em seções e adicionar um cabeçalho a cada seção, como na imagem abaixo:

insira a descrição da imagem aqui

Primeiro você cria sua classe de seção:

class MySection extends StatelessSection {

    String title;
    List<String> list;

    public MySection(String title, List<String> list) {
        // call constructor with layout resources for this Section header, footer and items 
        super(R.layout.section_header, R.layout.section_item);

        this.title = title;
        this.list = list;
    }

    @Override
    public int getContentItemsTotal() {
        return list.size(); // number of items of this section
    }

    @Override
    public RecyclerView.ViewHolder getItemViewHolder(View view) {
        // return a custom instance of ViewHolder for the items of this section
        return new MyItemViewHolder(view);
    }

    @Override
    public void onBindItemViewHolder(RecyclerView.ViewHolder holder, int position) {
        MyItemViewHolder itemHolder = (MyItemViewHolder) holder;

        // bind your view here
        itemHolder.tvItem.setText(list.get(position));
    }

    @Override
    public RecyclerView.ViewHolder getHeaderViewHolder(View view) {
        return new SimpleHeaderViewHolder(view);
    }

    @Override
    public void onBindHeaderViewHolder(RecyclerView.ViewHolder holder) {
        MyHeaderViewHolder headerHolder = (MyHeaderViewHolder) holder;

        // bind your header view here
        headerHolder.tvItem.setText(title);
    }
}

Em seguida, você configura o RecyclerView com suas seções e altera o SpanSize dos cabeçalhos com um GridLayoutManager:

// Create an instance of SectionedRecyclerViewAdapter 
SectionedRecyclerViewAdapter sectionAdapter = new SectionedRecyclerViewAdapter();

// Create your sections with the list of data
MySection section1 = new MySection("My Section 1 title", dataList1);
MySection section2 = new MySection("My Section 2 title", dataList2);

// Add your Sections to the adapter
sectionAdapter.addSection(section1);
sectionAdapter.addSection(section2);

// Set up a GridLayoutManager to change the SpanSize of the header
GridLayoutManager glm = new GridLayoutManager(getContext(), 2);
glm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
    @Override
    public int getSpanSize(int position) {
        switch(sectionAdapter.getSectionItemViewType(position)) {
            case SectionedRecyclerViewAdapter.VIEW_TYPE_HEADER:
                return 2;
            default:
                return 1;
        }
    }
});

// Set up your RecyclerView with the SectionedRecyclerViewAdapter
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerview);
recyclerView.setLayoutManager(glm);
recyclerView.setAdapter(sectionAdapter);
Gustavo
fonte
0

Sei que cheguei atrasado, mas apenas recentemente consegui implementar esse "addHeader" no adaptador. No meu projeto FlexibleAdapter , você pode chamar setHeaderum item seccionável e depois chamarshowAllHeaders . Se você precisar de apenas 1 cabeçalho, o primeiro item deverá ter o cabeçalho. Se você excluir este item, o cabeçalho será automaticamente vinculado ao próximo.

Infelizmente, os rodapés ainda não estão cobertos.

O FlexibleAdapter permite fazer muito mais do que criar cabeçalhos / seções. Você realmente deve dar uma olhada: https://github.com/davideas/FlexibleAdapter .

Davideas
fonte
0

Gostaria apenas de adicionar uma alternativa a todas as implementações do HeaderRecyclerViewAdapter. CompoundAdapter:

https://github.com/negusoft/CompoundAdapter-android

É uma abordagem mais flexível, pois você pode criar um AdapterGroup a partir de adaptadores. Para o exemplo do cabeçalho, use seu adaptador como está, juntamente com um adaptador que contém um item para o cabeçalho:

AdapterGroup adapterGroup = new AdapterGroup();
adapterGroup.addAdapter(SingleAdapter.create(R.layout.header));
adapterGroup.addAdapter(new MyAdapter(...));

recyclerView.setAdapter(adapterGroup);

É bastante simples e legível. Você pode implementar facilmente um adaptador mais complexo usando o mesmo princípio.

blurkidi
fonte