RecyclerView GridLayoutManager : 스팬 수를 자동 감지하는 방법은 무엇입니까?


110

새로운 GridLayoutManager 사용 : https://developer.android.com/reference/android/support/v7/widget/GridLayoutManager.html

명시적인 스팬 카운트가 필요하므로 이제 문제는 다음과 같습니다. 행당 얼마나 많은 "스팬"이 맞는지 어떻게 알 수 있습니까? 이것은 결국 그리드입니다. 측정 된 너비를 기준으로 RecyclerView가 맞을 수있는만큼의 범위가 있어야합니다.

old를 사용하면 GridView"columnWidth"속성을 설정하기 만하면 맞는 열 수를 자동으로 감지합니다. 이것은 기본적으로 RecyclerView에 대해 복제하려는 것입니다.

  • OnLayoutChangeListener를 RecyclerView
  • 이 콜백에서 단일 '그리드 항목'을 팽창시키고 측정합니다.
  • spanCount = recyclerViewWidth / singleItemWidth;

이것은 매우 일반적인 행동처럼 보이므로 내가 보지 못하는 더 간단한 방법이 있습니까?

답변:


122

Personaly 나는 이것을 위해 RecyclerView를 하위 클래스로 만들고 싶지 않습니다. 왜냐하면 저에게는 GridLayoutManager의 책임이 스팬 수를 감지하는 것 같기 때문입니다. 따라서 RecyclerView 및 GridLayoutManager에 대한 일부 Android 소스 코드를 파고 난 후 작업을 수행하는 자체 클래스 확장 GridLayoutManager를 작성했습니다.

public class GridAutofitLayoutManager extends GridLayoutManager
{
    private int columnWidth;
    private boolean isColumnWidthChanged = true;
    private int lastWidth;
    private int lastHeight;

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

