应公司要求,需要实现类似Ins搜索页面的排版效果,乍眼一看,说实话,当时挺懵逼的,脑海里唯一的想法就是 这他妈的产品设计实在是太会玩人了吧,这是虾米效果?怎么搞?根本没见过(然而,这是老板要的效果,作为一个小开发,毕竟是小胳膊拧不过大腿,没办法,只能站起来撸),首先仔细的看了一下布局,发现,这个数据其实是有规律的,心中暗暗窃喜,然后,果断造一波假数据(每十八个数据作为一组,十八张图作为一个item),给RecycleView。然后,轻松的实现了如图所示的效果。
然而,看了后台给的数据,List<Object> dataList; 我沉默了,麻蛋后台不知道给分组一下数据吗?(有种想摔键盘的冲动)数据不满18怎么处理?每个图片的index怎么识别?一堆的麻烦,心中顿时无数只羊驼飞奔而过,直到良久才冷静下来,通过一些热心的网友告知明白,原来这种效果叫做一拖多(一拖N),阿里有开源框架Vlayout(点击进入github源码地址)可以实现类似的效果。终于可以不用自己造轮子了,心中顿时松了一口气,然后果断去万能的github上冲浪了一下。跑了一下demo,看了一下源码。果然,是个很好用的框架啊!感觉像是发现了新大陆,心中无限欢喜。
然而,事实证明我高兴的太早了,1拖2的效果可以轻松实现,嵌套网格,也可以轻松实现,然而,还有一部分2拖1的怎么破啊?Vlayout并没有实现N拖1的功能,赶紧去研究一下源码,然而,事实证明,渣渣就是渣渣,看了大半天得出一个结论,hold不住啊。好烦躁啊。
一拖N,N拖一,在网上搜罗了好久,发现了一堆没用的东西,功夫不负有心人啊,最后还是让我找到了AsymmetricGridView(点击进入github源码地址),如图
绝对可以轻松解决问题啊。最终果断引用,然后轻松实现效果搞定问题,在这里特别奉上对大佬felipecsl开源精神的无上感激。解决了困扰我良久的问题。
作为无数有追求,有理想的开发之一,实现一波效果,绝对不能是我等的最终追求啊,了解原理,并能够从中学习某些东西才是目的。接下来我们就要来理一下AsymmetricGridView的实现原理
框架的内部主要提供了两个控件,AsymmetricGridView和AsymmetricRecyclerView两者的具体用法,本文不讲,github项目介绍已经解释的很详细了。本文主要讲讲个人对项目实现逻辑的理解。
AsymmetricGridView和AsymmetricRecyclerView两者最根本的区别是一个继承的是ListView,(是不是以为会是GridView?,接下来看分析你就明白了)一个是RecycleView,然而,其实现逻辑并没有多大的区别,其实是相同的,毕竟都是列表嘛。当然,在这里,我们还是要把它当成是网格列表来处理。这里以AsymmetricRecyclerView为例,作为网格,首先我们就要设置一下每行有多少列
recyclerView.setRequestedColumnCount(3);//设置三行
我在这里设置了三列。现在我们可以把它想象成一个设置了三列的GridView布局。
说道列表,无外乎两点。
1 数据
2 适配器Adapter
这里的数据,是需要继承AsymmetricItem接口的的。
public interface AsymmetricItem extends Parcelable {
int getColumnSpan();
int getRowSpan();
}
AsymmetricItem里面规定了每条数据可以进行跨行跨列,以demo里面的数据DemoItem为例
public class DemoItem implements AsymmetricItem {
private int columnSpan;
private int rowSpan;
private int position;
public DemoItem() {
this(1, 1, 0);
}
public DemoItem(int columnSpan, int rowSpan, int position) {
this.columnSpan = columnSpan;
this.rowSpan = rowSpan;
this.position = position;
}
public DemoItem(Parcel in) {
readFromParcel(in);
}
@Override public int getColumnSpan() {
return columnSpan;
}
@Override public int getRowSpan() {
return rowSpan;
}
public int getPosition() {
return position;
}
@Override public String toString() {
return String.format("%s: %sx%s", position, rowSpan, columnSpan);
}
@Override public int describeContents() {
return 0;
}
private void readFromParcel(Parcel in) {
columnSpan = in.readInt();
rowSpan = in.readInt();
position = in.readInt();
}
@Override public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeInt(columnSpan);
dest.writeInt(rowSpan);
dest.writeInt(position);
}
/* Parcelable interface implementation */
public static final Parcelable.Creator<DemoItem> CREATOR = new Parcelable.Creator<DemoItem>() {
@Override public DemoItem createFromParcel(@NonNull Parcel in) {
return new DemoItem(in);
}
@Override @NonNull public DemoItem[] newArray(int size) {
return new DemoItem[size];
}
};
}
DemoItem等同于我们从后台拿到的数据dataList里面一层的对象,两者级别一致。只是,我们需要对其进行一下转换继承AsymmetricItem,加入columnSpan,rowSpan,position参数而已,
适配器;
AsymmetricRecyclerViewAdapter与AdapterImpl
适配器里面的逻辑与常用的逻辑并没有什么不同,很好理解,
@Override public AdapterImpl.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return adapterImpl.onCreateViewHolder();
}
@Override public void onBindViewHolder(AdapterImpl.ViewHolder holder, int position) {
adapterImpl.onBindViewHolder(holder, position, recyclerView);
// adapterImpl.onClick(holder.itemView);
}
@Override public int getItemCount() {
// This is the row count for RecyclerView display purposes, not the actual item count
return adapterImpl.getRowCount();
}
由此可见,其内部的逻辑代码主要是由AdapterImpl来进行实现。
adapterImpl.onCreateViewHolder();
adapterImpl.getRowCount();
adapterImpl.onBindViewHolder(holder, position, recyclerView);
首先我们进入adapterImpl内部分析一下adapterImpl.onCreateViewHolder();
ViewHolder onCreateViewHolder() {
if (debugEnabled) {
Log.d(TAG, "onCreateViewHolder()");
}
LinearLayout layout = new LinearLayout(context, null);
if (debugEnabled) {
layout.setBackgroundColor(Color.parseColor("#83F27B"));
}
layout.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE);
layout.setDividerDrawable(
ContextCompat.getDrawable(context, R.drawable.item_divider_horizontal));
AbsListView.LayoutParams layoutParams = new AbsListView.LayoutParams(
AbsListView.LayoutParams.MATCH_PARENT, AbsListView.LayoutParams.WRAP_CONTENT);
layout.setLayoutParams(layoutParams);
return new ViewHolder(layout);
}
实际上是创建了一个没有子控件的LinearLayout,提供给onBindViewHolder()使用。
adapterImpl.onBindViewHolder(holder, position, recyclerView);实现目的效果的核心方法
/**
*holder recycleView Item(LinearLayout)
*position 当前数据对位置(已经被封装处理过的数据)
*parent RecycleView
*/
void onBindViewHolder(ViewHolder holder, int position, ViewGroup parent) {
if (debugEnabled) {
Log.d(TAG, "onBindViewHolder(" + String.valueOf(position) + ")");
}
RowInfo rowInfo = itemsPerRow.get(position);//对原始数据进行了重新排列,已经排列好的一行数据
if (rowInfo == null) {
return;
}
List<RowItem> rowItems = new ArrayList<>(rowInfo.getItems());
LinearLayout layout = initializeLayout(holder.itemView());//初始化缓存了之前的数据留着复用,recycleView的item的布局,
// Index to control the current position of the current column in this row
int columnIndex = 0;
// Index to control the current position in the array of all the items available for this row
int currentIndex = 0;
int spaceLeftInColumn = rowInfo.getRowHeight();//行数据跨列最多的那个为标准
while (!rowItems.isEmpty() && columnIndex < listView.getNumColumns()) {
RowItem currentItem = rowItems.get(currentIndex);
if (spaceLeftInColumn == 0) {
// No more space in this column. Move to next one
columnIndex++;
currentIndex = 0;
spaceLeftInColumn = rowInfo.getRowHeight();
continue;
}
// Is there enough space in this column to accommodate currentItem?
if (spaceLeftInColumn >= currentItem.getItem().getRowSpan()) {
rowItems.remove(currentItem);
int actualIndex = currentItem.getIndex();
int viewType = agvAdapter.getItemViewType(actualIndex);
ObjectPool<AsymmetricViewHolder<?>> pool = viewHoldersMap.get(viewType);
if (pool == null) {
pool = new ObjectPool<>();
viewHoldersMap.put(viewType, pool);
}
AsymmetricViewHolder viewHolder = pool.get();
if (viewHolder == null) {
viewHolder = agvAdapter.onCreateAsymmetricViewHolder(actualIndex, parent, viewType);
}
agvAdapter.onBindAsymmetricViewHolder(viewHolder, parent, actualIndex);
View view = viewHolder.itemView;
view.setTag(new ViewState(viewType, currentItem, viewHolder));
view.setOnClickListener(this);
view.setOnLongClickListener(this);
spaceLeftInColumn -= currentItem.getItem().getRowSpan();
currentIndex = 0;
view.setLayoutParams(new LinearLayout.LayoutParams(getRowWidth(currentItem.getItem()),
getRowHeight(currentItem.getItem())));
//给layout创建childLayout用来存放最小单位的数据(垂直布局),并将为最小单位的数据创建的View放入childLayout中
LinearLayout childLayout = findOrInitializeChildLayout(layout, columnIndex);
childLayout.addView(view);
} else if (currentIndex < rowItems.size() - 1) {
// Try again with next item
currentIndex++;
} else {
break;
}
}
if (debugEnabled && position % 20 == 0) {
Log.d(TAG, linearLayoutPool.getStats("LinearLayout"));
for (Map.Entry<Integer, ObjectPool<AsymmetricViewHolder<?>>> e : viewHoldersMap.entrySet()) {
Log.d(TAG, e.getValue().getStats("ConvertViewMap, viewType=" + e.getKey()));
}
}
}
看到这里相信还有大部分人是迷糊的,这需要我们去结合着数据去看。该方法最开始是从itemsPerRow里面去获取的数据,那么itemsPerRow的数据从何而来呢?通过查找,我们很容易便会发现数据是通过ProcessRowsTask异步任务获取。
@Override
protected final List<RowInfo> doInBackground(Void... params) {
// We need a map in order to associate the item position in the wrapped adapter.
List<RowItem> itemsToAdd = new ArrayList<>();
for (int i = 0; i < agvAdapter.getActualItemCount(); i++) {
try {
//从AsymmetricGridViewAdapter获取数据源RowItem 也就是被我们初步加工的List<Object> dataList;
itemsToAdd.add(new RowItem(i, agvAdapter.getItem(i)));
} catch (CursorIndexOutOfBoundsException e) {
Log.w(TAG, e);
}
}
FyLog.e(TAG, "itemsToAdd list="+ itemsToAdd.size());
//再封装成RowInfo返回
return calculateItemsPerRow(itemsToAdd);
}
doInBackground 方法,将适配器中的数据封装成了RowItem列表,然后再利用calculateItemsPerRow()方法处理成按行归类的RowInfo列表。那关键就在于calculateItemsPerRow()方法,该方法主要由两个子方法组成:calculateItemsPerRow()和calculateItemsForRow(),calculateItemsPerRow()目的是利用calculateItemsForRow()获取RowInfo列表,
private List<RowInfo> calculateItemsPerRow(List<RowItem> itemsToAdd) {
List<RowInfo> rows = new ArrayList<>();
while (!itemsToAdd.isEmpty()) {
RowInfo stuffThatFit = calculateItemsForRow(itemsToAdd);
List<RowItem> itemsThatFit = stuffThatFit.getItems();
if (itemsThatFit.isEmpty()) {
// we can't fit a single item inside a row.
// bail out.
break;
}
for (RowItem entry : itemsThatFit) {
itemsToAdd.remove(entry);
}
rows.add(stuffThatFit);
}
return rows;
}
calculateItemsForRow()方法如下
private RowInfo calculateItemsForRow(List<RowItem> items, float initialSpaceLeft) {
final List<RowItem> itemsThatFit = new ArrayList<>();
int currentItem = 0;
float rowHeight = 1;
float areaLeft = 1 * initialSpaceLeft; //剩余单元面积数
float totalArea = 0;
//还有列没填充 还有剩余数据没有使用
while (areaLeft > 0 && currentItem < items.size()) {
final RowItem item = items.get(currentItem++);
//计算所占单元面积数
float itemArea = item.getItem().getRowSpan() * item.getItem().getColumnSpan();
//小于当前格行所占行数
FyLog.i(TAG, "rowHeight="+ rowHeight+ " item.getItem().getRowSpan()="+ item.getItem().getRowSpan());
if (rowHeight < item.getItem().getRowSpan()) {
// restart with double height 重新计算
FyLog.i(TAG, "clear");
itemsThatFit.clear();
rowHeight = item.getItem().getRowSpan(); //使用最高的行单元数进行计算
currentItem = 0;
areaLeft = initialSpaceLeft * item.getItem().getRowSpan(); //计算总单元面积数
totalArea = 0;
} else if (areaLeft >= itemArea) {
//还有地方没被占用
areaLeft -= itemArea;//剩余单元面积数
totalArea += itemArea;
FyLog.e(TAG, "itemArea="+ itemArea + ", areaLeft="+ areaLeft);
itemsThatFit.add(item);
} else if (!listView.isAllowReordering()) {
FyLog.i(TAG, "isAllowReordering");
break;
}
}
FyLog.e(TAG, "list="+ itemsThatFit.size()+ "totalarea="+ totalArea);
return new RowInfo(rowHeight, itemsThatFit, totalArea);
}
看到这里想必大部分人应该对框架的实现原理已经有了初步的理解,下面我们做一下总结。
总结
页面渲染之初
数据:
我们要对从后台拉下来的数据List<Object>dataList进行处理,列表内部对象“Object”实现AsymmetricItem接口,DemoItem设定其跨行或者跨列数。AdapterImpl中利用ProcessRowsTask 异步方法在doInBackground中对List<DemoItem>进行再次处理(赋值index)转化为List<RowItem> 利用calculateItemsPerRow()将List<RowItem> 转化为List<RowInfo>,而calculateItemsPerRow()内部则利用calculateItemsForRow()对List<RowItem> 进行筛选,生成单个的RowInfo。
布局:
viewHolder创建空的LinearLayout(横向) layout, 给layout创建垂直方向的子容器childLayout ,将以RowItem为单位创建的View,放入childLayout中。
整体架构如下图所示(粗糙了点,勿怪啊)