您的位置:首页 > 移动开发 > Android开发

Android自定义View之快速实现下拉刷新, 点击加载更多ListView

2015-06-12 16:26 1111 查看

介绍

ListView是最常用UI组件之一. 由于手机的屏幕大小很有限, 如何在如此有限的空间简化交互操作, 将省下的空间用于显示更多的数据就显得相当有意义. 比如"刷新数据" 和 "加载下一页数据"等功能, 原来可能在视图的菜单栏上设计了固定的按钮, 但这些按钮无疑使界面看起来稍微"复杂"了一些. 于是大牛们将这种交互简化成列表下拉刷新, 上拉加载更多, 滑动到底部点击加载更多, 滑动到底部自动加载更多等等更为人性化的操作. 这种交互已经得到广泛的应用, 也有很多主流的第三方, 比如XListView, PullToRefresh等等.
用了那么久, 有没有想过自己也来实现下呢? 通过简单实践, 明白实现的原理.日后我们自己也能写出更强大的个性化组件. so, let's go 吧~

目的

例子虽简单, 但本次我们实现的是, 下拉刷新, 点击加载更多的ListView, 而且要支持设置模式, 比如仅支持下拉刷新, 仅支持点击加载更多, 或者都支持, 等等.这样才能在使用时满足更多的需求.

效果图:



实现原理

对于这种交互, 我们已经很熟悉. 代码实现的方式也很多, 比如继承ViewGroup, 或者继承LinearLayout之类的布局去包裹一个ListView等等, 这些实现都稍显复杂, 当然也有好处, 这样就能加入更多的接口方法, DIY更多的小功能. 而直接继承ListView去实现, 应该是最简单的.所以我们这次就采用继承ListView的方法. 但无论上面哪种实现方案, 关键的地方其实是相同的, 就是如何处理触摸事件, 监听列表滑动的状态.比如怎么知道列表已经滑动到底部, 或者已经滑动到顶部, 然后触发下拉刷新的事件等.

所以要实现OnScrollListener的onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,int totalItemCount)方法, 当可见的第一个item的位置为0, 则表示列表已经滑动到底部, 这时可以触发下拉刷新的事件.

@Override
	public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
            int totalItemCount)
	{
		if(visibleItemCount >= 3)
		{
			firstItemPosition = firstVisibleItem;
			lastItemPosition = firstItemPosition + visibleItemCount - 3;
			itemTotal = totalItemCount - 2;
		}
	}


