web-dev-qa-db-ja.com

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

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

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

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

  • RecyclerViewname__にOnLayoutChangeListenerを追加します
  • このコールバックで、単一の「グリッド項目」を膨らませて測定します
  • spanCount = recyclerViewWidth/singleItemWidth;

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

95
foo64

個人的には、このためにRecyclerViewをサブクラス化するのは好きではありません。なぜなら、私にとっては、スパンカウントを検出するGridLayoutManagerの責任があるように思われるからです。そのため、RecyclerViewとGridLayoutManagerのAndroidソースコードをいくつか掘り下げた後、仕事を行う独自のクラス拡張GridLayoutManagerを作成しました。

public class GridAutofitLayoutManager extends GridLayoutManager
{
    private int mColumnWidth;
    private boolean mColumnWidthChanged = true;

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

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

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

    public void setColumnWidth(int newColumnWidth)
    {
        if (newColumnWidth > 0 && newColumnWidth != mColumnWidth)
        {
            mColumnWidth = newColumnWidth;
            mColumnWidthChanged = true;
        }
    }

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)
    {
        int width = getWidth();
        int height = getHeight();
        if (mColumnWidthChanged && mColumnWidth > 0 && width > 0 && height > 0)
        {
            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;
        }
        super.onLayoutChildren(recycler, state);
    }
}

OnLayoutChildrenでスパンカウントの設定を選択した理由を実際には覚えていませんが、このクラスは少し前に書きました。しかし、ポイントは、ビューが測定された後にそうする必要があるということです。そのため、高さと幅を取得できます。

EDIT:スパンカウントを誤って設定する原因となったコードのエラーを修正します。ユーザーに感謝します @ Elyees Abouda レポートと提案 ソリューション

108
s.maks

ビューツリーオブザーバーを使用してこれを達成し、レンダリングされたレシルサービューの幅を取得し、リソースからカードビューの固定寸法を取得し、計算後にスパンカウントを設定しました。表示するアイテムの幅が固定されている場合にのみ実際に適用されます。これにより、画面のサイズや向きに関係なく、自動的にグリッドにデータが入力されました。

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

まあ、これは私が使用したもので、かなり基本的なものですが、仕事は完了です。このコードは基本的にディップで画面の幅を取得し、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);
13
Ribs

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

私の場合のように誰かが奇妙な列幅を取得した場合に備えて、これを投稿しています。

私の評判が低いため、 @ s-marks の回答にコメントすることはできません。私は彼のソリューションを適用しました solution が、奇妙な列幅を取得したので、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に変換することにより、問題が修正されました。

7
Ariq

Upvotedソリューションは問題ありませんが、入力値をピクセルとして処理するため、dpをテストおよび想定するために値をハードコーディングしている場合、手間がかかります。最も簡単な方法は、GridAutofitLayoutManagerを構成するときに列幅をディメンションに配置して読み取ることです。これにより、dpが正しいピクセル値に自動的に変換されます。

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

上記の答えは こちら

2
Omid Raha

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
Andreas
  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
Serjant.arbuz

これは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;
        }
    }
}
0
Tatarize