RecyclerView GridLayoutManager: làm thế nào để tự động phát hiện số khoảng thời gian?


110

Sử dụng GridLayoutManager mới: https://developer.android.com/reference/android/support/v7/widget/GridLayoutManager.html

Nó cần một số nhịp rõ ràng, vì vậy vấn đề bây giờ trở thành: làm thế nào để bạn biết có bao nhiêu "nhịp" phù hợp với mỗi hàng? Rốt cuộc, đây là một lưới. Cần có bao nhiêu nhịp để RecyclerView có thể phù hợp, dựa trên chiều rộng đo được.

Sử dụng cái cũ GridView, bạn sẽ chỉ cần đặt thuộc tính "columnWidth" và nó sẽ tự động phát hiện có bao nhiêu cột phù hợp. Về cơ bản đây là những gì tôi muốn sao chép cho RecyclerView:

  • thêm OnLayoutChangeListener trên RecyclerView
  • trong lệnh gọi lại này, hãy thổi phồng một 'mục lưới' và đo lường nó
  • spanCount = renderViewWidth / singleItemWidth;

Đây có vẻ là hành vi khá phổ biến, vậy có cách nào đơn giản hơn mà tôi không thấy không?

Câu trả lời:


122

Về cơ bản, tôi không thích phân lớp RecyclerView cho điều này, vì đối với tôi, có vẻ như GridLayoutManager có trách nhiệm phát hiện số lượng khoảng cách. Vì vậy, sau khi đào một số mã nguồn android cho RecyclerView và GridLayoutManager, tôi đã viết GridLayoutManager mở rộng lớp của riêng mình để thực hiện công việc:

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

Tôi thực sự không nhớ lý do tại sao tôi chọn đặt số lượng khoảng cách trong onLayoutChildren, tôi đã viết lớp này một thời gian trước. Nhưng vấn đề là chúng ta cần phải làm như vậy sau khi lượt xem được đo lường. vì vậy chúng tôi có thể lấy chiều cao và chiều rộng của nó.

CHỈNH SỬA 1: Sửa lỗi trong mã do đặt sai số khoảng thời gian. Cảm ơn người dùng @Elyees Abouda đã báo cáo và đề xuất giải pháp .

CHỈNH SỬA 2: Một số cấu trúc lại nhỏ và sửa trường hợp cạnh với xử lý thay đổi hướng thủ công. Cảm ơn người dùng @tatarize đã báo cáo và đề xuất giải pháp .


8
Điều này sẽ là câu trả lời được chấp nhận, nó LayoutManager's công việc đẻ con ra ngoài và không RecyclerView' s
Mewa

3
@ s.maks: Ý tôi là columnWidth sẽ khác nhau tùy thuộc vào dữ liệu được truyền vào bộ điều hợp. Giả sử nếu bốn từ chữ cái được chuyển thì nó sẽ chứa bốn mục liên tiếp nếu 10 từ chữ cái được chuyển thì nó chỉ có thể chứa 2 mục liên tiếp.
Shubham

2
Đối với tôi, khi xoay thiết bị, nó sẽ thay đổi một chút kích thước của lưới, thu nhỏ lại 20dp. Có bất kỳ thông tin về điều này? Cảm ơn.
Alexandre

3
Đôi khi getWidth()hoặc bằng getHeight()0 trước khi chế độ xem được tạo, điều này sẽ nhận được số lượng spanCount sai (1 vì totalSpace sẽ là <= 0). Những gì tôi đã thêm là bỏ qua setSpanCount trong trường hợp này. ( onLayoutChildrensẽ được gọi lại sau)
Elyess Abouda

3
Có một tình trạng cạnh không được bảo hiểm. Nếu bạn đặt configChanges để bạn xử lý xoay thay vì để nó xây dựng lại toàn bộ hoạt động, bạn sẽ gặp trường hợp kỳ lạ là chiều rộng của chế độ xem lại thay đổi, trong khi không có gì khác làm. Với chiều rộng và chiều cao đã thay đổi, spancount bị bẩn, nhưng mColumnWidth không thay đổi, vì vậy onLayoutChildren () hủy bỏ và không tính toán lại các giá trị hiện đã bị bẩn. Lưu chiều cao và chiều rộng trước đó và kích hoạt nếu nó thay đổi theo cách khác không.
Tatarize

29

Tôi đã hoàn thành điều này bằng cách sử dụng trình quan sát cây chế độ xem để có được chiều rộng của chế độ xem lại sau khi được hiển thị và sau đó nhận các kích thước cố định của chế độ xem thẻ của tôi từ các tài nguyên và sau đó đặt số lượng khoảng sau khi thực hiện tính toán của tôi. Nó chỉ thực sự áp dụng nếu các mục bạn đang hiển thị có chiều rộng cố định. Điều này đã giúp tôi tự động điền vào lưới bất kể kích thước hoặc hướng màn hình.

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
Tôi đã sử dụng cái này và nhận được ArrayIndexOutOfBoundsException( tại android.support.v7.widget.GridLayoutManager.layoutChunk (GridLayoutManager.java:361) ) khi cuộn RecyclerView.
fikr4n

Chỉ cần thêm mLayoutManager.requestLayout () sau setSpanCount () và nó hoạt động
alvinmeimoun

2
Lưu ý: removeGlobalOnLayoutListener()không được dùng nữa trong API cấp 16. sử dụng removeOnGlobalLayoutListener()thay thế. Tài liệu .
pez

typo loại bỏOnGLobalLayoutListener nên được xóaOnGlobalLayoutListener
Theo

17

