Android 5.0 - Dodaj nagłówek / stopkę do RecyclerView

123

Spędziłem chwilę, próbując znaleźć sposób na dodanie nagłówka do a RecyclerView, ale bezskutecznie.

Oto, co osiągnąłem do tej pory:

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

   ...
}

LayoutManagerWydaje się być przedmiotem obchodzenia się rozmieszczenie RecyclerViewelementów. Ponieważ nie mogłem znaleźć żadnej addHeaderView(View view)metody, zdecydowałem się skorzystać z metody LayoutManager'' addView(View view, int position)i dodać mój widok nagłówka na pierwszej pozycji, aby działał jako nagłówek.

Aa i tutaj sprawy stają się brzydsze:

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)

Po kilku NullPointerExceptionspróbach wywołania addView(View view)w różnych momentach tworzenia działania (próbowałem również dodać widok, gdy wszystko jest skonfigurowane, nawet dane adaptera), zdałem sobie sprawę, że nie mam pojęcia, czy jest to właściwy sposób (i to nie wygląda).

PS: Również rozwiązanie, które poradziłoby sobie z tym GridLayoutManageroprócz tego, LinearLayoutManagerbyłoby naprawdę mile widziane!

MathieuMaree
źródło
spójrz na ten stackoverflow.com/a/26573338/2127203
EC84B4
Problem tkwi w kodzie adaptera. Oznacza to, że w jakiś sposób zwracasz null viewholder w funkcji onCreateViewHolder.
Neo
Istnieje dobry sposób na dodanie nagłówka do StaggeredGridLayout stackoverflow.com/questions/42202735/ ...
Aleksey Timoshchenko

Odpowiedzi:

121

Musiałem dodać stopkę do mojego RecyclerViewi tutaj udostępniam mój fragment kodu, ponieważ pomyślałem, że może być przydatny. Sprawdź komentarze w kodzie, aby lepiej zrozumieć ogólny przepływ.

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
        }
    }
}

Powyższy fragment kodu dodaje stopkę do pliku RecyclerView. Możesz sprawdzić to repozytorium GitHub, aby sprawdzić implementację dodawania nagłówka i stopki.

Reaz Murshed
źródło
2
Działa dobrze. Powinno to być oznaczone jako poprawna odpowiedź.
Naga Mallesh Maddali
1
To jest dokładnie to, co zrobiłem. Ale co jeśli chcę, aby mój RecyclerView dostosowywał listę naprzemienną? Pierwszy element (nagłówek) również zostanie przesunięty. :(
Neon Warge,
To jest samouczek o tym, jak możesz RecyclerViewdynamicznie wypełniać swój plik . Możesz mieć kontrolę nad każdym z elementów swojego pliku RecyclerView. Proszę spojrzeć na sekcję kodu dla działającego projektu. Mam nadzieję, że to pomoże. github.com/comeondude/dynamic-recyclerview/wiki
Reaz Murshed
2
int getItemViewType (int position)- Zwróć typ widoku elementu na miejscu w celu ponownego wyświetlenia. Domyślna implementacja tej metody zwraca 0, przy założeniu jednego typu widoku dla adaptera. W przeciwieństwie do ListViewadapterów typy nie muszą być ciągłe. Rozważ użycie zasobów id, aby jednoznacznie identyfikować typy widoków pozycji. - Z dokumentacji. developer.android.com/reference/android/support/v7/widget/…
Reaz Murshed
3
Tyle ręcznej pracy na coś, co ludzie zawsze chcą robić. Nie mogę w to uwierzyć ...
JohnyTex
28

Bardzo proste do rozwiązania !!

Nie podoba mi się pomysł posiadania logiki wewnątrz adaptera jako innego typu widoku, ponieważ za każdym razem sprawdza on typ widoku przed zwróceniem widoku. Poniższe rozwiązanie pozwala uniknąć dodatkowych kontroli.

Po prostu dodaj widok nagłówka LinearLayout (pionowy) + recykling + widok stopki wewnątrz android.support.v4.widget.NestedScrollView .

Sprawdź to:

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

Dodaj tę linię kodu, aby płynnie przewijać

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

Spowoduje to utratę wydajności kampera, a kamper będzie próbował rozłożyć wszystkich posiadaczy widoku, niezależnie layout_heightod kampera

Zalecane w przypadku małych list rozmiarów, takich jak szuflada Nav lub ustawienia itp.

Nishant Shah
źródło
1
Działało dla mnie dość prosto
Hitesh Sahu
1
Jest to bardzo prosty sposób dodawania nagłówków i stopek do widoku recyklera, gdy nagłówki i stopki nie muszą się powtarzać na liście.
user1841702,
34
To bardzo prosty sposób na utratę wszystkich korzyści, jakie RecyclerViewniesie ze sobą - tracisz rzeczywisty recykling i optymalizację, jaką przynosi.
Marcin Koziński
1
Wypróbowałem ten kod, przewijanie nie działa prawidłowo ... przewijanie stało się zbyt wolne .. proszę sugerować, czy mógłbym coś z tym zrobić
Nibha Jain
2
użycie zagnieżdżonego widoku przewijania spowoduje, że recykling będzie buforował cały widok, a jeśli rozmiar widoku recyklera będzie bardziej przewijany, a czas ładowania wzrośnie. Radzę nie używać tego kodu
Tushar Saha
25

Miałem ten sam problem na Lollipopie i stworzyłem dwa podejścia do owijania Recyclerviewadaptera. Jeden jest dość łatwy w użyciu, ale nie jestem pewien, jak będzie się zachowywał przy zmieniającym się zestawie danych. Ponieważ opakowuje on twój adapter i musisz upewnić się, że wywołujesz metody takie jak notifyDataSetChangedw odpowiednim obiekcie adaptera.

Drugi nie powinien mieć takich problemów. Po prostu pozwól zwykłemu adapterowi rozszerzyć klasę, zaimplementuj metody abstrakcyjne i powinieneś być gotowy. A oto one:

istoty

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

}