@Override
	public boolean onTouchEvent(MotionEvent ev)
	{
		switch (ev.getAction()) {
		case MotionEvent.ACTION_DOWN:
			if(firstItemPosition == 0 && pullDownState == PULL_DOWN_REFRESH && canPullDown())
			{
				//标记可下拉刷新
				isCanPullDown = true;
				dDownY = (int)ev.getY();
			}
			else{
				isCanPullDown = false;
			}
			
			break;
		//...


如何给ListView添加顶部和底部的View? ListView已经提供了addHeaderView(...), addFooter(...)的方法, 我们直接利用就是了. 现在我们知道如何添加头部和底部View,也知道什么时候去触发下拉刷新事件. 那么如何实现下拉刷新的效果?

其实方法是有几种的, 比如利用setLayoutParams(...), setPadding(...)等等, 通过控制顶部View的高度,边距或者位置, 即可实现.这里我们使用setPadding(left, top, right, bottom).四个参数, 只需要设置top这一个值就行了. 在onTouch(...)方法中, 首先在ACTION_DOWN获取手指按下的Y轴坐标, 然后在ACTION_MOVE获取手指不断移动的Y轴坐标, 两个坐标相减获得实际移动的距离. 而这个距离就是顶部View要移动的距离,
将该距离值设入top中, 利用setPadding(...)不断刷新顶部View, 就成了我们所看到的下拉View随着手指滑动而移动的效果, 当顶部下移的距离超过一定值( 比如自身的高度 ), 就提示可放手刷新, 最后继续利用setPadding(...), 参数top的值改为0, 将View固定在顶部, 显示刷新效果, 当数据得到刷新, 又将top的值改为自身高度的负值, 隐藏起来. 这就是整个下拉刷新的过程. 其实上拉加载的效果更多也是类似的, 只是监听从top换成了bottom. 而滑动底部点击加载更多,
就更简单了, 无非是给底部View添加点击事件.

对了, 同样少不了提供给下拉刷新和点击加载更多事件的接口方法.

/**
	 * 上下拉接口
	 */
	public interface onTListViewListener
	{
		void onRefresh();
		void onLoadMore();
	}
	
	public void setOnTListViewListener(onTListViewListener listener)
	{
		this.listener = listener;
	}


到了这里, 我们应该对原理有个大概理解了. 再看代码就容易多了. 但考虑的深入一些, 我们还会想到更复杂的情况, 假如用户下拉刷新一次, 顶部View已经刷新, 但是用户紧接着又第二次甚至第三次地去下拉, 怎么处理呢? 怎么保证多次重复的操作只响应一次等等, 用户在实际使用中, 情况一般都比开发者的预想的要复杂一些, 所以完善的产品肯定是优化无止境的, 我们考虑多一些, 就能减少一点应用出问题的机会.就凑字数扯到这了.

最后放上相关源码:
关键的实现类, 随便起的名字... 刷新效果是简单的Alpha变换, 可以根据自己喜欢改改哦.

TListView :

/**
* 下拉刷新, 点击加载更多
* @author Alex Tam
*/
public class TListView extends ListView implements OnScrollListener{
private String TAG = "TListView";

private Context mContext;
private View headerView = null, footerView = null;

//是否允许下拉功能
private boolean isCanPullDown = true;
//顶部View,底部View的高度
private int headerHeight, footerHeight;

private int dDownY;
//监听item的滑动位置
private int firstItemPosition,lastItemPosition,itemTotal;

private final static int PULL_DOWN_REFRESH = 1;
private final static int CLICK_LOADMORE = 2;
private final static int RELEASE_TO_REFRESH = 3;
// private final static int RELEASE_TO_LOADMORE = 4;
private final static int REFRESHING = 5;
private final static int LOADING_MORE = 6;

private int pullDownState = PULL_DOWN_REFRESH;
private int loadmoreState = CLICK_LOADMORE;

private TextView tv_header,tv_footer;
private ImageView imv_footer;

private final String PULL_DOWN_STR = "下拉获取数据";
private final String PULL_REL_STR = "松开获取数据";
private final String PULL_LOADING = "努力加载中...";
private final String PULL_UP_STR = "点击加载更多";

/** 拖曳模式 **/
public static enum PULL_MODE{
BOTH_PULL,PULL_DOWN,CLICK_LOADMORE,NONE_OF_ALL
}
/** 当前拖曳模式 **/
private PULL_MODE pullMode;

private ValueAnimator vAnimatorHeader = null;

private onTListViewListener listener;



public TListView(Context context)
{
this(context, null);
}

public TListView(Context context, AttributeSet attrs)
{
this(context, attrs, 0);
}

public TListView(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
mContext = context;
initHeaderView();
setOnScrollListener(this);
}

private void initHeaderView()
{
headerView = LayoutInflater.from(mContext).inflate(R.layout.lv_header, null);

tv_header = (TextView)headerView.findViewById(R.id.tv_lv_header);
tv_header.setText(PULL_DOWN_STR);

vAnimatorHeader = getAnimator(headerView);

measureView(headerView);

headerHeight = headerView.getMeasuredHeight();
headerView.setPadding(0, -headerHeight, 0, 0);
addHeaderView(headerView);
}

@Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { if(visibleItemCount >= 3) { firstItemPosition = firstVisibleItem; lastItemPosition = firstItemPosition + visibleItemCount - 3; itemTotal = totalItemCount - 2; } }

@Override
public void onScrollStateChanged(AbsListView view, int scrollState)
{ }

private void initFooterView()
{
footerView = LayoutInflater.from(mContext).inflate(R.layout.lv_footer, null);
tv_footer = (TextView)footerView.findViewById(R.id.tv_lv_footer);
imv_footer = (ImageView)footerView.findViewById(R.id.imv_lv_footer);
tv_footer.setText(PULL_UP_STR);

measureView(footerView);

footerHeight = footerView.getMeasuredHeight();

footerView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if(loadmoreState == CLICK_LOADMORE)
{
loadmoreState = LOADING_MORE;
tv_footer.setText(PULL_LOADING);
imv_footer.setVisibility(View.INVISIBLE);
new pullTask(2, listener).execute();
}
else if(loadmoreState == LOADING_MORE)
{
updateFooterView();
}
}
});
addFooterView(footerView);
}