    public GridAutofitLayoutManager(
        @NonNull final Context context,
        final int columnWidth,
        final int orientation,
        final boolean reverseLayout) {

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

    private int checkedColumnWidth(@NonNull final Context context, final 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(final int newColumnWidth) {
        if (newColumnWidth > 0 && newColumnWidth != columnWidth) {
            columnWidth = newColumnWidth;
            isColumnWidthChanged = true;
        }
    }

    @Override
    public void onLayoutChildren(@NonNull final RecyclerView.Recycler recycler, @NonNull final RecyclerView.State state) {
        final int width = getWidth();
        final int height = getHeight();
        if (columnWidth > 0 && width > 0 && height > 0 && (isColumnWidthChanged || lastWidth != width || lastHeight != height)) {
            final int totalSpace;
            if (getOrientation() == VERTICAL) {
                totalSpace = width - getPaddingRight() - getPaddingLeft();
            } else {
                totalSpace = height - getPaddingTop() - getPaddingBottom();
            }
            final int spanCount = Math.max(1, totalSpace / columnWidth);
            setSpanCount(spanCount);
            isColumnWidthChanged = false;
        }
        lastWidth = width;
        lastHeight = height;
        super.onLayoutChildren(recycler, state);
    }
}

onLayoutChildren에서 스팬 수를 설정하기로 선택한 이유를 실제로 기억하지 못합니다.이 클래스는 얼마 전에 작성했습니다. 그러나 요점은 뷰가 측정 된 후에 그렇게해야한다는 것입니다. 높이와 너비를 얻을 수 있습니다.

편집 1 : 코드의 오류를 수정하여 스팬 수를 잘못 설정했습니다. 솔루션 을보고하고 제안 주신 @Elyees Abouda 사용자 에게 감사드립니다 .

편집 2 : 일부 작은 리팩토링 및 수동 방향 변경 처리로 가장자리 케이스를 수정합니다. 보고하고 해결책을 제안 주신 @tatarize 사용자 에게 감사드립니다 .


8
이것은 받아 들여진 대답이어야합니다. LayoutManager아이들을 배치하는 것이 아니라 RecyclerView's가 아닌 배치하는
mewa

3
@ s.maks : columnWidth는 어댑터에 전달 된 데이터에 따라 달라집니다. 4 자 단어가 전달되면 10 자 단어가 전달되면 연속으로 4 개 항목을 수용해야하며 연속적으로 2 개 항목 만 수용 할 수 있어야한다고 말합니다.
Shubham

2
저에게는 기기를 회전 할 때 그리드 크기가 약간 변경되어 20dp가 줄어 듭니다. 이것에 대한 정보가 있습니까? 감사.
Alexandre

3
때때로 getWidth()또는 getHeight()잘못된 spanCount 얻을 것이다 뷰가 생성되기 전에 0 (것 totalSpace 이후 1 <= 0)입니다. 내가 추가 한 것은이 경우 setSpanCount를 무시하는 것입니다. ( onLayoutChildren나중에 다시 호출됩니다)
Elyess Abouda

3
다루지 않는 가장자리 조건이 있습니다. 전체 활동을 다시 빌드하는 대신 회전을 처리하도록 configChanges를 설정하면 recyclerview의 너비가 변경되지만 다른 것은 변경되지 않는 이상한 경우가 있습니다. 너비와 높이가 변경되면 spancount가 더럽지 만 mColumnWidth는 변경되지 않았으므로 onLayoutChildren ()이 중단되고 현재 더티 값을 다시 계산하지 않습니다. 이전 높이와 너비를 저장하고 0이 아닌 방식으로 변경되면 트리거합니다.
Tatarize

29

뷰 트리 관찰자를 사용하여 한 번 렌더링 된 recylcerview의 너비를 얻은 다음 리소스에서 내 카드 뷰의 고정 치수를 가져온 다음 계산을 수행 한 후 스팬 수를 설정합니다. 표시하는 항목이 고정 너비 인 경우에만 실제로 적용 할 수 있습니다. 이것은 화면 크기 나 방향에 관계없이 자동으로 그리드를 채우는 데 도움이되었습니다.

mRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener(
            new ViewTreeObserver.OnGlobalLayoutListener() {
                @Override
                public void onGlobalLayout() {
                    mRecyclerView.getViewTreeObserver().removeOnGLobalLayoutListener(this);
                    int viewWidth = mRecyclerView.getMeasuredWidth();
                    float cardViewWidth = getActivity().getResources().getDimension(R.dimen.cardview_layout_width);
                    int newSpanCount = (int) Math.floor(viewWidth / cardViewWidth);
                    mLayoutManager.setSpanCount(newSpanCount);
                    mLayoutManager.requestLayout();
                }
            });

2
나는 이것을 사용하고있어 ArrayIndexOutOfBoundsException( android.support.v7.widget.GridLayoutManager.layoutChunk (GridLayoutManager.java:361)에서 을 스크롤 할 때) RecyclerView.
fikr4n

그냥 setSpanCount (후 mLayoutManager.requestLayout ()를 추가)과 작동
alvinmeimoun

2
참고 : removeGlobalOnLayoutListener()API 레벨 16에서는 더 이상 사용되지 않습니다 removeOnGlobalLayoutListener(). 대신 사용하십시오. 문서 .
pez

오타 removeOnGLobalLayoutListener는 removeOnGlobalLayoutListener이어야합니다.
Theo

17

글쎄, 이것은 내가 사용한 것입니다. 상당히 기본적이지만 나를 위해 일을 수행합니다. 이 코드는 기본적으로 화면 너비를 딥으로 얻은 다음 300으로 나눕니다 (또는 어댑터 레이아웃에 사용하는 너비). 따라서 300-500 폭의 딥 너비를 가진 더 작은 휴대폰은 하나의 열만 표시하고 태블릿은 2-3 개의 열 등을 표시합니다. 내가 볼 수있는 한 간단하고 소란스럽지 않고 단점이 없습니다.

Display display = getActivity().getWindowManager().getDefaultDisplay();
DisplayMetrics outMetrics = new DisplayMetrics();
display.getMetrics(outMetrics);

float density  = getResources().getDisplayMetrics().density;
float dpWidth  = outMetrics.widthPixels / density;
int columns = Math.round(dpWidth/300);
mLayoutManager = new GridLayoutManager(getActivity(),columns);
mRecyclerView.setLayoutManager(mLayoutManager);

1
RecyclerView의 너비 대신 화면 너비를 사용하는 이유는 무엇입니까? 300 하드 코딩은 나쁜 습관입니다 (xml 레이아웃과 동기화 상태를 유지해야 함)
foo64

xml의 ​​@ foo64는 항목에 match_parent를 설정할 수 있습니다. 그러나 예, 그 여전히 추한)
필립 줄리아니

15

RecyclerView를 확장하고 onMeasure 메서드를 덮어 썼습니다.

가능한 한 빨리 항목 너비 (멤버 변수)를 기본값 1로 설정했습니다. 이는 구성 변경시 업데이트됩니다. 이제 세로, 가로, 휴대 전화 / 태블릿 등에 들어갈 수있는만큼 많은 행이 있습니다.

@Override
protected void onMeasure(int widthSpec, int heightSpec) {
    super.onMeasure(widthSpec, heightSpec);
    int width = MeasureSpec.getSize(widthSpec);
    if(width != 0){
        int spans = width / mItemWidth;
        if(spans > 0){
            mLayoutManager.setSpanCount(spans);
        }
    }
}

12
+1 Chiu-ki Chan 은이 접근 방식을 설명 하는 블로그 게시물 과 이에 대한 샘플 프로젝트 도 있습니다.
CommonsWare

7

나는 누군가가 내 경우와 같이 이상한 열 너비를 얻는 경우를 대비하여 이것을 게시하고 있습니다.

저의 평판이 낮기 때문에 @ s-marks 의 답변 에 대해 언급 할 수 없습니다 . 그의 솔루션 솔루션을 적용 했지만 이상한 열 너비를 얻었으므로 다음과 같이 checkedColumnWidth 함수를 수정 했습니다 .

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

