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

介绍

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);
	}


}


评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值