Mile widziane opinie i widelce. Będę używać HeaderRecyclerViewAdapterV2samodzielnie i ewoluować, testować i publikować zmiany w przyszłości.

EDYCJA : @OvidiuLatcu Tak, miałem pewne problemy. Właściwie przestałem niejawnie kompensować Header position - (useHeader() ? 1 : 0)i zamiast tego utworzyłem int offsetPosition(int position)dla niego metodę publiczną . Ponieważ jeśli ustawisz OnItemTouchListenerna Recyclerview, możesz przechwycić dotyk, uzyskać współrzędne x, y dotyku, znaleźć odpowiedni widok dziecka, a następnie zadzwonić, recyclerView.getChildPosition(...)a zawsze otrzymasz nieprzesuniętą pozycję w adapterze! To jest niedociągnięcie w kodzie RecyclerView, nie widzę łatwej metody na pokonanie tego. Dlatego teraz ustawiam pozycje wyraźnie, gdy jest to konieczne, za pomocą własnego kodu.

seb
źródło
wygląda dobrze ! masz z tym jakieś kłopoty? czy możemy go bezpiecznie używać? : D
Ovidiu Latcu
1
@OvidiuLatcu zobacz post
seb
Wydaje się, że w tych implementacjach założyłeś, że liczba nagłówków i stopek wynosi tylko 1?
rishabhmhjn
@seb wersja 2 działa jak urok !! Jedyną rzeczą, którą musiałem zmodyfikować, jest warunek uzyskania stopki w obu metodach onBindViewHolder i getItemViewType. Problem polega na tym, że jeśli uzyskasz pozycję za pomocą position == getBasicItemCount (), nie zwróci ona wartości true dla aktualnej ostatniej pozycji, ale ostatniej pozycji - 1. Skończyło się na umieszczeniu tam FooterView (nie na dole). Naprawiliśmy to, zmieniając warunek na position == getBasicItemCount () + 1 i działało świetnie!
mmark
@seb w wersji 2 działa świetnie. Dzięki wielkie. używam tego. jedną małą rzeczą, którą sugeruję, jest dodanie słowa kluczowego „final” dla funkcji override.
RyanShao
10

Nie próbowałem tego, ale po prostu dodam 1 (lub 2, jeśli chcesz, aby zarówno nagłówek, jak i stopka) do liczby całkowitej zwróconej przez getItemCount w twoim adapterze. Następnie możesz nadpisać getItemViewTypew adapterze, aby zwrócić inną liczbę całkowitą, gdy i==0: https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html#getItemViewType(int)

createViewHolderjest następnie przekazywana liczba całkowita, z której zwrócono getItemViewType, co pozwala na utworzenie lub skonfigurowanie uchwytu widoku w inny sposób dla widoku nagłówka: https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html# createViewHolder (android.view.ViewGroup , int)

