RecyclerView GridLayoutManager: ऑटो-डिटेक्ट स्पैन काउंट कैसे करें?


110

नए GridLayoutManager का उपयोग करना: https://developer.android.com/reference/android/support/v7/widget/GridLayoutManager.html

यह एक स्पष्ट स्पैन काउंट लेता है, इसलिए अब यह समस्या बन गई है: आप कैसे जानते हैं कि प्रति पंक्ति में कितने "स्पैन" फिट होते हैं? यह एक ग्रिड है, सब के बाद। मापा चौड़ाई के आधार पर, रिसाइक्लर व्यू फिट किए जा सकते हैं।

पुराने का उपयोग करते हुए GridView, आप बस "कॉलमविद" संपत्ति सेट करेंगे और यह स्वचालित रूप से पता लगाएगा कि कितने कॉलम फिट हैं। यह मूल रूप से वही है जो मैं RecyclerView के लिए दोहराना चाहता हूं:

  • पर OnLayoutChangeListener जोड़ें RecyclerView
  • इस कॉलबैक में, एकल 'ग्रिड आइटम' को फुलाएं और इसे मापें
  • स्पैनकाउंट = recyclerViewWidth / singleItemWidth;

यह बहुत आम व्यवहार की तरह लगता है, तो क्या एक सरल तरीका है जो मैं नहीं देख रहा हूं?

जवाबों:


122

Personaly मुझे इसके लिए RecyclerView को उप-वर्ग करना पसंद नहीं है, क्योंकि मेरे लिए ऐसा लगता है कि स्प्रेड काउंट का पता लगाने की ज़िम्मेदारी GridLayoutManager की है। तो कुछ एंड्रॉइड सोर्स कोड को RecyclerView और 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 में स्पैन काउंट क्यों चुना, मैंने कुछ समय पहले यह क्लास लिखी थी। लेकिन यह देखने की जरूरत है कि बिंदु हमें मापा जाता है। तो हम इसे ऊंचाई और चौड़ाई प्राप्त कर सकते हैं।

EDIT 1: गलत तरीके से सेटिंग स्पैन की वजह से कोड में त्रुटि ठीक हुई। धन्यवाद उपयोगकर्ता @Elyees Abouda रिपोर्टिंग और समाधान का सुझाव देने के लिए ।

EDIT 2: मैनुअल ओरिएंटेशन में बदलाव के साथ कुछ छोटे रिफैक्टरिंग और फिक्स एज केस। रिपोर्ट करने और समाधान का सुझाव देने के लिए धन्यवाद उपयोगकर्ता @tatarize


8
यह स्वीकृत उत्तर होना चाहिए, LayoutManagerबच्चों को बाहर रखना काम है और RecyclerView's
mewa

3
@ s.maks: मेरा मतलब है कॉलम एडेप्टर में पारित डेटा के आधार पर भिन्न होगा। यह कहें कि यदि चार अक्षरों के शब्द पास हो गए हैं तो उसे एक पंक्ति में चार वस्तुओं को समायोजित करना चाहिए यदि 10 अक्षरों के शब्दों को पारित किया जाता है तो उसे एक पंक्ति में केवल 2 वस्तुओं को समायोजित करने में सक्षम होना चाहिए।
शुभम

2
मेरे लिए, डिवाइस को घुमाते समय, यह ग्रिड का आकार थोड़ा बदल रहा है, 20dp सिकुड़ रहा है। इस बारे में कोई जानकारी मिली? धन्यवाद।
अलेक्जेंड्रे 13

3
दृश्य बनाए जाने से पहले कभी-कभी getWidth()या getHeight()0 होता है, जो एक गलत स्पैनकाउंट मिलेगा (1 चूंकि कुल रिक्त स्थान <= 0 होगा)। मैंने जो जोड़ा है वह इस मामले में सेटपैनकाउंट की अनदेखी कर रहा है। ( onLayoutChildrenबाद में फिर बुलाया जाएगा)
एलिसा अबौदा