Đây là những gì tôi đã sử dụng, khá cơ bản, nhưng hoàn thành công việc cho tôi. Về cơ bản, mã này lấy chiều rộng màn hình tính bằng điểm sau đó chia cho 300 (hoặc bất kỳ chiều rộng nào bạn đang sử dụng cho bố cục bộ điều hợp của mình). Vì vậy, điện thoại nhỏ hơn với chiều rộng nhúng 300-500 chỉ hiển thị một cột, máy tính bảng 2-3 cột, v.v. Đơn giản, không phiền phức và không có nhược điểm, theo như tôi thấy.

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
Tại sao lại sử dụng chiều rộng màn hình thay vì chiều rộng của RecyclerView? Và hardcoding 300 là xấu thực hành (nó cần phải được giữ đồng bộ với bố cục xml của bạn)
foo64

@ foo64 trong xml, bạn chỉ có thể đặt match_parent trên mục. Nhưng có, nó vẫn xấu xí;)
Philip Giuliani

15

Tôi đã mở rộng RecyclerView và ghi đè phương thức onMeasure.

Tôi đặt chiều rộng mục (biến thành viên) sớm nhất có thể, với mặc định là 1. Điều này cũng cập nhật về cấu hình đã thay đổi. Bây giờ sẽ có nhiều hàng nhất có thể phù hợp với dọc, ngang, điện thoại / máy tính bảng, v.v.

@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 có một bài đăng trên blog phác thảo cách tiếp cận này và cả một dự án mẫu cho nó.
CommonsWare

7

Tôi đăng bài này chỉ trong trường hợp ai đó nhận được chiều rộng cột kỳ lạ như trong trường hợp của tôi.

Tôi không thể bình luận về câu trả lời của @ s-mark do danh tiếng của tôi thấp. Tôi áp dụng giải pháp của mình giải pháp nhưng tôi có một số độ rộng cột lạ, vì vậy tôi sửa đổi checkedColumnWidth chức năng như sau:

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

Bằng cách chuyển đổi chiều rộng cột nhất định thành DP đã khắc phục sự cố.


2

Để phù hợp với sự thay đổi hướng đối với câu trả lời của s-mark , tôi đã thêm một kiểm tra về sự thay đổi chiều rộng (chiều rộng từ getWidth (), không phải chiều rộng cột).

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

Giải pháp được ủng hộ là tốt, nhưng xử lý các giá trị đến dưới dạng pixel, điều này có thể khiến bạn tăng lên nếu bạn đang mã hóa các giá trị để thử nghiệm và giả sử là dp. Cách dễ nhất có lẽ là đặt chiều rộng cột trong một thứ nguyên và đọc nó khi định cấu hình GridAutofitLayoutManager, sẽ tự động chuyển đổi dp thành giá trị pixel chính xác:

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

yeah, điều đó đã khiến tôi phải lo lắng. thực sự nó sẽ chỉ là chấp nhận tài nguyên chính nó. Ý tôi là đó luôn là những gì chúng tôi sẽ làm.
Tatarize

2
  1. Đặt chiều rộng cố định tối thiểu của imageView (ví dụ: 144dp x 144dp)
  2. Khi bạn tạo GridLayoutManager, bạn cần biết có bao nhiêu cột với kích thước tối thiểu của 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. Sau đó, bạn cần thay đổi kích thước imageView trong bộ điều hợp nếu bạn có khoảng trống trong cột. Bạn có thể gửi newImageViewSize sau đó inisilize bộ điều hợp từ hoạt động ở đó bạn tính toán số màn hình và cột:

    @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); //Установка параметров
       ...
    }

Nó hoạt động theo cả hai hướng. Theo chiều dọc, tôi có 2 cột và theo chiều ngang - 4 cột. Kết quả: https://i.stack.imgur.com/WHvyD.jpg



1

Đây là lớp của s.maks với một sửa chữa nhỏ khi bản thân chế độ xem lại thay đổi kích thước. Chẳng hạn như khi bạn tự xử lý các thay đổi hướng (trong tệp kê khai android:configChanges="orientation|screenSize|keyboardHidden") hoặc một số lý do khác mà chế độ xem lại có thể thay đổi kích thước mà không thay đổi mColumnWidth. Tôi cũng đã thay đổi giá trị int mà nó cần là tài nguyên có kích thước và cho phép một phương thức khởi tạo không có tài nguyên sau đó setColumnWidth tự làm điều đó.

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

Đặt spanCount thành một số lớn (là số cột tối đa) và đặt SpanSizeLookup tùy chỉnh thành GridLayoutManager.

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

Nó hơi xấu, nhưng nó hoạt động.

Tôi nghĩ rằng một trình quản lý như AutoSpanGridLayoutManager sẽ là giải pháp tốt nhất, nhưng tôi không tìm thấy bất cứ điều gì như vậy.

CHỈNH SỬA: Có một lỗi, trên một số thiết bị, nó thêm khoảng trống ở bên phải


Khoảng trống bên phải không có lỗi. nếu số lượng khoảng là 5 và getSpanSizetrả về 3 sẽ có một khoảng trắng vì bạn không lấp đầy khoảng.
Nicolas Tyler

-6

Đây là các phần có liên quan của một trình bao bọc mà tôi đã sử dụng để tự động phát hiện số nhịp. Bạn khởi tạo nó bằng cách gọi setGridLayoutManagervới một tham chiếu R.layout.my_grid_item và nó sẽ tìm ra bao nhiêu trong số đó có thể phù hợp trên mỗi hàng.

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
Tại sao lại phản đối câu trả lời được chấp nhận. nên giải thích tại sao downvote
androidXP
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.