Android 5.0 - Aggiungere intestazione / piè di pagina di un RecyclerView

? MathieuMaree @ | Original: StackOverFlow
---

Ho passato un momento cercando di capire un modo per aggiungere un'intestazione a una RecyclerView, senza successo . Questo è quello che ho ottenuto finora :

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

   ...
}

Il LayoutManager sembra essere l'oggetto movimentazione la disposizione degli elementi RecyclerView . Come io non riuscivo a trovare un metodo addHeaderView(View view), ho deciso di andare con il metodo addView(View view, int position) del LayoutManager e di aggiungere la mia visione di intestazione in prima posizione per agire come un colpo di testa .

Aaand è qui che le cose si fanno più brutto :

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)

Dopo aver ottenuto diversi NullPointerExceptions cercando di chiamare il addView(View view) in diversi momenti della creazione di attività ( anche provato ad aggiungere la vista una volta che tutto è impostato, anche i dati della scheda ), mi sono reso conto ho idea se questo è il modo giusto per farlo ( e non sembra essere) .

Qualcuno potrebbe aiutarmi ? O almeno darmi qualche idea / nuova direzione da esplorare ?

Grazie in anticipo!

VieuMa

PS : Inoltre, una soluzione in grado di gestire il GridLayoutManager oltre al LinearLayoutManager sarebbe molto apprezzato !

---

Top 5 Risposta

1Ian Newson @

Non ho provato questo, ma vorrei semplicemente aggiungere 1 ( o 2, se si desidera utilizzare entrambi intestazione e piè di pagina) al numero intero restituito da GetItemCount nella scheda . È quindi possibile ignorare getItemViewType nel vostro adattatore per restituire un intero diverso quando i==0 : https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html#getItemViewType(int)

createViewHolder Viene quindi superato il numero intero tornati dal getItemViewType, che permette di creare o configurare il titolare vista in modo diverso per la visualizzazione dell'intestazione : https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html#createViewHolder(android.view.ViewGroup, Int )

Non dimenticare di sottrarre uno dalla posizione di numero intero passato a bindViewHolder .

2seb @

Ho avuto lo stesso problema e ha creato due approcci per avvolgere l'adattatore . Uno è abbastanza facile da usare, ma non sono sicuro di come si comporterà con un set di dati che cambia. Perché avvolge la scheda ed è necessario farsi assicurarsi di chiamare metodi come notifyDataSetChanged sull'adattatore - oggetto giusto .

L'altro non dovrebbe avere tali problemi . Lasciate la scheda regolare estendere la classe, implementare i metodi astratti e si dovrebbe essere pronti . Ed eccoli qui :

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 forcelle apprezzato . Userò HeaderRecyclerViewAdapterV2 di me stesso e di evolversi, di prova e dopo i cambiamenti in futuro .

EDIT : OvidiuLatcu Sì ho avuto qualche problema . In realtà mi sono fermato compensare l' intestazione implicitamente da position - (useHeader() ? 1 : 0) e invece creato un metodo pubblico int offsetPosition(int position) per esso . Perché se si imposta un OnItemTouchListener a Recyclerview, è possibile intercettare il tocco, ottenere le coordinate x, y del tocco, trovare la vista del bambino in base e quindi chiamare recyclerView.getChildPosition(...) e otterrete sempre la posizione di non- offsetted nel adattatore ! Si tratta di una shortcomming nel Codice RecyclerView, non vedo un metodo semplice per superare questo . È per questo che ora compensato la posizioni esplicite quando ho bisogno di dal mio stesso codice.

3darnmason @

Ho finito per attuare il mio adattatore per avvolgere qualsiasi altro adattatore e di fornire metodi per aggiungere viste di intestazione e piè di pagina .

Creato un gist qui : https://gist.github.com/darnmason/7bbf8beae24fe7296c8a

La caratteristica principale che volevo era una interfaccia simile a un ListView, così ho voluto essere in grado di gonfiare i punti di vista nel mio Frammento e aggiungerli alla RecyclerView in onCreateView . Questo viene fatto attraverso la creazione di un HeaderViewRecyclerAdapter passare l'adattatore da avvolgere, e chiamando addHeaderView e addFooterView che passano le vostre opinioni gonfiati . Quindi impostare il HeaderViewRecyclerAdapter istanza come scheda sul RecyclerView .

Un requisito extra era che avevo bisogno di essere in grado di scambiare facilmente fuori adattatori mantenendo le intestazioni e piè di pagina, non volevo avere più schede con più istanze di queste intestazioni e piè di pagina . Così si può chiamare setAdapter per modificare la scheda avvolta lasciando intatte le intestazioni e piè di pagina, con la RecyclerView dalla notifica del cambiamento .

4mato @

Sulla base di soluzione @ di seb, ho creato una sottoclasse di RecyclerView.Adapter che supporta un numero arbitrario di intestazioni e piè di pagina .

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

Anche se sembra essere una soluzione, credo che anche questa cosa dovrebbe essere gestito dalla LayoutManager . Purtroppo, ho bisogno adesso e non ho il tempo per implementare un StaggeredGridLayoutManager da zero ( e nemmeno estendono da esso ) .

Sto ancora provarla, ma si può provare se volete . Per favore fatemi sapere se trovate problemi con esso .