    else
    {
        columnWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, columnWidth,
                context.getResources().getDisplayMetrics());
    }
    return columnWidth;
}

주어진 열 너비를 DP로 변환하여 문제를 해결했습니다.


2

s-marks 의 대답에 대한 방향 변경을 수용하기 위해 너비 변경 (열 너비가 아닌 getWidth ()의 너비)에 대한 검사를 추가했습니다.

private boolean mWidthChanged = true;
private int mWidth;


@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)
{
    int width = getWidth();
    int height = getHeight();

    if (width != mWidth) {
        mWidthChanged = true;
        mWidth = width;
    }

    if (mColumnWidthChanged && mColumnWidth > 0 && width > 0 && height > 0
            || mWidthChanged)
    {
        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;
        mWidthChanged = false;
    }
    super.onLayoutChildren(recycler, state);
}

2

upvoted 솔루션은 괜찮지 만 들어오는 값을 픽셀로 처리하므로 테스트를 위해 값을 하드 코딩하고 dp를 가정하는 경우 문제가 발생할 수 있습니다. 가장 쉬운 방법은 열 너비를 차원에 넣고 GridAutofitLayoutManager를 구성 할 때 읽는 것입니다. 그러면 dp가 올바른 픽셀 값으로 자동 변환됩니다.

new GridAutofitLayoutManager(getActivity(), (int)getActivity().getResources().getDimension(R.dimen.card_width))

그래, 그것은 나를 반복했다. 실제로 자원 자체를 받아들이는 것입니다. 항상 우리가하게 될 일입니다.
Tatarize

2
  1. imageView의 최소 고정 너비 설정 (예 : 144dp x 144dp)
  2. GridLayoutManager를 만들 때 imageView의 최소 크기로 얼마나 많은 열이 될지 알아야합니다.

    WindowManager wm = (WindowManager) this.getSystemService(Context.WINDOW_SERVICE); //Получаем размер экрана
    Display display = wm.getDefaultDisplay();
    
    Point point = new Point();
    display.getSize(point);
    int screenWidth = point.x; //Ширина экрана
    
    int photoWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 144, this.getResources().getDisplayMetrics()); //Переводим в точки
    
    int columnsCount = screenWidth/photoWidth; //Число столбцов
    
    GridLayoutManager gridLayoutManager = new GridLayoutManager(this, columnsCount);
    recyclerView.setLayoutManager(gridLayoutManager);
  3. 그 후 열에 공간이 있으면 어댑터에서 imageView의 크기를 조정해야합니다. newImageViewSize를 보낸 다음 화면 및 열 수를 계산하는 활동에서 어댑터를 초기화 할 수 있습니다.

    @Override //Заполнение нашей плитки
    public void onBindViewHolder(PhotoHolder holder, int position) {
       ...
       ViewGroup.LayoutParams photoParams = holder.photo.getLayoutParams(); //Параметры нашей фотографии
    
       int newImageViewSize = screenWidth/columnsCount; //Новый размер фотографии
    
       photoParams.width = newImageViewSize; //Установка нового размера
       photoParams.height = newImageViewSize;
       holder.photo.setLayoutParams(photoParams); //Установка параметров
       ...
    }

두 방향 모두에서 작동합니다. 세로에는 2 개의 열이 있고 가로에는 4 개의 열이 있습니다. 결과 : https://i.stack.imgur.com/WHvyD.jpg



1

이것은 recyclerview 자체가 크기를 변경할 때 사소한 수정이있는 s.maks의 클래스입니다. 방향 변경을 직접 처리 할 때 (manifest에서 android:configChanges="orientation|screenSize|keyboardHidden") 또는 mColumnWidth 변경없이 recyclerview 크기가 변경 될 수있는 다른 이유가 있습니다. 또한 크기의 리소스가되는 데 필요한 int 값을 변경하고 리소스가없는 생성자를 허용 한 다음 setColumnWidth가이를 수행하도록 허용했습니다.

public class GridAutofitLayoutManager extends GridLayoutManager {
    private Context context;
    private float mColumnWidth;

