Android Recyclerview GridLayoutManager odstępy między kolumnami

251

Jak ustawić odstępy między kolumnami za pomocą RecyclerView za pomocą GridLayoutManager? Ustawienie marginesu / wypełnienia w moim układzie nie ma wpływu.

autostop. zjednoczony
źródło
Czy próbowałeś podklasowania GridLayoutManageri zastąpienia generateDefaultLayoutParams()i krewnych?
CommonsWare
Nie, myślałem, że istnieje metoda, której po prostu nie widziałem, aby ustawić odstępy w podobny sposób do widoku siatki. Spróbuję tego
hitch.united

Odpowiedzi:

348

RecyclerViews obsługuje koncepcję ItemDecoration : specjalne przesunięcia i rysowanie wokół każdego elementu. Jak widać w tej odpowiedzi , możesz użyć

public class SpacesItemDecoration extends RecyclerView.ItemDecoration {
  private int space;

  public SpacesItemDecoration(int space) {
    this.space = space;
  }

  @Override
  public void getItemOffsets(Rect outRect, View view, 
      RecyclerView parent, RecyclerView.State state) {
    outRect.left = space;
    outRect.right = space;
    outRect.bottom = space;

    // Add top margin only for the first item to avoid double space between items
    if (parent.getChildLayoutPosition(view) == 0) {
        outRect.top = space;
    } else {
        outRect.top = 0;
    }
  }
}

Następnie dodaj to przez

mRecyclerView = (RecyclerView) rootView.findViewById(R.id.my_recycler_view);
int spacingInPixels = getResources().getDimensionPixelSize(R.dimen.spacing);
mRecyclerView.addItemDecoration(new SpacesItemDecoration(spacingInPixels));
ianhanniballake
źródło
3
Użyj „outRect.top = spacja” i usuń „outRect.bottom”, jeśli nie chcesz zadzierać z „if for the first position”. ; -]
Amio.io
2
@zatziky - tak, jeśli już używasz górnego i dolnego wypełnienia jako części RecyclerView(i używasz clipToPadding="false"), możesz nieznacznie zmienić strukturę. Jeśli tego nie zrobisz, po prostu przesuń czek if, aby był ostatnim razem (ponieważ nadal chcesz dolne wypełnienie ostatniego elementu).
ianhanniballake
18
@ianhanniballake, chociaż działa to w przypadku korzystania z menedżera układu pojedynczego zakresu, nie działa w przypadku menedżera układu wieloprzęsłowego.
Avinash R
4
Jeśli zrobisz to w ten sposób za pomocą GridLayoutManager - wszystkie pierwsze elementy 2., 3. ... n-tej kolumny zostaną przyklejone do góry (ponieważ nie ma spacji). Więc myślę, że lepiej jest zrobić .top = spacja / 2 i .bottom = spacja / 2.
Yaroslav
1
Ta odpowiedź nie odpowiada na pierwotne pytanie. Nacisk kładziony jest na pytanie GridLayoutManager . Odpowiedź nie będzie działać na układach wielokolumnowych / wierszowych
19.19
428

Poniższy kod działa dobrze, a każda kolumna ma tę samą szerokość:

public class GridSpacingItemDecoration extends RecyclerView.ItemDecoration {

    private int spanCount;
    private int spacing;
    private boolean includeEdge;

    public GridSpacingItemDecoration(int spanCount, int spacing, boolean includeEdge) {
        this.spanCount = spanCount;
        this.spacing = spacing;
        this.includeEdge = includeEdge;
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        int position = parent.getChildAdapterPosition(view); // item position
        int column = position % spanCount; // item column

        if (includeEdge) {
            outRect.left = spacing - column * spacing / spanCount; // spacing - column * ((1f / spanCount) * spacing)
            outRect.right = (column + 1) * spacing / spanCount; // (column + 1) * ((1f / spanCount) * spacing)

            if (position < spanCount) { // top edge
                outRect.top = spacing;
            }
            outRect.bottom = spacing; // item bottom
        } else {
            outRect.left = column * spacing / spanCount; // column * ((1f / spanCount) * spacing)
            outRect.right = spacing - (column + 1) * spacing / spanCount; // spacing - (column + 1) * ((1f /    spanCount) * spacing)
            if (position >= spanCount) {
                outRect.top = spacing; // item top
            }
        }
    }
}

Stosowanie

1. bez krawędzi

wprowadź opis zdjęcia tutaj

int spanCount = 3; // 3 columns
int spacing = 50; // 50px
boolean includeEdge = false;
recyclerView.addItemDecoration(new GridSpacingItemDecoration(spanCount, spacing, includeEdge));

2. z krawędzią

wprowadź opis zdjęcia tutaj

int spanCount = 3; // 3 columns
int spacing = 50; // 50px
boolean includeEdge = true;
recyclerView.addItemDecoration(new GridSpacingItemDecoration(spanCount, spacing, includeEdge));
edwardaa
źródło
12
Działa, chyba że masz przedmioty o różnych rozpiętościach, takie jak nagłówki.
Matthew
8
Świetna odpowiedź; jedna wskazówka: odstępy są w pikselach (więc możesz przekonwertować dp na px za pomocą Math.round (someDpValue * getResources (). getDisplayMetrics (). gęstość))
Kevin Lee
1
Działa dobrze, ale mam problem, używam GridLayoutManager z spanCount 2 (domyślnie), ale użytkownik może zmienić spanCount, więc gdy spanCount zmienia się z pozycji domyślnej, na niektórych pozycjach jest znacznie bardziej widoczne wypełnienie, np. Jeśli spanCount będzie 3 niż wypełnienie / margines na 2,3 8,9 12,13 itd.
Haris Qureshi
2
Działa świetnie! Ale mam pewne problemy ze StaggeredGridLayoutManager. imgur.com/XVutH5u poziomy marginesy bywają różne .
Ufkoku,
2
To nie działa, gdy układ jest w formacie rtl (dla 2 kolumn lub więcej). obecnie spacja między kolumnami jest nieprawidłowa w trybie RTL. musisz wymienić: outRect.left na outRect.right, gdy jest w rtl.
Masoud Mohammadi
83

