关于Android瀑布流控件,已经在江湖上,流传已久,超过两年时间了。网上也有很多相关学习资源,可以拿来研究学习。github上,就有两个资源,可以供学习者膜拜。
1.https://github.com/maurycyw/StaggeredGridView 此链接有图片加载功能,但功能相对简单些。
2.https://github.com/etsy/AndroidStaggeredGrid 提供的瀑布流功能强大,可以自定义瀑布流列数。
本篇博客,就讲解etsy的源码为主了。首先看效果图:
首先明确StaggeredGridView中几个变量的定义:
private int mColumnCount; /*程序默认瀑布流的列数,默认情况,通过资源文件中的integers.xml 中grid_column_count定义*/ private int mItemMargin; /*程序默认瀑布流的的margin,通过layout文件activity_sgv.xml中的app:item_margin="8dp"定义*/ private int mColumnWidth; /*程序瀑布流的列宽变量*/ private boolean mNeedSync; private int mColumnCountPortrait = DEFAULT_COLUMNS_PORTRAIT; /*程序瀑布流竖屏列数*/ private int mColumnCountLandscape = DEFAULT_COLUMNS_LANDSCAPE;/*程序瀑布流横屏列数*/
针对瀑布流,搞清楚如下几个问题,也算是吃透其中的原理了。
1.瀑布流的列数定义好了后,如何计算每列的宽度?
2.瀑布流的列数定义好了后,如何计算每列的高度?
3.瀑布流的HeaderView是如何添加的,其高度宽度如何确定?
4.瀑布流的FooterView是如何添加的,其高度宽度如何确定?
5.点击瀑布流的item时,高亮和默认背景的selector如何来实现?
一.瀑布流的列数定义好了后,如何计算每列的宽度?
进入StaggeredGridActivity界面,瀑布流的UI效果已经出来了,主要看com.etsy.android.grid.StaggeredGridView的onMeasure方法,确定每一个view的尺度.
@Override protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (mColumnCount <= 0) { boolean isLandscape = isLandscape(); mColumnCount = isLandscape ? mColumnCountLandscape : mColumnCountPortrait; } // our column width is the width of the listview // minus it's padding // minus the total items margin // divided by the number of columns mColumnWidth = calculateColumnWidth(getMeasuredWidth()); ... .... ... .... ... ....
变量mColumnCount是从资源文件integers.xml 中grid_column_count获取,默认是2,函数calculateColumnWidth用于计算列宽,其定义也比较简单,容易理解.
private int calculateColumnWidth(final int gridWidth) { final int listPadding = getRowPaddingLeft() + getRowPaddingRight(); return (gridWidth - listPadding - mItemMargin * (mColumnCount + 1)) / mColumnCount; }
即屏幕宽度 - listPadding - mItemMargin 除以 列数即item的宽度.此时此刻,列宽即可以得到。继续往下debug代码,由于是使用SampleAdapter,瀑布流中的每一个item均要调用getView方法,此方法跟所有的Adapter一样,要从layout文件中,导入用户自定义的布局文件。代码为:convertView
= mLayoutInflater.inflate(R.layout.list_item_sample, parent, false);
资源文件list_item_sample.xml其定义如下:
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:id="@+id/panel_content" android:layout_width="match_parent" android:layout_height="match_parent" android:descendantFocusability="blocksDescendants" android:background="@drawable/recommend_app_bg"> <com.etsy.android.grid.util.DynamicHeightTextView android:id="@+id/txt_line1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center"/> <Button android:id="@+id/btn_go" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="top|right" android:text="Go" /> </FrameLayout>
此处需要了解DynamicHeightTextView 的定义了。整个Adapter中,子item的尺度,均由DynamicHeightTextView 来确定。其类中的onMeasure如下:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mHeightRatio > 0.0) { // set the image views size int width = MeasureSpec.getSize(widthMeasureSpec); int height = (int) (width * mHeightRatio); setMeasuredDimension(width, height); } else { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } }
遍历mHeightRatio宽高比,从setHeightRatio中获取,经过层层代码跟踪,瀑布流中子item的高度已经剖析出来了。子view的宽高确定,首先要根据屏幕尺寸,确定宽度
然后根据SampleAdapter中的getRandomHeightRatio函数,确定高度,高度是宽度的1~1.5倍。
private double getRandomHeightRatio() { return (mRandom.nextDouble() / 2.0) + 1.0; // height will be 1.0 - 1.5 the width }
剖析至此,可以回答上述的问题1和2了。
二.瀑布流的HeaderView和FooterView是如何添加的,其高度宽度如何确定?
首先看效果图:
从StaggeredGridActivity来看,就只有简单的一行代码,mGridView.addHeaderView(header),便可以给list增加head了。瀑布流的SampleAdapter如何跟headview结合在一起呢?
在mGridView.setAdapter(mAdapter)时,调用ExtendableListView的setAdapter方法,ExtendableListView继承于AbsListView。ExtendableListView.java 的setAdapter方法如下:
@Override public void setAdapter(final ListAdapter adapter) { if (mAdapter != null) { mAdapter.unregisterDataSetObserver(mObserver); } // use a wrapper list adapter if we have a header or footer if (mHeaderViewInfos.size() > 0 || mFooterViewInfos.size() > 0) { mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter); } else { mAdapter = adapter; } ... ... ... ... ... ...
在mHeaderViewInfos或者mFooterViewInfos的size不为0时,构造一个HeaderViewListAdapter。其定义如下:
public class HeaderViewListAdapter implements WrapperListAdapter, Filterable
从定义看以看出,该对象属于一个ListAdapter,既然是Adapter,当然逃不脱几个重要的函数了。getCount(),getItemViewType(),getView()。
public int getCount() { if (mAdapter != null) { return getFootersCount() + getHeadersCount() + mAdapter.getCount(); } else { return getFootersCount() + getHeadersCount(); } }
getCount即adapter的item数量,当mAdapter!=null时,返回head和footer加上mAdapter.getCount.这样。head和footer也就与普通的瀑布流item一起,作为adapter的元素了。
public int getItemViewType(int position) { int numHeaders = getHeadersCount(); if (mAdapter != null && position >= numHeaders) { int adjPosition = position - numHeaders; int adapterCount = mAdapter.getCount(); if (adjPosition < adapterCount) { return mAdapter.getItemViewType(adjPosition); } } return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER; }
getItemViewType函数即更加参数position,来确定view type id,view是从getView函数中创建的。
该函数的意思是:head和footer的位置,返回AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER,普通的瀑布流item,将返回mAdapter.getItemViewType(adjPosition)。
接着就分析getView函数了。任何一个Adapter,都要重写
getView
函数了,这是常识。
public View getView(int position, View convertView, ViewGroup parent) { // Header (negative positions will throw an ArrayIndexOutOfBoundsException) int numHeaders = getHeadersCount(); if (position < numHeaders) { return mHeaderViewInfos.get(position).view; } // Adapter final int adjPosition = position - numHeaders; int adapterCount = 0; if (mAdapter != null) { adapterCount = mAdapter.getCount(); if (adjPosition < adapterCount) { return mAdapter.getView(adjPosition, convertView, parent); } } // Footer (off-limits positions will throw an ArrayIndexOutOfBoundsException) return mFooterViewInfos.get(adjPosition - adapterCount).view; }
函数的意思理解也很容易,header位置,将返回mHeaderViewInfos.get(position).view;footer位置,返回mFooterViewInfos.get(adjPosition - adapterCount).view。其他的位置,也就是不规则GridView中的item view,返回mAdapter.getView(adjPosition, convertView, parent),mAdapter对象,将会调用到SampleAdapter.java中的getView方法了。
至此,就明白了Header和footer是如何被添加到瀑布流界面了。接下来,就来确定head和footer的高度宽度问题了。瀑布流中,从UI效果来看,有三种类型的type view,一个是head,一个是StaggeredGridView,另外一个是footer了。计算子view的尺寸,当然要关注onMeasureChild函数了。StaggeredGridView.java中的onMeasureChild函数定义如下:
@Override protected void onMeasureChild(final View child, final LayoutParams layoutParams) { final int viewType = layoutParams.viewType; final int position = layoutParams.position; if (viewType == ITEM_VIEW_TYPE_HEADER_OR_FOOTER || viewType == ITEM_VIEW_TYPE_IGNORE) { // for headers and weird ignored views super.onMeasureChild(child, layoutParams); } else { ... ... ... ... ... ...
可以看出,当viewType == ITEM_VIEW_TYPE_HEADER_OR_FOOTER时,调用父类ExtendableListView的onMeasureChild方法,计算head的尺度;当viewType
!=ITEM_VIEW_TYPE_HEADER_OR_FOOTER时,走else流程,根据int childWidthSpec = MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY) 来计算尺寸。
三.点击瀑布流的item时,高亮和默认背景的selector如何来实现?
这个在下一个blog中进行研究。