AsymmetricGridView开源框架初步解析(仿Instagram搜索页排版轻松实现一拖N与N拖一 或不规则网格布局)

应公司要求,需要实现类似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的实现原理

框架的内部主要提供了两个控件,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中。

整体架构如下图所示(粗糙了点,勿怪啊)

 

参考文件:非对称网格布局AsymmetricGridView小解

使用ListView实现GridView效果,其中每个子元素可以设置自己的占位,比如当前元素占几行几列(rowSpan 和columnSpan),所以看起来就像一个不规则的随机的网格布局。项目地址:https://github.com/felipecsl/AsymmetricGridView 效果图:使用说明:xml<com.felipecsl.asymmetricgridview.library.widget.AsymmetricGridView     xmlns:android="http://schemas.android.com/apk/res/android"     android:id="@ id/listView"     android:layout_width="match_parent"     android:layout_height="match_parent"/>activity中:@Override protected void onCreate(Bundle savedInstanceState) {     super.onCreate(savedInstanceState);     setContentView(R.layout.activity_main);     listView = (AsymmetricGridView) findViewById(R.id.listView);     // Choose your own preferred column width     listView.setRequestedColumnWidth(Utils.dpToPx(this, 120));     final List<AsymmetricItem> items = new ArrayList<>();     // initialize your items array     adapter = new ListAdapter(this, listView, items);     listView.setAdapter(adapter); }支持追加更多的元素:// Will append more items at the end of the adapter. listView.getAdapter().appendItems(moreItems); // resetting the adapter items. Will clear the adapter // and add the new items. listView.getAdapter().setItems(items);设置是否重新排列达到更好的显示效果:// Setting to true will move items up and down to better use the space // Defaults to false. listView.setAllowReordering(true); listView.isAllowReordering(); // true设置item的占位:item 一般这样定义:public DemoItem(final int columnSpan, final int rowSpan, int position) {     this.columnSpan = columnSpan;     this.rowSpan = rowSpan;     this.position = position; }columnSpan 和rowSpan分别代表列占位和行占位。说明:目前当item的rowSpan = 2 columnSpan = 2时可以达到最佳的状态。这个后续会继续改进。item的大小越统一,效率越高,特殊大小的元素少于20%是比较理想的状态。不然没法在不预留很多空位的情况下,合理的显示。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值