Poniżej znajduje się proste rozwiązanie krok po kroku, jeśli chcesz uzyskać równe odstępy wokół przedmiotów i równe rozmiary przedmiotów.

ItemOffsetDecoration

public class ItemOffsetDecoration extends RecyclerView.ItemDecoration {

    private int mItemOffset;

    public ItemOffsetDecoration(int itemOffset) {
        mItemOffset = itemOffset;
    }

    public ItemOffsetDecoration(@NonNull Context context, @DimenRes int itemOffsetId) {
        this(context.getResources().getDimensionPixelSize(itemOffsetId));
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
            RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        outRect.set(mItemOffset, mItemOffset, mItemOffset, mItemOffset);
    }
}

Realizacja

W kodzie źródłowym wartość dodana ItemOffsetDecorationdo RecyclerView. przesunięcia elementu powinna być o połowę mniejsza od rzeczywistej wartości, którą chcesz dodać jako spację między elementami.

mRecyclerView.setLayoutManager(new GridLayoutManager(context, NUM_COLUMNS);
ItemOffsetDecoration itemDecoration = new ItemOffsetDecoration(context, R.dimen.item_offset);
mRecyclerView.addItemDecoration(itemDecoration);

Ustaw także wartość przesunięcia elementu jako dopełnienie jej RecyclerViewi określ android:clipToPadding=false.

<android.support.v7.widget.RecyclerView
    android:id="@+id/recyclerview_grid"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipToPadding="false"
    android:padding="@dimen/item_offset"/>
yqritc
źródło
Idealne, jest proste i skuteczne.
B.shruti
28

Spróbuj tego. Zadba o równe odstępy dookoła. Działa zarówno z List, Grid, jak i StaggeredGrid.

Edytowane

Zaktualizowany kod powinien obsługiwać większość przypadków narożnych z rozpiętościami, orientacją itp. Należy pamiętać, że w przypadku korzystania z setSpanSizeLookup () z GridLayoutManager, ustawienie setSpanIndexCacheEnabled () jest zalecane ze względu na wydajność.

Uwaga: wydaje się, że w przypadku StaggeredGrid istnieje błąd, w którym indeks dzieci staje się zwariowany i trudny do śledzenia, więc poniższy kod może nie działać zbyt dobrze w przypadku StaggeredGridLayoutManager.

public class ListSpacingDecoration extends RecyclerView.ItemDecoration {

  private static final int VERTICAL = OrientationHelper.VERTICAL;

  private int orientation = -1;
  private int spanCount = -1;
  private int spacing;
  private int halfSpacing;


  public ListSpacingDecoration(Context context, @DimenRes int spacingDimen) {

    spacing = context.getResources().getDimensionPixelSize(spacingDimen);
    halfSpacing = spacing / 2;
  }

  public ListSpacingDecoration(int spacingPx) {

    spacing = spacingPx;
    halfSpacing = spacing / 2;
  }

  @Override
  public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {

    super.getItemOffsets(outRect, view, parent, state);

    if (orientation == -1) {
        orientation = getOrientation(parent);
    }

    if (spanCount == -1) {
        spanCount = getTotalSpan(parent);
    }

    int childCount = parent.getLayoutManager().getItemCount();
    int childIndex = parent.getChildAdapterPosition(view);

    int itemSpanSize = getItemSpanSize(parent, childIndex);
    int spanIndex = getItemSpanIndex(parent, childIndex);

    /* INVALID SPAN */
    if (spanCount < 1) return;

    setSpacings(outRect, parent, childCount, childIndex, itemSpanSize, spanIndex);
  }

  protected void setSpacings(Rect outRect, RecyclerView parent, int childCount, int childIndex, int itemSpanSize, int spanIndex) {

    outRect.top = halfSpacing;
    outRect.bottom = halfSpacing;
    outRect.left = halfSpacing;
    outRect.right = halfSpacing;

    if (isTopEdge(parent, childCount, childIndex, itemSpanSize, spanIndex)) {
        outRect.top = spacing;
    }

    if (isLeftEdge(parent, childCount, childIndex, itemSpanSize, spanIndex)) {
        outRect.left = spacing;
    }

    if (isRightEdge(parent, childCount, childIndex, itemSpanSize, spanIndex)) {
        outRect.right = spacing;
    }

    if (isBottomEdge(parent, childCount, childIndex, itemSpanSize, spanIndex)) {
        outRect.bottom = spacing;
    }
  }

  @SuppressWarnings("all")
  protected int getTotalSpan(RecyclerView parent) {

    RecyclerView.LayoutManager mgr = parent.getLayoutManager();
    if (mgr instanceof GridLayoutManager) {
        return ((GridLayoutManager) mgr).getSpanCount();
    } else if (mgr instanceof StaggeredGridLayoutManager) {
        return ((StaggeredGridLayoutManager) mgr).getSpanCount();
    } else if (mgr instanceof LinearLayoutManager) {
        return 1;
    }

    return -1;
  }

  @SuppressWarnings("all")
  protected int getItemSpanSize(RecyclerView parent, int childIndex) {

    RecyclerView.LayoutManager mgr = parent.getLayoutManager();
    if (mgr instanceof GridLayoutManager) {
        return ((GridLayoutManager) mgr).getSpanSizeLookup().getSpanSize(childIndex);
    } else if (mgr instanceof StaggeredGridLayoutManager) {
        return 1;
    } else if (mgr instanceof LinearLayoutManager) {
        return 1;
    }

    return -1;
  }

  @SuppressWarnings("all")
  protected int getItemSpanIndex(RecyclerView parent, int childIndex) {

    RecyclerView.LayoutManager mgr = parent.getLayoutManager();
    if (mgr instanceof GridLayoutManager) {
        return ((GridLayoutManager) mgr).getSpanSizeLookup().getSpanIndex(childIndex, spanCount);
    } else if (mgr instanceof StaggeredGridLayoutManager) {
        return childIndex % spanCount;
    } else if (mgr instanceof LinearLayoutManager) {
        return 0;
    }

    return -1;
  }

  @SuppressWarnings("all")
  protected int getOrientation(RecyclerView parent) {

    RecyclerView.LayoutManager mgr = parent.getLayoutManager();
    if (mgr instanceof LinearLayoutManager) {
        return ((LinearLayoutManager) mgr).getOrientation();
    } else if (mgr instanceof GridLayoutManager) {
        return ((GridLayoutManager) mgr).getOrientation();
    } else if (mgr instanceof StaggeredGridLayoutManager) {
        return ((StaggeredGridLayoutManager) mgr).getOrientation();
    }

    return VERTICAL;
  }

  protected boolean isLeftEdge(RecyclerView parent, int childCount, int childIndex, int itemSpanSize, int spanIndex) {

    if (orientation == VERTICAL) {

        return spanIndex == 0;

    } else {

        return (childIndex == 0) || isFirstItemEdgeValid((childIndex < spanCount), parent, childIndex);
    }
  }

  protected boolean isRightEdge(RecyclerView parent, int childCount, int childIndex, int itemSpanSize, int spanIndex) {

    if (orientation == VERTICAL) {

        return (spanIndex + itemSpanSize) == spanCount;

    } else {

        return isLastItemEdgeValid((childIndex >= childCount - spanCount), parent, childCount, childIndex, spanIndex);
    }
  }

  protected boolean isTopEdge(RecyclerView parent, int childCount, int childIndex, int itemSpanSize, int spanIndex) {

    if (orientation == VERTICAL) {

        return (childIndex == 0) || isFirstItemEdgeValid((childIndex < spanCount), parent, childIndex);

    } else {

        return spanIndex == 0;
    }
  }

  protected boolean isBottomEdge(RecyclerView parent, int childCount, int childIndex, int itemSpanSize, int spanIndex) {

    if (orientation == VERTICAL) {

        return isLastItemEdgeValid((childIndex >= childCount - spanCount), parent, childCount, childIndex, spanIndex);

    } else {

        return (spanIndex + itemSpanSize) == spanCount;
    }
  }

  protected boolean isFirstItemEdgeValid(boolean isOneOfFirstItems, RecyclerView parent, int childIndex) {

    int totalSpanArea = 0;
    if (isOneOfFirstItems) {
        for (int i = childIndex; i >= 0; i--) {
            totalSpanArea = totalSpanArea + getItemSpanSize(parent, i);
        }
    }

    return isOneOfFirstItems && totalSpanArea <= spanCount;
  }

  protected boolean isLastItemEdgeValid(boolean isOneOfLastItems, RecyclerView parent, int childCount, int childIndex, int spanIndex) {

    int totalSpanRemaining = 0;
    if (isOneOfLastItems) {
        for (int i = childIndex; i < childCount; i++) {
            totalSpanRemaining = totalSpanRemaining + getItemSpanSize(parent, i);
        }
    }

    return isOneOfLastItems && (totalSpanRemaining <= spanCount - spanIndex);
  }
}

Mam nadzieję, że to pomoże.

Pirdad Sakhizada
źródło
1
Mam podwójną rozpiętość zaraz po pierwszej linii przedmiotów. Dzieje się tak, ponieważ parent.getChildCount () zwraca 1 dla pierwszego elementu, 2 dla drugiego i tak dalej. Sugeruję więc dodanie miejsca do elementów górnej krawędzi, takich jak: outRect.top = childIndex <spanCount? spacingInPixels: 0; I dodaj dolne miejsce dla każdego elementu: outRect.bottom = spacingInPixels;
IvanP
W momencie przewijania RecyclerView odstępy uległy zmianie.
Yugesh,
3
Myślę, że parent.getChildCount () należy zmienić na „parent.getLayoutManager (). GetItemCount ()”. Ponadto należy zmienić funkcję isBottomEdge na „return childIndex> = childCount - spanCount + spanIndex”. Po ich zmianie uzyskałem równe odstępy. Pamiętaj jednak, że to rozwiązanie nie zapewnia mi równych rozmiarów elementów, jeśli liczba rozpiętości jest większa niż 2, ponieważ wartość przesunięcia jest różna w zależności od pozycji.
yqritc
1
@yqritc dzięki za nocticing parent.getChildCount (). Zaktualizowałem swoją odpowiedź, aby korzystać z parent.getLayoutManager (). GetItemCount ()
Pirdad Sakhizada
2
Działa to bardzo dobrze po wyjęciu z pudełka, nawet przy zmiennych zakresach, gratulacje i dziękuję!
Pierre-Luc Paour,
21

Poniższy kod będzie obsługiwał StaggeredGridLayoutManager, GridLayoutManager i LinearLayoutManager.

public class SpacesItemDecoration extends RecyclerView.ItemDecoration {

    private int halfSpace;

    public SpacesItemDecoration(int space) {
        this.halfSpace = space / 2;
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {

        if (parent.getPaddingLeft() != halfSpace) {
            parent.setPadding(halfSpace, halfSpace, halfSpace, halfSpace);
            parent.setClipToPadding(false);
        }

        outRect.top = halfSpace;
        outRect.bottom = halfSpace;
        outRect.left = halfSpace;
        outRect.right = halfSpace;
    }
}

Następnie użyj go

mRecyclerView.addItemDecoration(new SpacesItemDecoration(mMargin));
Mark Hetherington
źródło
1
To jest najprostsze. Jedną ważną rzeczą jest to, że musisz również dodać dopełnienie do elementu nadrzędnego w pliku XML. W moim przypadku to działa w ten sposób. Dzięki.
Samir
SpaceItemDecorationFaktycznie dodaje dopełnienie do rodzica (pogląd recyklingowa).
Mark Hetherington
tylko halfSpacedopełnienie pojawiło się (po prawej stronie), gdy nie ustawiłem dopełnienia do elementu nadrzędnego w formacie xml
Samir
Brakowało go tylko po prawej stronie? Możliwe, że masz pół spacji ustawione jako leftPadding po lewej stronie już w pliku xml i ten kod sprawdza tylko, czy lewe wypełnienie jest ustawione w RecyclerView, czy nie.
Mark Hetherington,
Cóż, nie mam żadnego zestawu wypełnień w xml.
Samir
11

Oto rozwiązanie, które nie wymaga „spanCount” (liczba kolumn) Używam go, ponieważ używam GridAutofitLayoutManager (oblicza liczbę kolumn zgodnie z wymaganym rozmiarem komórki)

(uwaga: działa to tylko w GridLayoutManager )

public class GridSpacesItemDecoration extends RecyclerView.ItemDecoration {
    private final boolean includeEdge;
    private int spacing;


    public GridSpacesItemDecoration(int spacing, boolean includeEdge) {
        this.spacing = spacing;
        this.includeEdge = includeEdge;
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        if (parent.getLayoutManager() instanceof GridLayoutManager) {
            GridLayoutManager layoutManager = (GridLayoutManager)parent.getLayoutManager();
            int spanCount = layoutManager.getSpanCount();
            int position = parent.getChildAdapterPosition(view); // item position
            int column = position % spanCount; // item column

            if (includeEdge) {
                outRect.left = spacing - column * spacing / spanCount; // spacing - column * ((1f / spanCount) * spacing)
                outRect.right = (column + 1) * spacing / spanCount; // (column + 1) * ((1f / spanCount) * spacing)

                if (position < spanCount) { // top edge
                    outRect.top = spacing;
                }
                outRect.bottom = spacing; // item bottom
            } else {
                outRect.left = column * spacing / spanCount; // column * ((1f / spanCount) * spacing)
                outRect.right = spacing - (column + 1) * spacing / spanCount; // spacing - (column + 1) * ((1f /    spanCount) * spacing)
                if (position >= spanCount) {
                    outRect.top = spacing; // item top
                }
            }

        }

    }
}

Oto GridAutofitLayoutManager - każdy jest zainteresowany:

public class GridAutofitLayoutManager extends GridLayoutManager {
    private int mColumnWidth;
    private boolean mColumnWidthChanged = true;

    public GridAutofitLayoutManager(Context context, int columnWidth)
    {
        /* Initially set spanCount to 1, will be changed automatically later. */
        super(context, 1);
        setColumnWidth(checkedColumnWidth(context, columnWidth));
    }

    public GridAutofitLayoutManager(Context context,int unit, int columnWidth)
    {
        /* Initially set spanCount to 1, will be changed automatically later. */
        super(context, 1);
        int pixColumnWidth = (int) TypedValue.applyDimension(unit, columnWidth, context.getResources().getDisplayMetrics());
        setColumnWidth(checkedColumnWidth(context, pixColumnWidth));
    }

    public GridAutofitLayoutManager(Context context, int columnWidth, int orientation, boolean reverseLayout)
    {
        /* Initially set spanCount to 1, will be changed automatically later. */
        super(context, 1, orientation, reverseLayout);
        setColumnWidth(checkedColumnWidth(context, columnWidth));
    }

    private int checkedColumnWidth(Context context, int columnWidth)
    {
        if (columnWidth <= 0)
        {
            /* Set default columnWidth value (48dp here). It is better to move this constant
            to static constant on top, but we need context to convert it to dp, so can't really
            do so. */
            columnWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48,
                    context.getResources().getDisplayMetrics());
        }
        return columnWidth;
    }

    public void setColumnWidth(int newColumnWidth)
    {
        if (newColumnWidth > 0 && newColumnWidth != mColumnWidth)
        {
            mColumnWidth = newColumnWidth;
            mColumnWidthChanged = true;
        }
    }

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)
    {
        int width = getWidth();
        int height = getHeight();
        if (mColumnWidthChanged && mColumnWidth > 0 && width > 0 && height > 0)
        {
            int totalSpace;
            if (getOrientation() == VERTICAL)
            {
                totalSpace = width - getPaddingRight() - getPaddingLeft();
            }
            else
            {
                totalSpace = height - getPaddingTop() - getPaddingBottom();
            }
            int spanCount = Math.max(1, totalSpace / mColumnWidth);
            setSpanCount(spanCount);

            mColumnWidthChanged = false;
        }
        super.onLayoutChildren(recycler, state);
    }
}