Nie zapomnij odjąć jednego z przekazanej liczby całkowitej pozycji bindViewHolder.

Ian Newson
źródło
Nie sądzę, że to zadanie przeglądu recyklingu. Zadaniem Recyclerviews jest po prostu ponowne wykorzystanie widoków. Najlepszym sposobem jest napisanie menedżera układu z implementacją nagłówka lub stopki.
IZI_Shadow_IZI
Dzięki @IanNewson za odpowiedź. Po pierwsze, wydaje się, że to rozwiązanie działa, nawet używając tylko getItemViewType(int position) { return position == 0 ? 0 : 1; }( RecyclerViewnie ma getViewTypeCount()metody). Z drugiej strony zgadzam się z @IZI_Shadow_IZI, naprawdę mam wrażenie, że LayoutManager powinien zajmować się tego rodzaju rzeczami. Masz inny pomysł?
MathieuMaree
@VieuMa prawdopodobnie oboje macie rację, ale obecnie nie wiem, jak to zrobić i byłem prawie pewien, że moje rozwiązanie zadziała. Nieoptymalne rozwiązanie jest lepsze niż brak rozwiązania, czyli to, co miałeś wcześniej.
Ian Newson
@VieuMa również, abstrahowanie nagłówka i stopki w adapterze oznacza, że ​​powinien on obsługiwać oba typy żądanych układów, a napisanie własnego menedżera układu oznacza ponowne zaimplementowanie obu typów układu.
Ian Newson
wystarczy utworzyć adapter opakowujący dowolny adapter i dodający obsługę widoku nagłówka pod indeksem 0. HeaderView w widoku listy tworzy wiele przypadków brzegowych, a wartość dodana jest minimalna, biorąc pod uwagę, że jest to łatwy problem do rozwiązania za pomocą adaptera opakowującego.
yigit
9

Możesz użyć tej biblioteki GitHub, która umożliwia dodanie nagłówka i / lub stopki w RecyclerView w najprostszy możliwy sposób.

Musisz dodać bibliotekę HFRecyclerView do swojego projektu lub możesz ją również pobrać z Gradle:

compile 'com.mikhaellopez:hfrecyclerview:1.0.0'

To jest wynik na obrazie:

Zapowiedź

EDYTOWAĆ:

Jeśli chcesz tylko dodać margines na górze i / lub na dole za pomocą tej biblioteki: SimpleItemDecoration :

int offsetPx = 10;
recyclerView.addItemDecoration(new StartOffsetItemDecoration(offsetPx));
recyclerView.addItemDecoration(new EndOffsetItemDecoration(offsetPx));
lopez.mikhael
źródło
Ta biblioteka poprawnie dodaje widok do nagłówka w LinearLayoutManager, ale chcę ustawić widok jako nagłówek w GridLayoutManager, które zajmują całą szerokość ekranu. Czy to możliwe dzięki tej bibliotece.
ved
Nie, ta biblioteka umożliwia zmianę pierwszego i ostatniego elementu recyklingu podczas dostosowywania (RecyclerView.Adapter). Możesz używać tego adaptera do widoku GridView bez problemów. Myślę więc, że ta biblioteka pozwala robić, co chcesz.
lopez.mikhael
6

Skończyło się na zaimplementowaniu własnego adaptera, aby opakować dowolny inny adapter i zapewnić metody dodawania widoków nagłówka i stopki.

Utworzono streszczenie tutaj: HeaderViewRecyclerAdapter.java

Główną funkcją, której potrzebowałem, był podobny interfejs do ListView, więc chciałem mieć możliwość zawyżania widoków w moim fragmencie i dodawania ich do pliku RecyclerViewin onCreateView. Odbywa się to poprzez utworzenie HeaderViewRecyclerAdapterprzekazującego adapter, który ma zostać opakowany, oraz wywołanie addHeaderViewi addFooterViewprzekazanie zawyżonych widoków. Następnie ustaw HeaderViewRecyclerAdapterinstancję jako adapter w RecyclerView.

Dodatkowym wymaganiem było to, że musiałem mieć możliwość łatwej wymiany adapterów, zachowując nagłówki i stopki. Nie chciałem mieć wielu adapterów z wieloma wystąpieniami tych nagłówków i stopek. Możesz więc wywołać setAdapterzmianę opakowanego adaptera, pozostawiając nienaruszone nagłówki i stopki, z RecyclerViewpowiadomieniem o zmianie.

darnmason
źródło
3

