RecyclerView GridLayoutManager:スパン数を自動検出する方法は?


110

新しいGridLayoutManagerの使用:https ://developer.android.com/reference/android/support/v7/widget/GridLayoutManager.html

明示的なスパンカウントが必要になるため、問題は次のようになります。行ごとにいくつの「スパン」が収まるかをどのようにして知ることができますか?結局、これはグリッドです。測定された幅に基づいて、RecyclerViewが収まるだけのスパンが必要です。

古いを使用するとGridView、「columnWidth」プロパティを設定するだけで、適合する列の数が自動的に検出されます。これは基本的に、RecyclerViewで複製したいものです。

  • OnLayoutChangeListenerを RecyclerView
  • このコールバックでは、単一の「グリッドアイテム」を膨らませて測定します
  • spanCount = recyclerViewWidth / singleItemWidth;

これはかなり一般的な動作のようですので、私が見ない簡単な方法はありますか?

回答:


122

個人的には、このために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
MEWA

3
@ s.maks:つまり、アダプターに渡されるデータに応じてcolumnWidthは異なります。たとえば、4文字の単語が渡された場合、4つの項目を連続して収容する必要があります。10文字の単語が渡された場合、2つの項目を連続して収容できるはずです。
Shubham

2
私にとって、デバイスを回転させると、グリッドのサイズが少し変化し、20 dpが小さくなります。これについて何か情報を得ましたか?ありがとう。
Alexandre

3
時々、getWidth()またはgetHeight()ビューが作成される前は0であり、誤ったspanCount(totalSpaceは<= 0になるため1)を取得します。この場合、追加したのはsetSpanCountを無視することです。(onLayoutChildren後でまた呼び出されます)
Elyess Abouda 2016年

3
カバーされていないエッジ条件があります。アクティビティ全体を再構築せずにローテーションを処理するようにconfigChangesを設定すると、recyclerviewの幅が変更され、それ以外は変更されないという奇妙なケースがあります。幅と高さが変更された場合、スパンカウントはダーティですが、mColumnWidthは変更されていないため、onLayoutChildren()は中止され、現在ダーティな値を再計算しません。以前の高さと幅を保存し、ゼロ以外の方法で変更された場合にトリガーします。
18

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
私はこれを使用して得たArrayIndexOutOfBoundsExceptionandroid.support.v7.widget.GridLayoutManager.layoutChunk(GridLayoutManager.java:361)でスクロール時)RecyclerView
fikr4n 2014

setSpanCount()の後にmLayoutManager.requestLayout()を追加するだけで機能します
alvinmeimoun

2
注:removeGlobalOnLayoutListener()APIレベル16 removeOnGlobalLayoutListener()では非推奨です。代わりに使用してください。ドキュメンテーション
pez

typo removeOnGLobalLayoutListenerはremoveOnGlobalLayoutListenerである必要があります
Theo

17

まあ、これは私が使用したもので、かなり基本的ですが、仕事は私のために行われます。このコードは基本的に画面の幅をディップで取得してから、300(またはアダプターのレイアウトに使用している幅)で割ります。したがって、300〜500のディップ幅の小さなスマートフォンでは、1列、タブレット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、2015

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

賛成されたソリューションは問題ありませんが、入力値をピクセルとして処理します。テストのために値をハードコーディングし、dpを仮定している場合、これは失敗する可能性があります。最も簡単な方法は、おそらく列幅を次元に入れ、GridAutofitLayoutManagerを構成するときにそれを読み取ることです。これにより、dpが正しいピクセル値に自動的に変換されます。

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

ええ、それはループのために私を投げました。実際には、リソース自体を受け入れるだけです。それはいつも私たちがやっていることのようです。
18

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のクラスで、recyclerview自体のサイズが変更された場合のマイナーな修正が含まれています。マニフェストで方​​向の変更を自分で処理する場合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
なぜ受け入れられた回答に反対票を投じるのか。なぜ反対票を投じるのかを説明する必要があります
androidXP
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.