Wreszcie:

mDevicePhotosView.setLayoutManager(new GridAutofitLayoutManager(getContext(), getResources().getDimensionPixelSize(R.dimen.item_size)));
mDevicePhotosView.addItemDecoration(new GridSpacesItemDecoration(Util.dpToPx(getContext(), 2),true));
Gil SH
źródło
Cześć. Działa to niesamowicie, ale używam nagłówka z twoim rozwiązaniem. Czy możesz zasugerować, jak osiągnąć nagłówek o pełnej szerokości?
Ajeet
uprzejmie możesz sprawdzić pozycję w menedżerze układu, a następnie layoutManager.getPosition(view)sprawdzić, czy pozycja jest zerowa, która będzie Twoim nagłówkiem. W ten sposób możesz dodać kolejny nagłówek w dowolnych pozycjach :)
Mina Samir
9

Jest tylko jedno łatwe rozwiązanie, które możesz zapamiętać i wdrożyć w dowolnym miejscu. Bez błędów, bez szalonych obliczeń. Umieść margines w układzie karty / przedmiotu i ustaw ten sam rozmiar co wypełnienie w RecyclerView:

item_layout.xml

<CardView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:margin="10dp">

activity_layout.xml

<RecyclerView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp"/>

AKTUALIZACJA: wprowadź opis zdjęcia tutaj