@Override
public boolean onTouchEvent(MotionEvent ev)
{
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
if(firstItemPosition == 0 && pullDownState == PULL_DOWN_REFRESH && canPullDown())
{
//标记可下拉刷新
isCanPullDown = true;
dDownY = (int)ev.getY();
}
else{
isCanPullDown = false;
}

break;

case MotionEvent.ACTION_MOVE:
if(isCanPullDown && firstItemPosition == 0 && canPullDown())
{
int mY = (int)ev.getY();
int distance = (mY - dDownY)/2;

headerView.setPadding(0, distance - headerHeight, 0, 0);

if(headerView.getPaddingTop() >= 0)
{
pullDownState = RELEASE_TO_REFRESH;
tv_header.setText(PULL_REL_STR);
return true;
}
else if(headerView.getPaddingTop() < 0)
{
pullDownState = PULL_DOWN_REFRESH;
tv_header.setText(PULL_DOWN_STR);
}
}


break;

case MotionEvent.ACTION_UP:
if(pullDownState == RELEASE_TO_REFRESH){
updateHeaderView();
}
else if(pullDownState == PULL_DOWN_REFRESH){
updateHeaderView();
}


break;

default:
break;
}

return super.onTouchEvent(ev);
}

private Handler mHandler = new Handler()
{
@Override
public void handleMessage(Message msg)
{
if(msg.what == PULL_DOWN_REFRESH)
{
completeHeader();
}
else if(msg.what == RELEASE_TO_REFRESH)
{
tv_header.setText(PULL_LOADING);
headerView.setPadding(0, 0, 0, 0);

if(listener != null)
{
pullDownState = REFRESHING;
startAnimator(vAnimatorHeader);
new pullTask(1, listener).execute();
}
else
{
pullDownState = PULL_DOWN_REFRESH;
updateHeaderView();
Log.e(TAG, "The onPullListViewListener used is null...");
}
postInvalidate();
}
else if(msg.what == CLICK_LOADMORE)
{
completeFooter();
}
else if(msg.what == LOADING_MORE)
{
Toast.makeText(mContext, "数据正在获取中", Toast.LENGTH_SHORT).show();
}
super.handleMessage(msg);
}
};

/**
* 刷新headerView
*/
private void updateHeaderView()
{
switch (pullDownState) {
case PULL_DOWN_REFRESH:
//放手刷新
isCanPullDown = true;
mHandler.sendEmptyMessage(PULL_DOWN_REFRESH);

break;

case RELEASE_TO_REFRESH:
//提示继续下拉
isCanPullDown = false;
mHandler.sendEmptyMessage(RELEASE_TO_REFRESH);

break;

default:
break;
}
}

/**
* 刷新footerView
*/
private void updateFooterView()
{
switch (loadmoreState) {
case CLICK_LOADMORE:
mHandler.sendEmptyMessage(CLICK_LOADMORE);
break;

case LOADING_MORE:
mHandler.sendEmptyMessage(LOADING_MORE);
break;

default:
break;
}
}

/**
* 恢復headerView
*/
public void completeHeader()
{
tv_header.setText(PULL_DOWN_STR);
headerView.setPadding(0, -headerHeight, 0, 0);

endAnimator(vAnimatorHeader);
postInvalidate();
}

/**
* 恢復footerView
*/
public void completeFooter()
{
tv_footer.setText(PULL_UP_STR);
imv_footer.setVisibility(View.VISIBLE);
loadmoreState = CLICK_LOADMORE;
}

/**
* 开始动画
* @param animator
*/
private void startAnimator(Animator animator)
{
if(animator != null)
{
animator.start();
}
}

/**
* 停止动画
* @param animator
*/
private void endAnimator(Animator animator)
{
if(animator != null)
{
animator.cancel();
}
}


/**
* 执行异步任务类
*/
private class pullTask extends AsyncTask<Void, Void, Void>
{
private int code;
private onTListViewListener mListener;

public pullTask(int code, onTListViewListener listener)
{
this.code = code;
this.mListener = listener;
}

@Override
protected Void doInBackground(Void... params)
{
try
{
if(mListener != null)
{
if(code == 1)
{ //刷新
try {
mListener.onRefresh();
//恢复初始态
pullDownState = PULL_DOWN_REFRESH;
updateHeaderView();
}
catch (Exception e) {
e.printStackTrace();
pullDownState = PULL_DOWN_REFRESH;
updateHeaderView();
}
}
else if(code == 2)
{ //加载更多
try {
mListener.onLoadMore();
//恢复初始态
loadmoreState = CLICK_LOADMORE;
updateFooterView();
}
catch (Exception e) {
e.printStackTrace();
loadmoreState = CLICK_LOADMORE;
updateFooterView();
}
}
}
else
{
Log.e(TAG, "onPullListViewListener in pullTask is NULL...");
}
}
catch (Exception e)
{
e.printStackTrace();
}
return null;
}

@Override
protected void onPostExecute(Void result){ }

@Override
protected void onPreExecute(){ }
}


