实现效果:
像ListView一样调用
实现View复用
可以设定列数
实现下拉刷新和上拉加载的接口,方便调用者自定义刷新View的样式和加载View的样式
瀑布流为了优化体验,在图片未加载前要先获取图片尺寸,因此最后服务端传回图片的尺寸,否则只能下载好所有图片之后再去添加到瀑布流中。
理论上说几千张图片没关系的,关键自己要控制图片的回收,就像ListView在显示很多图片那样。
想看效果的话可以直接应用市场搜App“猜猜我在哪儿”,下载地址http://gdown.baidu.com/data/wisegame/df72eeae87e4873b/caicaiwozainaer_9.apk,帐号和密码都为i,看看就行,不要乱发东西哦,基本和蘑菇街应用效果类似,有BUG,感兴趣自己找吧。PS(请不要反编译APK)
设计思路:
1. 瀑布流控件仿照ListView来搞,第一个就是View复用。这个ListView用的这么多了,道理我就不说了。实现过程就是所有的ChildView在滑动过程判断是否可见,不可见放到集合viewList中,下次Activity的Adapter中调用getView(View v, int position)时候,如果集合里viewList里有view,那么取出来一个传出去,否则new出来一个传出去。
2.瀑布流的子控件的位置计算,这个嘛一眼难尽。正着排序(手指往上滑动),那一行短排在那一行。而且数据的顺序也是按照正常顺序的。当倒着排序(手指往下滑动),当然也是那一行上边界最“短”排在那一行上面,不过这时候对应的数据可不是按照顺序排的。ao
3.可以像ListView那样可以添加HeadView,这里只能添加一个,需要添加多个可以整合成一个HeadView来实现,当然你也可以修改源码
4.不可以像ListView那样可以添加FootView,主要我觉得下面不齐,添加一个FootView会显得丑。
5.实现了下拉刷新和上拉加载更多的View的显示机制,View需要Activity中初始化好再传进来,这样样式可以方便自定义
其他,以后在补充,Demo有空再上传,下面是主要的类;
Adapter, WaterFall, Item, StateListener
package com.ilioili.waterfall;
import java.util.ArrayList;
import java.util.Observable;
import java.util.Observer;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.GestureDetector.OnGestureListener;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;
public class WaterFall extends ViewGroup implements Observer{
private Adapter adapter;
private GestureDetector detector;
/**
* 最上面可见的位置,FreshView的上边界,如果没有FreshView则为headView的上边界,如果都没有则为ItemList的上边界,即0
*/
private int topBound;
/**
* onScrll可以滑动的上边界
*/
private int scrollTopLimit;
/**
* onScrll可以滑动的下边界
*/
private int scrollBottomLimit = Integer.MAX_VALUE;
/**
* fling滑动的时候的curY的最下边界(最大值)
*/
private int flingBottomLimit = Integer.MAX_VALUE;
/**
* fling滑动的时候的curY的最上边界(最小值)
*/
private int flingTopLimit;
private Item[] bottomItems;
/**
* 子空间高度Measure的参数
*/
private int childHeightMeasureSpec;
/**
* 子空间宽度Measure的参数
*/
private int childWidthMeasureSpec;
/**
* Item的View添加到此集合,不包含headView和footView,用于View复用
*/
private ArrayList itemViewList;
/**
* 当前滑动的位置
*/
private int curY = 0;
public static final int DEFAULT_EXTRA_SIZE = 100;
/**
* ACTION_DOWN的时候记录坐标,用于判断事件是否要拦截
*/
private float firstFingerY;
/**
* ACTION_DOWN的时候记录坐标,用于判断事件是否要拦截
*/
private float fisrtFingerX;
/**
* 是否可以刷新,当可以刷新时,flingTopLimit的�?和onScrollTopLimit�?��,可以看见headView
*/
private boolean freshable = true;
private View freshView;
private OnGestureListener gestureListener = new GestureDetector.SimpleOnGestureListener(){
@Override
public boolean onDown(MotionEvent e) {
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,float velocityY) {
if(scroller.isFinished()){
scroller.fling(0, curY, 0, (int)-velocityY, 0, 0, curY-5000, curY+5000);
flingStarted = true;
postInvalidate();//会调用computScroll()
return true;
}else{
return false;
}
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,float distanceY) {
scrollTo(0, (int)(curY+distanceY));//会调用onScrollChanged()
computePreState();
return true;
}
};
/**
* 回调当前手指抬起后将要进行的操作
*/
private void computePreState(){
if(stateListener==null) return;
if(!isLoading){
if(curY+viewHeight>=itemsBottomBound+loadMoreView.getMeasuredHeight()){
if(isLoading){
stateListener.getPreState(StateListener.UP_TO_LOAD_MORE);
}
}else if(curY+viewHeight>itemsBottomBound){
if(isLoading)
stateListener.getPreState(StateListener.UP_TO_CANCEL_LOAD_MORE);
}
}
if(!isFreshing){
if(curY <= topBound){
stateListener.getPreState(StateListener.UP_TO_FRESH);
}else if(curY<=headViewHeight){
stateListener.getPreState(StateListener.UP_TO_CANCEL_FRESH);
}
}
}
public boolean isOnFling(){
if(flingStarted && !scroller.isFinished()){
return true;
}else{
flingStarted = false;
return false;
}
}
private View headView;
private int headViewHeight;
private int headViewTop;
private ArrayList- idleItemList;
/**
* �?��不可见的View添加到此集合里面,用于View复用
*/
private ArrayList
itemRecycler;
/**
*/
private boolean flingStarted;
/**
* 是否向下滑动
*/
private boolean isDownward = true;
/**
* 是否正在刷新,在OnTouchEnvent()中,如果在调用stateListener.startFresh()之前置为true,对滑动边界的设定有影响
*/
private boolean isFreshing;
private boolean isFreshViewMeasured;
/**
* 调用完initItems()后,设置为true,用于在onMeasure(int agr0, int agr1)里防止重复测�?
*/
private boolean isInitItemsDone;
/**
* 是否正在刷新,在OnTouchEnvent()中,如果在调用stateListener.startLoadMore()之前置为true,对滑动边界的设定有影响
*/
private boolean isLoading;
private boolean isLoadMoreViewMeasured;
/**
* 程序在调用measureChildren时此值为true
*/
private ArrayList- itemList;
private int itemWidth;
/**
* 是否可以上拉加载
*/
private boolean loadable = true;
/**
* 下拉刷新的View
*/
private View loadMoreView;
/**
* 瀑布流的列数
*/
private int N = 2;
/**
* ItemList的最下边界
*/
private int itemsBottomBound;
private Scroller scroller;
private StateListener stateListener;
private Item[] topItems;
private int viewHeight;
private int viewWidth;
public WaterFall(Context context) {
super(context);
init();
}
public WaterFall(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
/**
* 取消刷新
*/
public void cancelFreshing(){
isFreshing = false;
if(freshable){
scrollTopLimit = topBound;
}else{
scrollTopLimit = headViewTop;
}
flingTopLimit = headViewTop;
flingBottomLimit = scrollBottomLimit - loadMoreView.getMeasuredHeight();
if(curY
itemsBottomBound){
scroller.startScroll(0, curY, 0, itemsBottomBound-viewHeight-curY, 1000);
postInvalidate();
}
loadMoreView.setVisibility(View.GONE);
}
/**
* 添加新的Item之后要计算ItemList的下边界
*/
final private void computeBottom(){
for(int i=0; iitemsBottomBound){
itemsBottomBound=bottomItems[i].bottom;
}
}else{
break;
}
}
}
private void computeChildVisibity(){
// long start = System.currentTimeMillis();
Item item = null;
int col = 0;
if(itemList.isEmpty()){
return;
}else if(isDownward){//向下滑动
col = getNextDownWhenDown();
while(bottomItems[col].top>curY+viewHeight && bottomItems[col].upItem!=null){
p("回收Item"+bottomItems[col].index);
bottomItems[col].v.setVisibility(View.GONE);
itemRecycler.add(bottomItems[col].v);
bottomItems[col] = bottomItems[col].upItem;
col = getNextDownWhenDown();
}
col = getNextUpWhenDown();
while(topItems[col].top>curY && topItems[col].upItem!=null){
p("添加Item"+topItems[col].index);
item = topItems[col].upItem;
topItems[col] = item;
item.v = obtainView(item.index);
item.v.layout(item.left, item.top, item.right, item.bottom);
}
}else{//向上滑动
col = getNextUpWhenUp();
while(topItems[col].bottomviewHeight){//超出�?��
scrollBottomLimit = itemsBottomBound-viewHeight;
}else{
scrollBottomLimit = 0;
}
flingBottomLimit = scrollBottomLimit;
}else{
loadMoreView.setVisibility(View.VISIBLE);
if(headViewHeight+itemsBottomBound>viewHeight){//超出�?��
loadMoreView.layout(0, itemsBottomBound, viewWidth, itemsBottomBound+loadMoreView.getMeasuredHeight());
loadMoreView.invalidate();
scrollBottomLimit = itemsBottomBound+loadMoreView.getMeasuredHeight()-viewHeight;
if(isLoading){
flingBottomLimit = scrollBottomLimit;
}else{
flingBottomLimit = scrollBottomLimit-loadMoreView.getMeasuredHeight();
}
}else{
loadMoreView.layout(0, viewHeight-headViewHeight, viewWidth, viewHeight-headViewHeight+loadMoreView.getMeasuredHeight());
scrollBottomLimit = loadMoreView.getMeasuredHeight();
flingBottomLimit = 0;
}
}
p("flingBottomLimit"+flingBottomLimit);
p("scrollBottomLimit"+scrollBottomLimit);
}
private void initBottomConfigs(){
p("initBottomConfigs");
flingBottomLimit = Integer.MAX_VALUE;
scrollBottomLimit = Integer.MAX_VALUE;
if(null!=loadMoreView){
loadMoreView.setVisibility(View.GONE);
}
}
@Override
public void computeScroll() {
super.computeScroll();
if(scroller.computeScrollOffset()){
scrollTo(0, scroller.getCurrY());
}
}
public void freshHeadView() {
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY);
measureHeadView(widthMeasureSpec);
}
public int getColumn(){
return N;
}
final private int getNextDownWhenDown(){
int index = 0;
for(int i=1; ibottomItems[index].top){
index = i;
}
}
return index;
}
final private int getNextDownWhenUp(){
for(int i=0; itopItems[index].top){
index = i;
}
}
return index;
}
final private int getNextUpWhenUp(){
int index = 0;
for(int i=1; i();
itemViewList = new ArrayList();
itemList = new ArrayList- ();
idleItemList = new ArrayList
- ();
detector = new GestureDetector(getContext(), gestureListener);
if(android.os.Build.VERSION.SDK_INT>10){
scroller = new Scroller(getContext(),null,true);
}else{
scroller = new Scroller(getContext(),null);
}
}
private void initItems(){
p("initItems()");
if(isInitItemsDone || adapter.getCount()==0){
return;
}else{
p("initItems() data loading");
int num = adapter.getCount();
for(int i=0; i
curY+viewHeight){
break;
}else{
item = obtainItem();
item.top = bottomItems[colum].bottom;
item.upItem = bottomItems[colum];
bottomItems[colum].downItem = item;
}
}
itemList.add(item);
item.v = obtainView(i);
item.left = colum*itemWidth;
item.right = item.left+itemWidth;
item.bottom = item.top+item.v.getMeasuredHeight();
bottomItems[colum] = item;
item.v.layout(item.left, item.top, item.right, item.bottom);
item.index = i;
p("添加Item"+item.index);
}
itemsBottomBound = 0;
computeBottom();
if(itemList.size()==adapter.getCount()){
setBottomConfigs();
}
}
}
final private boolean isAllItemsLoaded(){
if(adapter.getCount()==0){
return false;
}else{
return itemList.size()==adapter.getCount();
}
}
/**
* @return true表示向下滑动,否则认为是向上滑动(优化回收策略)
*/
public boolean isDownWard(){
return isDownward;
}
public boolean isFreshing(){
return isFreshing;
}
/**
* @param position
* @param extraBound 超出边界在extraBound之内的认为可�?
* @return
*/
public boolean isItemVisible(int position, int extraBound){
if(position>=itemList.size()){
return true;
}
Item item = itemList.get(position);
if(item.bottom==0){
return true;
}else{
return item.isVisible(curY, viewHeight, DEFAULT_EXTRA_SIZE);
}
}
public boolean isLoaing(){
return isLoading;
}
private void measureFreshViewIfNotMeasured(int widthMeasureSpec) {
if(null!=freshView && !isFreshViewMeasured){
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(LayoutParams.WRAP_CONTENT, MeasureSpec.UNSPECIFIED);
freshView.measure(widthMeasureSpec, heightMeasureSpec);
topBound = headViewTop-freshView.getMeasuredHeight();
if(freshable){
scrollTopLimit = topBound;
}else{
scrollTopLimit = headViewTop;
}
freshView.layout(0, topBound, freshView.getMeasuredWidth(), headViewTop);
isFreshViewMeasured = true;
}
}
private void measureHeadView(int widthMeasureSpec) {
p("measureHeadView()");
if(null==headView){
return;
}
LayoutParams params = headView.getLayoutParams();
if(null==headView.getLayoutParams()){
params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
headView.setLayoutParams(params);
}
int heightSpec = 0;
if(params.height==LayoutParams.WRAP_CONTENT){
heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
}else{
heightSpec = MeasureSpec.makeMeasureSpec(params.height, MeasureSpec.EXACTLY);
}
int widthSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY);
headView.measure(widthSpec, heightSpec);
if(headViewHeight != headView.getMeasuredHeight()){
headViewHeight = headView.getMeasuredHeight();
headViewTop = -headViewHeight;
flingTopLimit = headViewTop;
topBound = headViewTop-freshView.getMeasuredHeight();
scrollTopLimit = topBound;
headView.layout(0, headViewTop, headView.getMeasuredWidth(), 0);
freshView.layout(0, topBound, freshView.getMeasuredWidth(), headViewTop);
scrollTo(0, headViewTop);
}
}
private void measureLoadMoreViewIfNotMeasured(int widthMeasureSpec) {
if(null!=loadMoreView && !isLoadMoreViewMeasured){
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(LayoutParams.WRAP_CONTENT, MeasureSpec.UNSPECIFIED);
loadMoreView.measure(widthMeasureSpec, heightMeasureSpec);
isLoadMoreViewMeasured = true;
}
}
/**
* 如果没有滑动到可以触发刷新的位置则返回到刷新不可见位�?
* 如果没有滑动到可以触发加载更多的位置则返回到LoadMoreView不可见位�?
*/
private void modifyOverBound() {
if(null!=freshView){
if(!isFreshing){
if(curY <= topBound){
if(null!=stateListener){
isFreshing = true;
flingTopLimit = scrollTopLimit;
stateListener.startFresh();
}
}else if(curYtopBound){
flingTopLimit = headViewHeight;
scroller.forceFinished(true);
scroller.startScroll(0, curY, 0, headViewTop-curY, 500);
postInvalidate();
}
}
}
if(null!=loadMoreView){
if(!isLoading && curY+viewHeight >= itemsBottomBound+loadMoreView.getMeasuredHeight()){
if(null!=stateListener){
isLoading = true;
flingBottomLimit = scrollBottomLimit;
scrollTo(0, flingBottomLimit);
stateListener.startLoadMore();
}
}else if(curY+viewHeight>itemsBottomBound){//loadMoreView可见,动画弹�?
flingBottomLimit = scrollBottomLimit-loadMoreView.getMeasuredHeight();
scroller.forceFinished(true);
scroller.startScroll(0, curY, 0, itemsBottomBound-viewHeight-curY, 500);
postInvalidate();
}
}
}
final private Item obtainItem(){
Item item = null;
if(idleItemList.isEmpty()){
item = new Item();
}else{
item = idleItemList.get(0);
item.reset();
idleItemList.remove(0);
}
return item;
}
final private View obtainView(int i) {
View v = null;
if(itemRecycler.isEmpty()){
v = adapter.getView(null,this,i);
itemViewList.add(v);
addView(v);
}else{
v = itemRecycler.get(0);
v = adapter.getView(v, this, i);
v.setVisibility(View.VISIBLE);
itemRecycler.remove(0);
}
v.forceLayout();
v.measure(childWidthMeasureSpec, childHeightMeasureSpec);
return v;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(ev.getAction()==MotionEvent.ACTION_DOWN){
firstFingerY = ev.getY();
fisrtFingerX = ev.getX();
detector.onTouchEvent(ev);//让Detector记录Down事件,否则onScroll的最后一次事件是上次的Up事件的位�?
scroller.forceFinished(true);
}
//判断滑动方向为竖直方向则拦截事件
if(ev.getAction()==MotionEvent.ACTION_MOVE
&& Math.abs(ev.getY()-firstFingerY)+Math.abs(ev.getX()-fisrtFingerX)>6
&& Math.abs(ev.getY()-firstFingerY)>Math.abs(ev.getX()-fisrtFingerX)){
return true;
}
return false;
}
@Override
protected void onLayout(boolean change, int l, int t, int r, int b) {
p("onLayout()");
// headView.layout(0, headViewTop, viewWidth, 0);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
p("onMeasure");
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
viewHeight = getMeasuredHeight();
viewWidth = getMeasuredWidth();
itemWidth = viewWidth/N;
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(itemWidth, MeasureSpec.EXACTLY);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
measureFreshViewIfNotMeasured(widthMeasureSpec);
measureLoadMoreViewIfNotMeasured(widthMeasureSpec);
measureHeadView(widthMeasureSpec);
initItems();
}
private int scrollDistance;
@Override
/**
* 调用scrollTo(int x, inty)和scrollBy(int x,int y)会执行到这里
*/
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
if(t==oldt) return;
scrollDistance = t-curY;
curY = t;
isDownward = tbottomLimit){
scrollDistance = bottomLimit-curY;
curY = bottomLimit;
if(!scroller.isFinished()){
scroller.forceFinished(true);
}
}
scrollTo(0, curY);
}
}
if(scrollDistance!=0){
computeChildVisibity();
}
if(null!=stateListener){
stateListener.onScroll(curY);
}
}
@Override
public boolean onTouchEvent(MotionEvent e) {
if(e.getAction()==MotionEvent.ACTION_UP){
modifyOverBound();
}
return detector.onTouchEvent(e);
}
final private void p(String s){
// System.out.println(s);
}
public void setAdapter(Adapter adapter){
adapter.addObserver(this);
this.adapter = adapter;
requestLayout();
}
public void setCanFresh(boolean b){
freshable = b;
}
public void setCanLoad(boolean b){
loadable = b;
}
public void setColumn(int n){
this.N = n;
topItems = new Item[N];
bottomItems = new Item[N];
}
/**
* 下拉刷新显示的View
*/
public void setFreshView(View v){
freshView = v;
addView(freshView);
}
/**
* 像ListView那样添加�?��HeadView,不过只能添加一�?
* @param headerView
*/
public void setHeadView(View v){
headView = v;
addView(headView);
}
/**
* 加载更多显示的View
*/
public void setLoadMoreView(View v){
loadMoreView = v;
addView(loadMoreView);
}
public void setStateListener(StateListener listener){
this.stateListener = listener;
}
public void startFresh(){
if(!isFreshing){
scrollTopLimit = topBound;
scroller.startScroll(0, curY, 0, topBound-curY);
isFreshing = true;
flingTopLimit = topBound;
stateListener.startFresh();
postInvalidate();
}
}
@Override
public void update(Observable observable, Object data){
p("update()");
if(null!=data){
int position = (Integer) data;
p("update() fresh item : position="+position);
if(position
package com.ilioili.waterfall;
import java.util.Observable;
import android.view.View;
import android.view.ViewGroup;
public abstract class Adapter extends Observable{
/**
* justMeasure如果有图片先不要加载,设置好图片ImageView的LayoutParams即可,
* 在return v 之前请调�?v.forceLayout()
*/
public abstract View getView(View v,ViewGroup vp, int position);
public abstract int getCount();
/**
* @param updateMode
*/
public void notifyDataSetChanged(int updateMode){
this.updateMode = updateMode;
notifyObservers();
}
public void notifyDataSetChanged(){
updateMode = MODE_UPDATE_ALL;
notifyObservers();
}
public void freshItemIfVisible(int position){
notifyObservers(Integer.valueOf(position));
}
public void notifyDataAdded(){
notifyObservers();
}
@Override
public boolean hasChanged() {
return true;
}
public int getUpdateMode(){
return updateMode;
}
final static public int MODE_UPDATE_ALL = 0;
final static public int MODE_UPDATE_APPEND = 1;
private int updateMode = MODE_UPDATE_ALL;
}
package com.ilioili.waterfall;
import android.view.View;
public class Item {
View v;
Item upItem;
Item downItem;
int index;
int top;
int left;
int right;
int bottom;
/**
* 是否可见(超出可视边�?000个像素认为不可见,否则认为可见)
* @param scrollPosition
* @param parentHeight
* @return
*/
boolean isVisible(int scrollPosition,int parentHeight, int offset){
if(scrollPosition<=bottom+offset && scrollPosition+parentHeight+offset>=top){
return true;
}else{
return false;
}
}
final void reset() {
left = 0;
bottom = 0;
right = 0;
bottom = 0;
upItem = null;
downItem = null;
v = null;
index = 0;
}
@Override
public String toString() {
return "top:"+top+"&bottom:"+bottom;
}
}
package com.ilioili.waterfall;
public interface StateListener{
/**
* 获取当前滑动的位�?
* @param y 当前滑动的到的位�?
*/
void onScroll(int y);
/**
* 缓慢滑动
*/
void scrollToBottomOfLongestColumn();
/**
* 滑动到最短一列的底部
*/
void scrollToBottomOfShortestColumn();
/**
* 滑动到顶部,继续下拉刷新的View就可见了
*/
void scrollToTop();
/**
* 开始刷新,刷新完成需调用cancelFreshing()可将刷新的View关掉
*/
void startFresh();
/**
*开始加载更多,加载完成后需调用用cancelLoading()可以将加载更多关掉
*/
void startLoadMore();
void getPreState(int state);
public static int UP_TO_LOAD_MORE = 1;
public static int UP_TO_CANCEL_LOAD_MORE = 2;
public static int UP_TO_FRESH = 3;
public static int UP_TO_CANCEL_FRESH = 4;
}