Galya
źródło
To działa dobrze! czy mógłbyś rozwinąć tę kwestię?
elyar abad
Dziękuję bardzo! Szukałem jakiegoś technicznego powodu, dla którego taka współpraca między wyściółką recyklera a marżą przedmiotu jest potrzebna. Tak czy inaczej zrobiłeś dla mnie tyle. . .
elyar abad
7

Jeśli chcesz NAPRAWIONO rozmiar RecyclerViewprzedmiotu na wszystkich urządzeniach. Możesz to zrobić w ten sposób

public class GridSpacingItemDecoration extends RecyclerView.ItemDecoration {

    private int mSpanCount;
    private float mItemSize;

    public GridSpacingItemDecoration(int spanCount, int itemSize) {
        this.mSpanCount = spanCount;
        mItemSize = itemSize;
    }

    @Override
    public void getItemOffsets(final Rect outRect, final View view, RecyclerView parent,
            RecyclerView.State state) {
        final int position = parent.getChildLayoutPosition(view);
        final int column = position % mSpanCount;
        final int parentWidth = parent.getWidth();
        int spacing = (int) (parentWidth - (mItemSize * mSpanCount)) / (mSpanCount + 1);
        outRect.left = spacing - column * spacing / mSpanCount;
        outRect.right = (column + 1) * spacing / mSpanCount;

        if (position < mSpanCount) {
            outRect.top = spacing;
        }
        outRect.bottom = spacing;
    }
}

