最近UI修改的时候,出现了一种world文档的两端对齐UI,如图:
乍一看,这不就是GridLayoutManager设置吗,然后一套代码撸下来
CmsAdapter cmsAdapter = new CmsAdapter(R.layout.item_mall_cms_layout_5, cmsList) {
@Override
protected void convert(BaseViewHolder helper, StoreyImgTxtBean item) {
super.convert(helper, item);
helper.setText(R.id.tv_cms_name, item.getName());
ImageView imageView = helper.getView(R.id.iv_cms);
GlideHelper.load240p(mContext, item.getLogoPath(), imageView);
imageView.setOnClickListener(v -> handleClickEvent(item));
}
};
recyclerView.setLayoutManager(gridLayoutManager);
recyclerView.setAdapter(cmsAdapter);
点击运行,完结,撒花运行验收!
UI妹子一看就说我这个不对,太丑了
仔细一看,确实距离间距有所差别,无奈UI妹子强迫修改,网上也没有太多这种操作,既然不能白嫖,那就只能自己动啦~!
GridLayoutManager 布局分析
由于recyclerView的布局绘制是交个LayoutManager来管理的,原理在recyclerView的onMeasure方法调用了
setLayoutManager的onLayoutChildren
private void dispatchLayoutStep1() {
....
mLayout.onLayoutChildren(mRecycler, mState);
....
}
private void dispatchLayoutStep2() {
....
mLayout.onLayoutChildren(mRecycler, mState);
....
}
这两个方法主要是负责布局测量过后的itemView
接下来,我们回看GridLayoutManager.onLayoutChildren
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (state.isPreLayout()) {
cachePreLayoutSpanMapping();
}
super.onLayoutChildren(recycler, state);
if (DEBUG) {
validateChildOrder();
}
clearPreLayoutSpanMappingCache();
}
这里只调用了他的super,所以又回到LinearLayoutManager.onLayoutChildren
关键代码
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
fill(recycler, mLayoutState, state, false);
}
/**
* The magic functions :). Fills the given layout, defined by the layoutState. This is fairly
* independent from the rest of the {@link android.support.v7.widget.LinearLayoutManager}
* and with little change, can be made publicly available as a helper class.
*
* @param recycler Current recycler that is attached to RecyclerView
* @param layoutState Configuration on how we should fill out the available space.
* @param state Context passed by the RecyclerView to control scroll steps.
* @param stopOnFocusable If true, filling stops in the first focusable new child
* @return Number of pixels that it added. Useful for scroll functions.
*/
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
}
注释意思是根据layoutState来填充recycler的itemView
追踪fill的代码,发现里面有一个while函数,该函数也正是绘制itemView
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunk(recycler, state, layoutState, layoutChunkResult);
}
那么关键的绘制代码就在layoutChunk里面,接下来我们只需要关心这个chunk到底是怎么绘制的,就能了解layoutmanager是如何绘制itemView,整体来说就是在while函数里面不停的测量绘制,值得注意的是这里有个
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
//find itemView
View view = layoutState.next(recycler);
...
LayoutParams params = (LayoutParams) view.getLayoutParams();
if (layoutState.mScrapList == null) {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
//根据layoutState来add布局
addView(view);
} else {
addView(view, 0);
}
} else {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addDisappearingView(view);
} else {
addDisappearingView(view, 0);
}
}
//测量子布局
measureChildWithMargins(view, 0, 0);
.....
//布局子view
layoutDecoratedWithMargins(view, left, top, right, bottom);
}
所以可以看到的是,recyclerView通过layoutManager的layoutChunk来布局itemView的
那么我们现在回到GridLayoutManager
GridLayoutManager的layoutChunk
@Override
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
....
//reset计算itemView的右边框的距离
if (flexibleInOtherDir) {
updateMeasurements(); // reset measurements
}
}
GridLayoutManager将计算每行布局所空余的空间大小
static int[] calculateItemBorders(int[] cachedBorders, int spanCount, int totalSpace) {
//确定数组大小为spanCount + 1] 因为 cachedBorders[0] = 0,
if (cachedBorders == null || cachedBorders.length != spanCount + 1
|| cachedBorders[cachedBorders.length - 1] != totalSpace) {
cachedBorders = new int[spanCount + 1];
}
cachedBorders[0] = 0;
//每个布局占据多少控件
int sizePerSpan = totalSpace / spanCount;
//剩余的布局间隙
int sizePerSpanRemainder = totalSpace % spanCount;
int consumedPixels = 0;
int additionalSize = 0;
for (int i = 1; i <= spanCount; i++) {
//根据设置的spanCount,来计算正确的剩余的距离
int itemSize = sizePerSpan;
//一共布局完多出的空间
additionalSize += sizePerSpanRemainder;
if (additionalSize > 0 && (spanCount - additionalSize) < sizePerSpanRemainder) {
itemSize += 1;
additionalSize -= spanCount;
}
consumedPixels += itemSize;
//这里将每个itemView的剩余空间存储起来
cachedBorders[i] = consumedPixels;
}
return cachedBorders;
}
在GridLayoutManager layoutChunk中,可以看到
for (int i = 0; i < count; i++) {
//根据mCachedBorders存储的多余间隙来计算每个位置的位置
View view = mSet[i];
LayoutParams params = (LayoutParams) view.getLayoutParams();
if (mOrientation == VERTICAL) {
if (isLayoutRTL()) {
right = getPaddingLeft() + mCachedBorders[mSpanCount - params.mSpanIndex];
left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
} else {
left = getPaddingLeft() + mCachedBorders[params.mSpanIndex];
right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
}
} else {
top = getPaddingTop() + mCachedBorders[params.mSpanIndex];
bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
}
// 根据位置,来布局每个itemView
layoutDecoratedWithMargins(view, left, top, right, bottom);
// Consume the available space if the view is not removed OR changed
if (params.isItemRemoved() || params.isItemChanged()) {
result.mIgnoreConsumed = true;
}
result.mFocusable |= view.hasFocusable();
}
最后的布局代码
public void layoutDecoratedWithMargins(View child, int left, int top, int right,
int bottom) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Rect insets = lp.mDecorInsets;
child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin,
right - insets.right - lp.rightMargin,
bottom - insets.bottom - lp.bottomMargin);
}
一目了然;
也就是说,recyclerView的layoutmanager在布局的时候会频繁的计算位置,那么要做到两端对齐也就是要从新计算每个布局的间距,在GridLayoutManager中,recyclerView的itemView是存储在mSet[]这个数组里面的
private void ensureViewSet() {
if (mSet == null || mSet.length != mSpanCount) {
mSet = new View[mSpanCount];
}
}
也是在layoutChunk addView(mSet[i])
layoutChunk里面太多recyclerView自己的操作,重写这个肯定不现实,那么久来个简单暴力的自我子酸吧
计算GridLayoutManager布局的间隙
xml item布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/lay_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:paddingTop="5dp">
<LinearLayout
android:id="@+id/ly_content_cms"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/iv_cms"
android:layout_width="56dp"
android:layout_height="56dp"
tools:background="@drawable/pic_default" />
<TextView
android:id="@+id/tv_cms_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:textColor="@color/new_black_33333"
android:textSize="11sp"
tools:text="获客爆款" />
</LinearLayout>
</LinearLayout>
LinearLayout lyParent = helper.getView(R.id.lay_parent);
//左右布局5个margin,colum表示设置的spanCount
if (helper.getAdapterPosition() % colum == 0) {
RecyclerView.LayoutParams leftParamsStart = (RecyclerView.LayoutParams) lyParent.getLayoutParams();
leftParamsStart.leftMargin = ConvertUtils.dp2px(5);
lyParent.setLayoutParams(leftParamsStart);
//父布局坐对齐
lyParent.setGravity(Gravity.START);
}
if (helper.getAdapterPosition() > 0 && (helper.getAdapterPosition() + 1) % colum == 0) {
RecyclerView.LayoutParams rightParamsEnd = (RecyclerView.LayoutParams) lyParent.getLayoutParams();
rightParamsEnd.rightMargin = ConvertUtils.dp2px(5);
lyParent.setLayoutParams(rightParamsEnd);
//父布局右对齐
lyParent.setGravity(Gravity.END);
}
以上就是左右两端对齐,那么现在来计算中间item的左右margin
LinearLayout lyCms = helper.getView(R.id.ly_content_cms);
lyCms.post(() -> {
//每个item内容的一半
float rItem = lyCms.getWidth() / 2f;
//每个item的半宽=(0可绘制内容(整行)-上面给的左右5个padding)/
float b = (ScreenUtils.getScreenWidth() - ConvertUtils.dp2px(5) * 2) / (colum * 2f);
//那么每个item左右边距等于b-rItem
float aSpace = b - rItem;
//处理非首尾
//假设colum=4,4列的中间,一个marginLeft 1/3 aSpace,一个marginRight 1/3 aSpace
if (helper.getAdapterPosition() % colum != 0 && (helper.getAdapterPosition() + 1) % colum != 0) {
if (helper.getAdapterPosition() % 2 == 0) {
//奇数行
LinearLayout.LayoutParams leftParams = (LinearLayout.LayoutParams) lyCms.getLayoutParams();
leftParams.leftMargin = (int) Math.floor(aSpace / (colum - 1)) - 2;//微调2个像素
lyCms.setLayoutParams(leftParams);
} else {
//偶数行
LinearLayout.LayoutParams rightParams = (LinearLayout.LayoutParams) lyCms.getLayoutParams();
rightParams.rightMargin = (int) Math.floor(aSpace / (colum - 1)) - 2;
lyCms.setLayoutParams(rightParams);
}
}
});
代码可能不好理解,画张图就很好理解了
字是有点丑,大致画一下就理解了,哈哈~!
总结
其实没有几行代码,这里一开始抛出问题是想看看GridLayoutManager源码是否支持该设置,然后阅读源码的过程中
理解了recyclerView布局是通过layoutmanager,在while循环里层层计算而来,所以采用自己计算布局的方法来实现,完结,撒花~