private void measureView(View viewSholdBeMeasure)
{
ViewGroup.LayoutParams viewLayoutParams=(LayoutParams) viewSholdBeMeasure.getLayoutParams();
if(viewLayoutParams == null)
{
viewLayoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
}


int widthSpecificationConstraint = ViewGroup.getChildMeasureSpec(0, 0, viewLayoutParams.width);
int heightSpecificationConstraint;
if(viewLayoutParams.height > 0)
{
heightSpecificationConstraint=MeasureSpec.makeMeasureSpec(0, MeasureSpec.EXACTLY );
}
else
{
heightSpecificationConstraint=MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED );
}
viewSholdBeMeasure.measure(widthSpecificationConstraint, heightSpecificationConstraint);
}

/**
* 设置透明度动画
* @param target
* @return
*/
private ValueAnimator getAnimator(final View target)
{
ValueAnimator vAnimator = ValueAnimator.ofFloat(0.8f,1.0f);
vAnimator.setTarget(target);
vAnimator.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
target.setAlpha((float)animation.getAnimatedValue());
}
});
vAnimator.setDuration(500);
vAnimator.setRepeatCount(ValueAnimator.INFINITE);
vAnimator.setRepeatMode(ValueAnimator.REVERSE);
return vAnimator;
}

/** * 上下拉接口 */ public interface onTListViewListener { void onRefresh(); void onLoadMore(); } public void setOnTListViewListener(onTListViewListener listener) { this.listener = listener; }

@Override
public void setAdapter(ListAdapter adapter)
{
super.setAdapter(adapter);
}

/**
* 是否可下拉
* @return
*/
private boolean canPullDown()
{
if(pullMode == PULL_MODE.BOTH_PULL
|| pullMode == PULL_MODE.PULL_DOWN)
{
return true;
}
return false;
}

/**
* 设置列表拖曳模式
* @param mode
*/
public void setTListViewMode(PULL_MODE mode)
{
this.pullMode = mode;
if(pullMode != PULL_MODE.BOTH_PULL && pullMode != PULL_MODE.CLICK_LOADMORE)
{
if(footerView != null){
removeFooterView(footerView);
}
}
else{
if(footerView != null){
addFooterView(footerView);
}
else
{
initFooterView();
}
}
}

public PULL_MODE getTListViewMode()
{
return this.pullMode;
}
}


测试的主类, MainActivity :

public class MainActivity extends Activity {
	private ArrayAdapter<String> adapter;
	private List<String> datas = new ArrayList<String>();
	private TListView lv_main;
	
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		lv_main = (TListView)findViewById(R.id.lv_main);
		lv_main.setTListViewMode(PULL_MODE.BOTH_PULL);
		init();
		
	}
	
	private Handler mHandler = new Handler(){
		@Override
		public void handleMessage(Message msg)
		{
			if(msg.what == 0x101)
			{
				datas.add("刷新数据 " + datas.size());
				adapter.notifyDataSetChanged();
				Toast.makeText(MainActivity.this, "刷新成功~", Toast.LENGTH_SHORT).show();
			}
			else if(msg.what == 0x102)
			{
				for(int i=0; i<3; i++)
				{
					datas.add("增加数据 " + datas.size());
				}
				adapter.notifyDataSetChanged();
				Toast.makeText(MainActivity.this, "加载更多~", Toast.LENGTH_SHORT).show();	
			}
			super.handleMessage(msg);
		}
	};
	
	private void init()
	{
		for(int i=0;i<10; i++)
		{
			datas.add("数据 " + i);
		}
		
		lv_main.setOnTListViewListener(new onTListViewListener() {
			@Override
			public void onRefresh() {
				try {
					Thread.sleep(3000);
					
				} catch (Exception e) {
					e.printStackTrace();
				}
				mHandler.sendEmptyMessage(0x101);
				
			}
			
			@Override
			public void onLoadMore() {
				try {
					Thread.sleep(3000);
					
				} catch (Exception e) {
					e.printStackTrace();
				}
				mHandler.sendEmptyMessage(0x102);
			}
		});
		
		adapter = new ArrayAdapter<String>(MainActivity.this, android.R.layout.simple_dropdown_item_1line, datas);
		lv_main.setAdapter(adapter);
	}

}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: