RecyclerView GridLayoutManager: วิธีตรวจจับการนับสแปนอัตโนมัติ


111

การใช้ GridLayoutManager ใหม่: https://developer.android.com/reference/android/support/v7/widget/GridLayoutManager.html

ต้องใช้การนับช่วงที่ชัดเจนดังนั้นปัญหาจึงกลายเป็น: คุณจะรู้ได้อย่างไรว่า "ช่วง" พอดีต่อแถว? นี่คือเส้นตารางหลังจากทั้งหมด ควรมีระยะห่างมากที่สุดเท่าที่ RecyclerView จะใส่ได้ตามความกว้างที่วัดได้

เมื่อใช้แบบเก่าGridViewคุณจะตั้งค่าคุณสมบัติ "columnWidth" และจะตรวจหาจำนวนคอลัมน์ที่พอดีโดยอัตโนมัติ นี่คือสิ่งที่ฉันต้องการทำซ้ำสำหรับ RecyclerView:

  • เพิ่ม OnLayoutChangeListener บนไฟล์ RecyclerView
  • ในการเรียกกลับนี้ขยาย 'รายการตาราง' รายการเดียวแล้ววัดผล
  • spanCount = รีไซเคิลViewWidth / singleItemWidth;

ดูเหมือนว่าจะเป็นพฤติกรรมทั่วไปดังนั้นมีวิธีที่ง่ายกว่าที่ฉันไม่เห็นหรือไม่?

คำตอบ:


123

ส่วนตัวฉันไม่ชอบคลาสย่อย RecyclerView สำหรับสิ่งนี้เพราะสำหรับฉันดูเหมือนว่ามีความรับผิดชอบของ GridLayoutManager ในการตรวจจับจำนวนช่วง ดังนั้นหลังจากที่ซอร์สโค้ด Android บางตัวขุดสำหรับ RecyclerView และ GridLayoutManager ฉันจึงเขียน 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 จะแตกต่างกันไปขึ้นอยู่กับข้อมูลที่ส่งผ่านไปยังอะแดปเตอร์ บอกว่าถ้ามีการส่งตัวอักษรสี่คำก็ควรรองรับสี่รายการในแถวหากมีการส่งตัวอักษร 10 คำก็จะรองรับได้เพียง 2 รายการในแถว
Shubham

2
สำหรับฉันเวลาหมุนอุปกรณ์มันเปลี่ยนขนาดของกริดนิดหน่อยหด 20dp มีข้อมูลเกี่ยวกับเรื่องนี้หรือไม่? ขอบคุณ.
Alexandre

3
บางครั้งgetWidth()หรือgetHeight()เป็น 0 ก่อนที่จะสร้างมุมมองซึ่งจะได้รับ spanCount ผิด (1 เนื่องจาก totalSpace จะเป็น <= 0) สิ่งที่ฉันเพิ่มคือละเว้น setSpanCount ในกรณีนี้ ( onLayoutChildrenจะเรียกอีกครั้งในภายหลัง)
Elyess Abouda

3
มีเงื่อนไขขอบที่ไม่ครอบคลุม หากคุณตั้งค่า configChanges เพื่อให้คุณจัดการกับการหมุนแทนที่จะปล่อยให้มันสร้างกิจกรรมทั้งหมดขึ้นมาใหม่คุณมีกรณีแปลก ๆ ที่ความกว้างของมุมมองรีไซเคิลเปลี่ยนไปในขณะที่ไม่มีอะไรทำ ด้วยความกว้างและความสูงที่เปลี่ยนไป spancount จึงสกปรก แต่ mColumnWidth ไม่เปลี่ยนแปลงดังนั้น onLayoutChildren () จึงยกเลิกและไม่คำนวณค่าสกปรกในขณะนี้ใหม่ บันทึกความสูงและความกว้างก่อนหน้านี้และทริกเกอร์หากมีการเปลี่ยนแปลงในลักษณะที่ไม่ใช่ศูนย์
Tatarize

29

ฉันทำสิ่งนี้ให้สำเร็จโดยใช้ผู้สังเกตการณ์แผนผังมุมมองเพื่อรับความกว้างของมุมมอง recylcer เมื่อแสดงผลแล้วรับขนาดคงที่ของมุมมองการ์ดของฉันจากทรัพยากรจากนั้นตั้งค่าจำนวนช่วงหลังจากทำการคำนวณของฉัน จะใช้ได้จริงก็ต่อเมื่อรายการที่คุณแสดงมีความกว้างคงที่ สิ่งนี้ช่วยให้ฉันเติมข้อมูลในตารางโดยอัตโนมัติโดยไม่คำนึงถึงขนาดหน้าจอหรือการวางแนว

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

เพียงเพิ่ม mLayoutManager.requestLayout () หลัง setSpanCount () ก็ใช้งานได้
alvinmeimoun

2
หมายเหตุ: removeGlobalOnLayoutListener()เลิกใช้แล้วใน API ระดับ 16 ให้ใช้removeOnGlobalLayoutListener()แทน เอกสารประกอบ .
pez

typo removeOnGLobalLayoutListener ควรจะเอาออก OnGlobalLayoutListener
Theo

17

นี่คือสิ่งที่ฉันใช้ค่อนข้างธรรมดา แต่ทำให้งานสำเร็จสำหรับฉัน โดยทั่วไปโค้ดนี้จะได้รับความกว้างของหน้าจอเป็น dips แล้วหารด้วย 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 และ hardcoding 300 เป็นการปฏิบัติที่ไม่ดี (จำเป็นต้องซิงค์กับเลย์เอาต์ xml ของคุณ)
foo64

@ foo64 ใน xml คุณสามารถตั้งค่า match_parent ในรายการได้ แต่ใช่มันยังน่าเกลียด;)
Philip Giuliani

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 ฉันได้เพิ่มการตรวจสอบการเปลี่ยนแปลงความกว้าง (ความกว้างจาก 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

โซลูชันที่เพิ่มขึ้นนั้นใช้ได้ดี แต่จัดการกับค่าที่เข้ามาเป็นพิกเซลซึ่งจะทำให้คุณเคลื่อนที่ได้หากคุณกำลังเข้ารหัสค่าสำหรับการทดสอบและสมมติว่า 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

นี่คือคลาสของ s.maks ที่มีการแก้ไขเล็กน้อยเมื่อตัวเองรีไซเคิลดูเปลี่ยนขนาด เช่นเมื่อคุณจัดการกับการวางแนวจะเปลี่ยนแปลงตัวเอง (ในรายการandroid:configChanges="orientation|screenSize|keyboardHidden") หรือเหตุผลอื่น ๆ ที่มุมมองรีไซเคิลอาจเปลี่ยนขนาดโดยที่ mColumnWidth ไม่เปลี่ยนแปลง ฉันยังเปลี่ยนค่า 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 และgetSpanSizeส่งกลับ 3 จะมีช่องว่างเนื่องจากคุณไม่ได้เติมช่วง
Nicolas Tyler

-6

นี่คือส่วนที่เกี่ยวข้องของ Wrapper ที่ฉันใช้เพื่อตรวจหาจำนวนช่วงโดยอัตโนมัติ คุณเริ่มต้นด้วยการเรียกsetGridLayoutManagerด้วยข้อมูลอ้างอิงR.layout.my_grid_itemและจะหาจำนวนที่สามารถใส่ได้ในแต่ละแถว

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
ทำไมต้องโหวตให้คำตอบที่ยอมรับ ควรอธิบายว่าทำไม downvote
androidXP
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.