ItemDecoration實現等分間距

一.背景

遠古時代,GridView 和 ListView 可以直接使用其自帶的 api 設置 item 之間的分割線,通過修改分割線的粗細和顏色等可以輕鬆實現分割線和間距類的效果,還有的直接通過在 item 的佈局裏設置 margin 或 padding 來實現,後來有了 RecyclerVIew,但是卻沒提供設置分割線的 api,不過提供了一個功能豐富的 ItemDecoration 類,這個類首先能實現的最簡單的功能就是分割線了,這裏先來記錄一下關鍵方法 getItemOffsets 相關用法,裏面的 outRect 容易理解錯。
在這裏插入圖片描述
在這裏插入圖片描述

二.方案

tv 端應用裏有很多這種間距平分的頁面,如果不做任何處理,一般你會把 item 寬高設置固定值,但是因爲沒控制間距,總會出現間距不平分,item 會往左靠,最右邊可能會有一部分空白,各 item 間左右會有間距,但是並不是你代碼設置的,想修改也沒地方改,這樣肯定達不到設計圖上的效果。

推薦方式(設計圖尺寸是19201080):
item 寬度設置 match_parent,根據UI圖上的 item 間距尺寸和距離屏幕邊緣的尺寸來控制頁面的尺寸,例如上圖中(中間那部分黃色背景的RecyclerView)每個 item 之間的間距是48px,item 寬高分別爲 250px,331px,距離父元素 RecyclerView 左右 90px,上 24px,下 36px,拿到這種設計圖,可以這樣來搞:
1.不用關心 item 的寬,只需要關心高和間距,以及距離父元素的距離;
2.通過 ItemDecoration 來設置 item 之間的間距;
3.不用 ItemDecoration 設置 item 距離父元素 RecyclerView 的距離,而是通過父元素自己設置 padding 來控制;
這樣可以精細的控制 UI 效果,例如設置完間距後,間距的總寬度是 48
5+90*2 = 420,剩下的會因 item 寬度設置 match_parent 平分成 6 份 (1920-420)/6 = 250px,正好和設計圖中完全對上。

三.實現

3.1先按方案中第一步的思路來設置 item 佈局:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#c2b8b8"
    android:focusable="true"
    >
    <ImageView
        android:id="@+id/image"
        android:layout_width="match_parent"
        android:layout_height="@dimen/px_331"
        android:src="@mipmap/ic_img"
        android:clipChildren="false"
        android:scaleType="fitXY"/>
    <TextView
        android:id="@+id/textview"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/image"
        android:layout_marginEnd="@dimen/px_12"
        android:layout_marginStart="@dimen/px_12"
        android:layout_marginTop="@dimen/px_2"
        android:textColor="#333333"
        android:textSize="@dimen/px_31"
        />
</RelativeLayout>

3.2自定義 ItemDecoration 實現間距:
後面全拿垂直的風格佈局來說,這裏有個容易理解錯的地方,getItemOffsets 方法裏的 outRect 可以理解成是包在 item 佈局外面的一個參與 item 測量的框,不能理解成是每個 item 之間的只用來佔位的一個 rect,如果理解錯誤會導致這樣一種思路,如果是第一列,就設置 outRect.left=0,其它全部只設置 outRect.left=mSpace,outRect.right=0,或者全設置右邊等於mSpace,左邊等於0,最後一列右邊等於0,程序運行起來會出現第一列的 item 的寬度都比其它的 item 寬的情況:

	@Override
    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent,
                               @NonNull RecyclerView.State state) {
        ...
        int position = parent.getChildAdapterPosition(view);
        int column = position % spanCount;

        if (column == 0) { // 如果是第一列,不設置左邊間距
            outRect.left = 0;
        } else { // 其它列全設置左邊的間距
            outRect.left = mSpace;
        }
    }

在這裏插入圖片描述
上圖效果就能很好的說明了 outRect 是包在 item 外的一層偏移,第一列 item 尺寸問題可在源碼中找到原因:

    Rect getItemDecorInsetsForChild(View child) {
        ...
        final Rect insets = lp.mDecorInsets;
        insets.set(0, 0, 0, 0);
        final int decorCount = mItemDecorations.size();
        for (int i = 0; i < decorCount; i++) {
            mTempRect.set(0, 0, 0, 0);
            mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
            insets.left += mTempRect.left;
            insets.top += mTempRect.top;
            insets.right += mTempRect.right;
            insets.bottom += mTempRect.bottom;
        }
        lp.mInsetsDirty = false;
        return insets;
    }
...
    public void measureChildWithMargins(View child, int widthUsed, int heightUsed) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();

        final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
        widthUsed += insets.left + insets.right;
        heightUsed += insets.top + insets.bottom;

        final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
                                                  getPaddingLeft() + getPaddingRight()
                                                  + lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
                                                  canScrollHorizontally());
        final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
                                                   getPaddingTop() + getPaddingBottom()
                                                   + lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
                                                   canScrollVertically());
        if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
            child.measure(widthSpec, heightSpec);
        }
	}

上面代碼 29 行,獲取自定義 ItemDecoration 中設置的 outRect,然後傳到33行的 getChildMeasureSpec() 方法中:

public static int getChildMeasureSpec(int parentSize, int parentMode, int padding,
                int childDimension, boolean canScroll) {
            int size = Math.max(0, parentSize - padding);
            int resultSize = 0;
            int resultMode = 0;
    ...
            if (childDimension >= 0) {
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    resultSize = size;
                    resultMode = parentMode;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    resultSize = size;
                    if (parentMode == MeasureSpec.AT_MOST || parentMode == MeasureSpec.EXACTLY) {
                        resultMode = MeasureSpec.AT_MOST;
                    } else {
                        resultMode = MeasureSpec.UNSPECIFIED;
                    }
                }
    ...
            //noinspection WrongConstant
            return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
        }

從第三行看到 size 賦的是 parentSize 減去傳進來的 padding 來作爲 item 的寬度不設固定值情況下的最大範圍,由於第一列的 outRect 未設置值,所以這一列的 item 可分配的最大值比其它列多出了 outRect.left 的值,就造成了圖中的錯誤效果。

正確的方式:
實際的結構如下圖:
在這裏插入圖片描述
圖中 insets 就是 getItemOffsets 方法中的 outRect;
上面從源碼中也看到了這個 outRect 和 item 尺寸是有關係的,那麼就可以按這個原則來搞,讓所有的 outRect 的尺寸保持一致,例如所有 item的 outRect.left + outRect.right相等,outRect.top + outRect.bottom 相等;

按這個思路再想讓所有的 item 間距平分,就需要分析計算一下了,下面都只拿橫向平分來說;

計算這些值需要分兩種情況,第一種是第一列和最後一列離兩邊的無邊距,第二種是有邊距,這裏不考慮第二種情況,個人覺得這個邊距由RecyclerView 設置 padding 或 margin 來控制更合適,使用 outRect 是可以控制,但是就感覺像是設置一個垂直LinearLayout 裏的每一個子佈局的 margin_left 和 margin_right 一樣,爲何不統一設置到 LinearLayout 的 padding_left 和 padding_right 上呢。

下面來看無邊距的情況,按設計圖尺寸,space = 48px,那麼各 item 的 outRect.left + ourRect.right = 48 * 5 / 6 = 40px:

position=0:left 0,right 40
position=1:left 8,right 32
position=2:left 16,right 24

position=5:left 40,right 0

每個 item 的 outRect 除了第一列的左邊,其它的左邊等於 40px 減去前一個 item 的 outRect 的右邊,右邊等於 40px 減去自己的左邊,就可以寫成下面這樣:

int position = parent.getChildAdapterPosition(view);
int column = position % spanCount;

int totalSpace = mSpace * (spanCount - 1);// 48*5=240
int itemSpace = totalSpace / spanCount;// 240/6=40
int the = mSpace - itemSpace;// 等差數列的值 48 - 40 = 8

outRect.left = column*the;// 0, 8, 16, 24
outRect.right = itemSpace-column*the;

然後把上面的計算一頓合併同類項,最終變成如下代碼:

package com.lcp.tvtest;

import android.graphics.Rect;
import android.support.annotation.NonNull;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.view.View;

/**
 * Created by Aislli on 2019/10/24 0024.
 */
public class GridItemDecoration extends RecyclerView.ItemDecoration {
    private int mSpace = 20;

    public GridItemDecoration(int space) {
        this.mSpace = space;
    }

    @Override
    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, 
                               @NonNull RecyclerView.State state) {
        int spanCount;
        if (parent.getLayoutManager() instanceof StaggeredGridLayoutManager) {
            StaggeredGridLayoutManager manager = (StaggeredGridLayoutManager) parent.getLayoutManager();
            spanCount = manager.getSpanCount();
        } else if (parent.getLayoutManager() instanceof GridLayoutManager) {
            GridLayoutManager manager = (GridLayoutManager) parent.getLayoutManager();
            spanCount = manager.getSpanCount();
        } else {
            spanCount = 1;
        }
        int position = parent.getChildAdapterPosition(view);
        int column = position % spanCount;

        outRect.left = column * mSpace / spanCount;
        outRect.right = mSpace - (column + 1) * mSpace / spanCount;

        if (position >= spanCount) {
            outRect.top = mSpace;
        }
    }
}

再設置 parent 的 padding 達到設置圖中四邊間距的效果

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ListActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#66e6e675"
        android:clipToPadding="false"
        android:paddingBottom="@dimen/px_36"
        android:paddingEnd="@dimen/px_90"
        android:paddingStart="@dimen/px_90"
        android:paddingTop="@dimen/px_24"
        >
        <!--android:padding="@dimen/px_48"-->
    </android.support.v7.widget.RecyclerView>
</RelativeLayout>

Activity中的測試代碼:

public class ListActivity extends AppCompatActivity {

    private RecyclerView recyclerView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_list);
        recyclerView = findViewById(R.id.recycler);

        ArrayList<String> strings = new ArrayList<>();
        for (int i = 0; i < 12; i++) {
            strings.add("i:" + i);
        }

        GridItemDecoration gridItemDecoration = new GridItemDecoration((int) getResources().getDimension(R.dimen.px_48));
        GridLayoutManager gridLayoutManager = new GridLayoutManager(this, 6);
        ListAdapter listAdapter = new ListAdapter(strings);

        recyclerView.setLayoutManager(gridLayoutManager);
        recyclerView.addItemDecoration(gridItemDecoration);
        recyclerView.setAdapter(listAdapter);
    }
}

這裏就實現文中開始的第二張效果圖樣式了,去掉 RecyclerView 的 padding 就是第一張圖的樣式,其它方向的間距設置,原理和上面一致。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章