mój sposób na proste, głupie ... marnuje trochę zasobów, wiem, ale nie obchodzi mnie to, ponieważ mój kod jest prosty, więc ... 1) dodaj stopkę z widocznością GONE do twojego item_layout

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

2), a następnie ustaw go jako widoczny na ostatniej pozycji

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

zrób odwrotnie dla nagłówka

Luca Rocchi
źródło
1

W oparciu o rozwiązanie @ seb stworzyłem podklasę RecyclerView.Adapter, która obsługuje dowolną liczbę nagłówków i stopek.

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

Chociaż wydaje się to rozwiązaniem, uważam również, że tym rozwiązaniem powinien zarządzać LayoutManager. Niestety, potrzebuję go teraz i nie mam czasu na implementację StaggeredGridLayoutManager od podstaw (ani nawet rozszerzanie z niego).

Wciąż to testuję, ale możesz to wypróbować, jeśli chcesz. Daj mi znać, jeśli znajdziesz z tym jakieś problemy.

mato
źródło
1

Możesz użyć viewtype, aby rozwiązać ten problem, oto moje demo: https://github.com/yefengfreedom/RecyclerViewWithHeaderFooterLoadingEmptyViewErrorView

  1. możesz zdefiniować tryb wyświetlania widoku recyklera:

    publiczne statyczne końcowe int MODE_DATA = 0, MODE_LOADING = 1, MODE_ERROR = 2, MODE_EMPTY = 3, MODE_HEADER_VIEW = 4, MODE_FOOTER_VIEW = 5;

2. zastąpić mothod 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. zastąpić metodę getItemCount

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

4. zastąpić metodę onCreateViewHolder. utwórz uchwyt widoku według 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. Zastąp metodę onBindViewHolder. powiązać dane według typu widoku

@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
źródło
co, jeśli w przyszłości twój link zostanie uszkodzony?
Gopal Singh Sirvi
Dobre pytanie,
poprawię
1

Możesz użyć biblioteki SectionedRecyclerViewAdapter, aby pogrupować elementy w sekcje i dodać nagłówek do każdej sekcji, jak na poniższym obrazku:

wprowadź opis obrazu tutaj

Najpierw utwórz klasę sekcji:

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

Następnie skonfiguruj RecyclerView ze swoimi sekcjami i zmień SpanSize nagłówków za pomocą 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
źródło
0

Wiem, że się spóźniam, ale dopiero niedawno udało mi się zaimplementować taki „addHeader” do Adaptora. W moim FlexibleAdapter projektu można zadzwonić setHeaderna Sectionable elementu, a następnie zadzwonić showAllHeaders. Jeśli potrzebujesz tylko 1 nagłówka, pierwszy element powinien mieć nagłówek. Jeśli usuniesz ten element, nagłówek zostanie automatycznie połączony z następnym.

Niestety stopki nie są (jeszcze) objęte.

FlexibleAdapter pozwala na znacznie więcej niż tworzenie nagłówków / sekcji. Naprawdę powinieneś rzucić okiem: https://github.com/davideas/F flexibleAdapter .

Davideas
źródło
0

Po prostu dodałbym alternatywę do wszystkich tych implementacji HeaderRecyclerViewAdapter. CompoundAdapter:

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

Jest to bardziej elastyczne podejście, ponieważ można utworzyć grupę adapterów z adapterów. W przykładzie z nagłówkiem użyj adaptera w takiej postaci, w jakiej jest, wraz z adapterem zawierającym jeden element dla nagłówka:

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

recyclerView.setAdapter(adapterGroup);

Jest dość prosty i czytelny. Na tej samej zasadzie można łatwo zaimplementować bardziej złożony adapter.

blurkidi
źródło
0

recyclerview:1.2.0wprowadza klasę ConcatAdapter, która łączy wiele adapterów w jeden. Pozwala więc na tworzenie oddzielnych adapterów nagłówka / stopki i ponowne wykorzystanie ich na wielu listach.

myRecyclerView.adapter = ConcatAdapter(headerAdapter, listAdapter, footerAdapter)

Przeczytaj artykuł z ogłoszeniem . Zawiera przykład, jak wyświetlić postęp ładowania w nagłówku i stopce za pomocą ConcatAdapter.

Na razie kiedy piszę tę odpowiedź wersja 1.2.0biblioteki jest w fazie alfa, więc api może się zmienić. Możesz sprawdzić status tutaj .

Valeriy Katkov
źródło