recyclerview_item.xml

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="@dimen/recycler_view_item_width" 
    ...
    >
    ...
</LinearLayout>

dimens.xml

 <dimen name="recycler_view_item_width">60dp</dimen>

Czynność

int numberOfColumns = 3;
mRecyclerView.setLayoutManager(new GridLayoutManager(this, numberOfColumns));
mRecyclerView.setAdapter(...);
mRecyclerView.addItemDecoration(new GridSpacingItemDecoration(3,
        getResources().getDimensionPixelSize(R.dimen.recycler_view_item_width)));

wprowadź opis zdjęcia tutaj wprowadź opis zdjęcia tutaj

Phan Van Linh
źródło
czy będzie działał zgodnie z rozmiarem ekranu oznacza, że ​​jest wyświetlany na 5-calowym ekranie, wyglądają tak samo na innych rozmiarach ekranu?
Sunil
rozmiar przedmiotu zostanie ustalony, ale odstępy między przedmiotami mogą się różnić, możesz też zobaczyć 2 zdjęcia powyżej, aby zrozumieć
Phan Van Linh
Wyglądają inaczej na różnych rozmiarach ekranu. W dowolny sposób, ale działają dziękuję
Sunil
6

Wybrana odpowiedź jest prawie idealna, ale w zależności od miejsca szerokość elementów może nie być równa. (W moim przypadku było to krytyczne). Skończyłem z tym kodem, który trochę zwiększa przestrzeń, więc wszystkie elementy mają tę samą szerokość.

   class GridSpacingItemDecoration(private val columnCount: Int, @Px preferredSpace: Int, private val includeEdge: Boolean): RecyclerView.ItemDecoration() {

    /**
     * In this algorithm space should divide by 3 without remnant or width of items can have a difference
     * and we want them to be exactly the same
     */
    private val space = if (preferredSpace % 3 == 0) preferredSpace else (preferredSpace + (3 - preferredSpace % 3))

    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State?) {
        val position = parent.getChildAdapterPosition(view)

        if (includeEdge) {

            when {
                position % columnCount == 0 -> {
                    outRect.left = space
                    outRect.right = space / 3
                }
                position % columnCount == columnCount - 1 -> {
                    outRect.right = space
                    outRect.left = space / 3
                }
                else -> {
                    outRect.left = space * 2 / 3
                    outRect.right = space * 2 / 3
                }
            }

            if (position < columnCount) {
                outRect.top = space
            }

            outRect.bottom = space

        } else {

            when {
                position % columnCount == 0 -> outRect.right = space * 2 / 3
                position % columnCount == columnCount - 1 -> outRect.left = space * 2 / 3
                else -> {
                    outRect.left = space / 3
                    outRect.right = space / 3
                }
            }

            if (position >= columnCount) {
                outRect.top = space
            }
        }
    }

}
Kuva
źródło
Dodałbym następujące wiersze, jeśli ktoś taki jak ja za pomocą GridLayoutManager z spanCount = 1 columnCount == 1 -> { outRect.left = space outRect.right = space }
massivemadness
5

Skopiowałem dostarczony kod @edwardaa i robię to, aby idealnie obsługiwał RTL:

public class GridSpacingItemDecoration extends RecyclerView.ItemDecoration {
    private int spanCount;
    private int spacing;
    private boolean includeEdge;
    private int headerNum;
    private boolean isRtl = TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault()) == ViewCompat.LAYOUT_DIRECTION_RTL;

    public GridSpacingItemDecoration(int spanCount, int spacing, boolean includeEdge, int headerNum) {
        this.spanCount = spanCount;
        this.spacing = spacing;
        this.includeEdge = includeEdge;
        this.headerNum = headerNum;
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        int position = parent.getChildAdapterPosition(view) - headerNum; // item position
        if (position >= 0) {
            int column = position % spanCount; // item column
            if(isRtl) {
                column = spanCount - 1 - column;
            }
            if (includeEdge) {
                outRect.left = spacing - column * spacing / spanCount; // spacing - column * ((1f / spanCount) * spacing)
                outRect.right = (column + 1) * spacing / spanCount; // (column + 1) * ((1f / spanCount) * spacing)

                if (position < spanCount) { // top edge
                    outRect.top = spacing;
                }
                outRect.bottom = spacing; // item bottom
            } else {
                outRect.left = column * spacing / spanCount; // column * ((1f / spanCount) * spacing)
                outRect.right = spacing - (column + 1) * spacing / spanCount; // spacing - (column + 1) * ((1f /    spanCount) * spacing)
                if (position >= spanCount) {
                    outRect.top = spacing; // item top
                }
            }
        } else {
            outRect.left = 0;
            outRect.right = 0;
            outRect.top = 0;
            outRect.bottom = 0;
        }
    }
}
Xingxing
źródło
każdy może skopiować kod z gist.github.com/xingstarx/f2525ef32b04a5e67fecc5c0b5c4b939
Xingxing
4

Odpowiedzi powyżej wyjaśniły sposoby ustawiania obsługi marginesów GridLayoutManager i LinearLayoutManager.

Jednak w przypadku StaggeredGridLayoutManager odpowiedź Pirdada Sakhizady brzmi: „Może nie działać dobrze ze StaggeredGridLayoutManager”. To powinien być problem dotyczący IndexOfSpan.

Możesz to uzyskać w ten sposób:

private static class MyItemDecoration extends RecyclerView.ItemDecoration {
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        int index = ((StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams()).getSpanIndex();
    }
}
wklbeta
źródło
4
public class GridSpacingItemDecoration extends RecyclerView.ItemDecoration {

    private int spanCount;
    private int spacing;
    private boolean includeEdge;

    public GridSpacingItemDecoration(int spanCount, int spacing, boolean includeEdge) {
        this.spanCount = spanCount;
        this.spacing = spacing;
        this.includeEdge = includeEdge;
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        StaggeredGridLayoutManager.LayoutParams params = (StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams();
        int column = params.getSpanIndex();

        if (includeEdge) {
            outRect.left = spacing - column * spacing / spanCount; // spacing - column * ((1f / spanCount) * spacing)
            outRect.right = (column + 1) * spacing / spanCount; // (column + 1) * ((1f / spanCount) * spacing)

            if (position < spanCount) { // top edge
                outRect.top = spacing;
            }
            outRect.bottom = spacing; // item bottom
        } else {
            outRect.left = column * spacing / spanCount; // column * ((1f / spanCount) * spacing)
            outRect.right = spacing - (column + 1) * spacing / spanCount; // spacing - (column + 1) * ((1f /    spanCount) * spacing)
            if (position >= spanCount) {
                outRect.top = spacing; // item top
            }
        }
    }
}

Różni się nieco od odpowiedzi edwardaa, różnica polega na tym, jak kolumna jest określana, ponieważ w przypadkach takich jak przedmioty o różnych wysokościach, kolumny nie można określić po prostu% spanCount

Jonathan Lee
źródło
4
class VerticalGridSpacingDecoration(private val spacing: Int) : RecyclerView.ItemDecoration() {

  override fun getItemOffsets(
    outRect: Rect,
    view: View,
    parent: RecyclerView,
    state: State
  ) {
    val layoutManager = parent.layoutManager as? GridLayoutManager
    if (layoutManager == null || layoutManager.orientation != VERTICAL) {
      return super.getItemOffsets(outRect, view, parent, state)
    }

    val spanCount = layoutManager.spanCount
    val position = parent.getChildAdapterPosition(view)
    val column = position % spanCount
    with(outRect) {
      left = if (column == 0) 0 else spacing / 2
      right = if (column == spanCount.dec()) 0 else spacing / 2
      top = if (position < spanCount) 0 else spacing
    }
  }
}
fraggjkee
źródło
3

Oto moja modyfikacja, SpacesItemDecorationktóra może zająć numOfColums i spację równo na górze, dole, lewej i prawej stronie .

public class SpacesItemDecoration extends RecyclerView.ItemDecoration {
    private int space;
    private int mNumCol;

    public SpacesItemDecoration(int space, int numCol) {
        this.space = space;
        this.mNumCol=numCol;
    }

    @Override
    public void getItemOffsets(Rect outRect, View view,
                               RecyclerView parent, RecyclerView.State state) {

        //outRect.right = space;
        outRect.bottom = space;
        //outRect.left = space;

        //Log.d("ttt", "item position" + parent.getChildLayoutPosition(view));
        int position=parent.getChildLayoutPosition(view);

        if(mNumCol<=2) {
            if (position == 0) {
                outRect.left = space;
                outRect.right = space / 2;
            } else {
                if ((position % mNumCol) != 0) {
                    outRect.left = space / 2;
                    outRect.right = space;
                } else {
                    outRect.left = space;
                    outRect.right = space / 2;
                }
            }
        }else{
            if (position == 0) {
                outRect.left = space;
                outRect.right = space / 2;
            } else {
                if ((position % mNumCol) == 0) {
                    outRect.left = space;
                    outRect.right = space/2;
                } else if((position % mNumCol) == (mNumCol-1)){
                    outRect.left = space/2;
                    outRect.right = space;
                }else{
                    outRect.left=space/2;
                    outRect.right=space/2;
                }
            }

        }

        if(position<mNumCol){
            outRect.top=space;
        }else{
            outRect.top=0;
        }
        // Add top margin only for the first item to avoid double space between items
        /*
        if (parent.getChildLayoutPosition(view) == 0 ) {

        } else {
            outRect.top = 0;
        }*/
    }
}

i użyj poniższego kodu na swojej logice.

recyclerView.addItemDecoration(new SpacesItemDecoration(spacingInPixels, numCol));
andy anexinet
źródło
2

Istnieje bardzo proste, a jednocześnie elastyczne rozwiązanie tego problemu, wykorzystujące tylko XML, który działa na każdym narzędziu LayoutManager.

Załóżmy, że chcesz równych odstępów X (na przykład 8dp).

  1. Zawiń swój element CardView w innym układzie

  2. Nadaj zewnętrznemu układowi wypełnienie X / 2 (4dp)

  3. Ustaw przezroczyste tło zewnętrznego układu

...

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="200dp"
    android:layout_height="200dp"
    android:background="@android:color/transparent"
    android:padding="4dip">

    <android.support.v7.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </android.support.v7.widget.CardView>

</LinearLayout>
  1. Daj swojemu recyklerowi widok wypełnienia X / 2 (4dp)

...

<android.support.v7.widget.RecyclerView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="4dp" />

i to wszystko. Masz idealne odstępy X (8dp).

kaveh gh
źródło
2

Dla tych, którzy mają problemy z staggeredLayoutManager (jak https://imgur.com/XVutH5u )

metody recyclinglerView:

getChildAdapterPosition(view)
getChildLayoutPosition(view)

czasami zwraca -1 jako indeks, abyśmy mogli napotkać problemy z ustawieniem itemDecor. Moje rozwiązanie polega na zastąpieniu przestarzałej metody ItemDecoration:

public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent)

zamiast nowicjusza:

public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)

lubię to:

recyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
            @Override
            public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
                TheAdapter.VH vh = (TheAdapter.VH) recyclerView.findViewHolderForAdapterPosition(itemPosition);
                View itemView = vh.itemView;    //itemView is the base view of viewHolder
                //or instead of the 2 lines above maybe it's possible to use  View itemView = layoutManager.findViewByPosition(itemPosition)  ... NOT TESTED

                StaggeredGridLayoutManager.LayoutParams itemLayoutParams = (StaggeredGridLayoutManager.LayoutParams) itemView.getLayoutParams();

                int spanIndex = itemLayoutParams.getSpanIndex();

                if (spanIndex == 0)
                    ...
                else
                    ...
            }
        });

Do tej pory wydaje mi się, że działa :)

navid
źródło
Świetna odpowiedź człowieku! Działa we wszystkich przypadkach, w tym w niesymetrycznym „regularnym” GridLayoutManager, w którym między elementami znajduje się element nagłówka. Dzięki!
Shirane85,
2

Odpowiedzi na to pytanie wydają się bardziej złożone niż powinny. Oto moje zdanie na ten temat.

Powiedzmy, że chcesz odstępów 1dp między elementami siatki. Wykonaj następujące czynności:

  1. Dodaj wypełnienie 0,5dp do każdego elementu
  2. Dodaj dopełnienie -0,5dp do RecycleView
  3. Otóż ​​to! :)
Sabaat Ahmad
źródło
1

Będzie to działać również RecyclerViewz nagłówkiem.

public class GridSpacingItemDecoration extends RecyclerView.ItemDecoration {

    private int spanCount;
    private int spacing;
    private boolean includeEdge;
    private int headerNum;

    public GridSpacingItemDecoration(int spanCount, int spacing, boolean includeEdge, int headerNum) {
        this.spanCount = spanCount;
        this.spacing = spacing;
        this.includeEdge = includeEdge;
        this.headerNum = headerNum;
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        int position = parent.getChildAdapterPosition(view) - headerNum; // item position

        if (position >= 0) {
            int column = position % spanCount; // item column

            if (includeEdge) {
                outRect.left = spacing - column * spacing / spanCount; // spacing - column * ((1f / spanCount) * spacing)
                outRect.right = (column + 1) * spacing / spanCount; // (column + 1) * ((1f / spanCount) * spacing)

                if (position < spanCount) { // top edge
                    outRect.top = spacing;
                }
                outRect.bottom = spacing; // item bottom
            } else {
                outRect.left = column * spacing / spanCount; // column * ((1f / spanCount) * spacing)
                outRect.right = spacing - (column + 1) * spacing / spanCount; // spacing - (column + 1) * ((1f /    spanCount) * spacing)
                if (position >= spanCount) {
                    outRect.top = spacing; // item top
                }
            }
        } else {
            outRect.left = 0;
            outRect.right = 0;
            outRect.top = 0;
            outRect.bottom = 0;
        }
    }
    }
}
DeepakPanwar
źródło
Co to jest headerNum?
Tim Kranen
1

Odpowiedź yqritc działała dla mnie idealnie. Jednak korzystałem z Kotlina, więc tutaj jest to odpowiednik.

class ItemOffsetDecoration : RecyclerView.ItemDecoration  {

    // amount to add to padding
    private val _itemOffset: Int

    constructor(itemOffset: Int) {
        _itemOffset = itemOffset
    }

    constructor(@NonNull context: Context, @DimenRes itemOffsetId: Int){
       _itemOffset = context.resources.getDimensionPixelSize(itemOffsetId)
    }

    /**
     * Applies padding to all sides of the [Rect], which is the container for the view
     */
    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView,state: RecyclerView.State) {
        super.getItemOffsets(outRect, view, parent, state)
        outRect.set(_itemOffset, _itemOffset, _itemOffset, _itemOffset)
    }
}

Cała reszta jest taka sama.

stegnerd
źródło
1

Dla użytkowników StaggeredGridLayoutManager , bądź ostrożny, wiele odpowiedzi tutaj, w tym najczęściej głosowanych, oblicza kolumnę z poniższym kodem:

int column = position % spanCount

który zakłada, że ​​pozycje 1/3/5 / .. zawsze znajdują się po lewej stronie, a pozycje 2/4/6 / .. zawsze znajdują się po prawej stronie. Czy to założenie jest zawsze prawdziwe? Nie.

Powiedzmy, że twój pierwszy przedmiot ma 100dp, a drugi to tylko 50dp, zgadnij gdzie jest twój trzeci przedmiot, po lewej czy po prawej?

ZhouX
źródło
0

Skończyło się to tak dla mojego RecyclerView z GridLayoutManager i HeaderView .

W poniższym kodzie ustawiam odstęp 4dp między każdym przedmiotem (2dp wokół każdego pojedynczego elementu i dopełnienie 2dp wokół całego widoku recyclingu).

layout.xml

<android.support.v7.widget.RecyclerView
    android:id="@+id/recycleview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="2dp" />

fragment / aktywność

GridLayoutManager manager = new GridLayoutManager(getContext(), 3);
recyclerView.setLayoutManager(manager);
int spacingInPixels = Utils.dpToPx(2);
recyclerView.addItemDecoration(new SpacesItemDecoration(spacingInPixels));

SpaceItemDecoration.java

public class SpacesItemDecoration extends RecyclerView.ItemDecoration {

    private int mSpacing;

    public SpacesItemDecoration(int spacing) {
        mSpacing = spacing;
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView recyclerView, RecyclerView.State state) {
        outRect.left = mSpacing;
        outRect.top = mSpacing;
        outRect.right = mSpacing;
        outRect.bottom = mSpacing;
    }
}

Utils.java

public static int dpToPx(final float dp) {
    return Math.round(dp * (Resources.getSystem().getDisplayMetrics().xdpi / DisplayMetrics.DENSITY_DEFAULT));
}
Simon Schubert
źródło
0

Aby https://stackoverflow.com/a/29905000/1649371 (powyżej) zadziałało, musiałem zmodyfikować następujące metody (i wszystkie kolejne wywołania)

@SuppressWarnings("all")
protected int getItemSpanSize(RecyclerView parent, View view, int childIndex) {

    RecyclerView.LayoutManager mgr = parent.getLayoutManager();
    if (mgr instanceof GridLayoutManager) {
        return ((GridLayoutManager) mgr).getSpanSizeLookup().getSpanSize(childIndex);
    } else if (mgr instanceof StaggeredGridLayoutManager) {
        return ((StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams()).isFullSpan() ? spanCount : 1;
    } else if (mgr instanceof LinearLayoutManager) {
        return 1;
    }

    return -1;
}

@SuppressWarnings("all")
protected int getItemSpanIndex(RecyclerView parent, View view, int childIndex) {

    RecyclerView.LayoutManager mgr = parent.getLayoutManager();
    if (mgr instanceof GridLayoutManager) {
        return ((GridLayoutManager) mgr).getSpanSizeLookup().getSpanIndex(childIndex, spanCount);
    } else if (mgr instanceof StaggeredGridLayoutManager) {
        return ((StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams()).getSpanIndex();
    } else if (mgr instanceof LinearLayoutManager) {
        return 0;
    }

    return -1;
}
Damián Arrillaga
źródło
0

Ten link działał dla mnie we wszystkich sytuacjach, w których możesz spróbować.

Vivek Barai
źródło
0

Jeśli masz przełącznik, który przełącza między listą a siatką, nie zapomnij zadzwonić recyclerView.removeItemDecoration()przed ustawieniem nowej dekoracji przedmiotu. Jeśli nie, nowe obliczenia odstępów byłyby niepoprawne.


Coś takiego.

        recyclerView.removeItemDecoration(gridItemDecorator)
        recyclerView.removeItemDecoration(listItemDecorator)
        if (showAsList){
            recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
            recyclerView.addItemDecoration(listItemDecorator)
        }
        else{
            recyclerView.layoutManager = GridLayoutManager(this, spanCount)
            recyclerView.addItemDecoration(gridItemDecorator)
        }
Sahil Patel
źródło
0

Jeśli używasz żniwna z GridLayoutManager użyć tego kodu napisanego w Kotlin dla odstępy między sieciami:

inner class SpacesItemDecoration(itemSpace: Int) : RecyclerView.ItemDecoration() {
    var space: Int = itemSpace

    override fun getItemOffsets(outRect: Rect?, view: View?, parent: RecyclerView?, state: RecyclerView.State?) {
        super.getItemOffsets(outRect, view, parent, state)
        val position = parent!!.getChildAdapterPosition(view)
        val viewType = parent.adapter.getItemViewType(position)
       //check to not to set any margin to header item 
        if (viewType == GridViewAdapter.TYPE_HEADER) {
            outRect!!.top = 0
            outRect.left = 0
            outRect.right = 0
            outRect.bottom = 0
        } else {
            outRect!!.left = space
            outRect.right = space
            outRect.bottom = space

            if (parent.getChildLayoutPosition(view) == 0) {
                outRect.top = space
            } else {
                outRect.top = 0
            }
        }
    }
    }

I przejść ItemDecorationdo recyclerviewjak

mIssueGridView.addItemDecoration(SpacesItemDecoration(10))
Kapil Rajput
źródło
0

Podczas korzystania z CardView dla dzieci problem ze spacjami między elementami można rozwiązać, ustawiając app: cardUseCompatPadding na true.

Dla większych marginesów powiększ elewację przedmiotu. CardElevation jest opcjonalna (użyj wartości domyślnej).

<androidx.cardview.widget.CardView
    xmlns:app="http://schemas.android.com/apk/res-auto"
    app:cardUseCompatPadding="true"
    app:cardElevation="2dp">
David Vareka
źródło
-1

dzięki odpowiedzi edwardaa https://stackoverflow.com/a/30701422/2227031

Należy również zauważyć, że:

jeśli całkowite odstępy i całkowite itemWidth nie są równe szerokości ekranu, należy również dostosować itemWidth, na przykład w metodzie adaptera onBindViewHolder

Utils.init(_mActivity);
int width = 0;
if (includeEdge) {
    width = ScreenUtils.getScreenWidth() - spacing * (spanCount + 1);
} else {
    width = ScreenUtils.getScreenWidth() - spacing * (spanCount - 1);
}
int itemWidth = width / spanCount;

ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) holder.imageViewAvatar.getLayoutParams();
// suppose the width and height are the same
layoutParams.width = itemWidth;
layoutParams.height = itemWidth;
holder.imageViewAvatar.setLayoutParams(layoutParams);
jk2K
źródło
-1

Wersja Kotlina, którą stworzyłem w oparciu o świetną odpowiedź edwardaa

class RecyclerItemDecoration(private val spanCount: Int, private val spacing: Int) : RecyclerView.ItemDecoration() {

  override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {

    val spacing = Math.round(spacing * parent.context.resources.displayMetrics.density)
    val position = parent.getChildAdapterPosition(view)
    val column = position % spanCount

    outRect.left = spacing - column * spacing / spanCount
    outRect.right = (column + 1) * spacing / spanCount

    outRect.top = if (position < spanCount) spacing else 0
    outRect.bottom = spacing
  }

}
Leon
źródło