3
एक किनारे की स्थिति है जो कवर नहीं है। यदि आप कॉन्फिगरेशन को इस तरह सेट करते हैं कि आप रोटेशन को संभालने के बजाय इसे पूरी गतिविधि को फिर से बनाने दें, तो आपके पास यह अजीब मामला है कि रिसाइक्लेर्यू की चौड़ाई बदलती है, जबकि कुछ और नहीं करता है। परिवर्तित चौड़ाई और ऊंचाई के साथ स्पैन्काउंट गंदा है, लेकिन mColumnWidth परिवर्तित नहीं हुआ है, इसलिए onLayoutChildren () गर्भपात करता है और अब गंदे मूल्यों को पुनर्गणना नहीं करता है। पिछली ऊंचाई और चौड़ाई को सहेजें, और ट्रिगर करें यदि यह गैर-शून्य तरीके से बदलता है।
ताताराइज करें

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

बस जोड़ें mLayoutManager.requestLayout () setSpanCount के बाद () और यह काम करता है
alvinmeimoun

2
नोट: removeGlobalOnLayoutListener()एपीआई स्तर 16 में पदावनत किया गया है removeOnGlobalLayoutListener()। इसके बजाय उपयोग करें । प्रलेखन
pez

टाइपो removeOnGLobalLayoutListener होना चाहिए removeOnGlobalLayoutListener
थियो

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 लेआउट के साथ
तालमेल में रखने की आवश्यकता है

@ xoo64 की xml में आप आइटम पर सिर्फ match_parent सेट कर सकते हैं। लेकिन हाँ, यह अभी भी बदसूरत है;)
फिलिप गिउलियानी

15

मैंने RecyclerView बढ़ाया और ऑनमार्ट विधि को ओवररोड किया।

मैं एक आइटम की चौड़ाई (सदस्य चर) को जितनी जल्दी हो सके, 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 चिउ-की चान के पास इस दृष्टिकोण को रेखांकित करने वाला एक ब्लॉग पोस्ट है और इसके लिए एक नमूना परियोजना भी है।
कॉमन्सवेयर

7

मैं इसे सिर्फ पोस्ट कर रहा हूँ अगर किसी को मेरे मामले में अजीब कॉलम चौड़ाई मिलती है।

मैं अपनी कम प्रतिष्ठा के कारण @ s- मार्क्स के उत्तर पर टिप्पणी करने में सक्षम नहीं हूं । मैंने उसका समाधान हल लागू किया लेकिन मुझे कुछ अजीब कॉलम की चौड़ाई मिली, इसलिए मैंने संशोधित चेकलाइन कॉलम फ़ंक्शन को निम्नानुसार संशोधित किया :

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 में परिवर्तित हो जाएगा:

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

हाँ, यह मुझे एक पाश के लिए फेंक दिया। वास्तव में यह केवल संसाधन को स्वीकार करना होगा। मेरा मतलब है कि हमेशा की तरह हम क्या कर रहे हैं।
तांत्रिक करें

2
  1. ImageView की न्यूनतम निश्चित चौड़ाई सेट करें (उदाहरण के लिए 144dp x 144dp)
  2. जब आप GridLayoutManager बनाते हैं, तो आपको यह जानने की आवश्यकता होती है कि छवि दृश्य के न्यूनतम आकार के साथ कितने स्तंभ होंगे:

    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. उसके बाद आपको एडॉप्टर में इमेज व्यू का आकार बदलना होगा, यदि आपके पास कॉलम में जगह है। आप 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 का वर्ग है जिसके लिए एक छोटे से फिक्स के साथ recyclerview ही आकार बदलता है। जैसे कि जब आप अभिविन्यास से निपटते हैं तो अपने आप को (प्रकट में android:configChanges="orientation|screenSize|keyboardHidden") बदल देते हैं, या किसी अन्य कारण से रिसाइकलव्यू आकार में बदलाव के बिना mColumnWidth बदल सकता है। मैंने उस इंट वैल्यू को भी बदल दिया जो आकार का संसाधन है और बिना किसी संसाधन के एक निर्माता को सेट करने की अनुमति देता है फिर स्वयं को ऐसा करने के लिए सेट करें।

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

एक बड़ी संख्या (जो स्तंभ की अधिकतम संख्या है) पर स्पैनकाउंट सेट करें और एक कस्टम 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 है तो स्पेस होगा क्योंकि आप स्पैन नहीं भर रहे हैं।
निकोलस टायलर

-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
क्यों स्वीकार किए जाते हैं जवाब देने के लिए। समझाना चाहिए कि क्यों
डाउनवोट
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.