    private float currentColumnWidth = -1;
    private int currentWidth = -1;
    private int currentHeight = -1;


    public GridAutofitLayoutManager(Context context) {
        /* Initially set spanCount to 1, will be changed automatically later. */
        super(context, 1);
        this.context = context;
        setColumnWidthByResource(-1);
    }

    public GridAutofitLayoutManager(Context context, int resource) {
        this(context);
        this.context = context;
        setColumnWidthByResource(resource);
    }

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

    public void setColumnWidthByResource(int resource) {
        if (resource >= 0) {
            mColumnWidth = context.getResources().getDimension(resource);
        } else {
            /* 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. */
            mColumnWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48,
                    context.getResources().getDisplayMetrics());
        }
    }

    public void setColumnWidth(float newColumnWidth) {
        mColumnWidth = newColumnWidth;
    }

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        recalculateSpanCount();
        super.onLayoutChildren(recycler, state);
    }

    public void recalculateSpanCount() {
        int width = getWidth();
        if (width <= 0) return;
        int height = getHeight();
        if (height <= 0) return;
        if (mColumnWidth <= 0) return;
        if ((width != currentWidth) || (height != currentHeight) || (mColumnWidth != currentColumnWidth)) {
            int totalSpace;
            if (getOrientation() == VERTICAL) {
                totalSpace = width - getPaddingRight() - getPaddingLeft();
            } else {
                totalSpace = height - getPaddingTop() - getPaddingBottom();
            }
            int spanCount = (int) Math.max(1, Math.floor(totalSpace / mColumnWidth));
            setSpanCount(spanCount);
            currentColumnWidth = mColumnWidth;
            currentWidth = width;
            currentHeight = height;
        }
    }
}

-1

spanCount를 큰 수 (최대 열 수)로 설정하고 사용자 지정 SpanSizeLookup을 GridLayoutManager로 설정합니다.

mLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
    @Override
    public int getSpanSize(int i) {
        return SPAN_COUNT / (int) (mRecyclerView.getMeasuredWidth()/ CELL_SIZE_IN_PX);
    }
});

약간 못 생겼지 만 작동합니다.

AutoSpanGridLayoutManager와 같은 관리자가 최상의 솔루션이라고 생각하지만 그런 것은 찾지 못했습니다.

편집 : 버그가 있으며 일부 장치에서는 오른쪽에 빈 공간을 추가합니다.


오른쪽의 공간은 버그가 아닙니다. 스팬 수가 5이고 getSpanSize3을 반환하면 스팬을 채우지 않기 때문에 공백이 생깁니다.
Nicolas Tyler

-6

다음은 스팬 수를 자동 감지하는 데 사용했던 래퍼의 관련 부분입니다. R.layout.my_grid_item 참조 로 호출 setGridLayoutManager하여 초기화하고 각 행에 들어갈 수있는 항목 수를 파악합니다.

public class AutoSpanRecyclerView extends RecyclerView {
    private int     m_gridMinSpans;
    private int     m_gridItemLayoutId;
    private LayoutRequester m_layoutRequester = new LayoutRequester();

    public void setGridLayoutManager( int orientation, int itemLayoutId, int minSpans ) {
        GridLayoutManager layoutManager = new GridLayoutManager( getContext(), 2, orientation, false );
        m_gridItemLayoutId = itemLayoutId;
        m_gridMinSpans = minSpans;

        setLayoutManager( layoutManager );
    }

    @Override
    protected void onLayout( boolean changed, int left, int top, int right, int bottom ) {
        super.onLayout( changed, left, top, right, bottom );

        if( changed ) {
            LayoutManager layoutManager = getLayoutManager();
            if( layoutManager instanceof GridLayoutManager ) {
                final GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
                LayoutInflater inflater = LayoutInflater.from( getContext() );
                View item = inflater.inflate( m_gridItemLayoutId, this, false );
                int measureSpec = View.MeasureSpec.makeMeasureSpec( 0, View.MeasureSpec.UNSPECIFIED );
                item.measure( measureSpec, measureSpec );
                int itemWidth = item.getMeasuredWidth();
                int recyclerViewWidth = getMeasuredWidth();
                int spanCount = Math.max( m_gridMinSpans, recyclerViewWidth / itemWidth );

                gridLayoutManager.setSpanCount( spanCount );

                // if you call requestLayout() right here, you'll get ArrayIndexOutOfBoundsException when scrolling
                post( m_layoutRequester );
            }
        }
    }

    private class LayoutRequester implements Runnable {
        @Override
        public void run() {
            requestLayout();
        }
    }
}

1
수락 된 답변에 반대 투표하는 이유.
반대 투표
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.