今天蓝老师要讲的是关于腾讯微博下拉刷新listview的实现。如上图所示,这是一个用户体验非常好的操作方式,在腾讯微博,新浪微博等等应用中相当常见,相信很多童鞋以后做应用也都会碰到。关于其实现原理,其实网上已有很多demo,鼻祖要数GitHub的J兄了,J兄的做法主要是重写 OnScroll,根据各种状态修改headview的size;还有种则是在OnTouchEvent里做文章,同样是对headview设置padding改变其大小;可能对于多数人后者比较容易理解,蓝老师就第二种实现方式展开本节课程,示例demo是蓝老师重构代码后同时加以扩展添加底部点击获取更多的操作方式,完成腾讯微博的listview实现效果,废话不多说,先上效果图:
大致跟童鞋们说下思路,实现原理是给listview加上一个headview,然后headview有四种状态
- public interface IListViewState
- {
- int LVS_NORMAL = 0; // 普通状态
- int LVS_PULL_REFRESH = 1; // 下拉刷新状态
- int LVS_RELEASE_REFRESH = 2; // 松开刷新状态
- int LVS_LOADING = 3; // 加载状态
- }
所以关键在于MotionEvent.ACTION_MOVE时根据手势情况切换各种headview状态
MotionEvent.ACTION_UP时根据当前状态确定是该进入加载状态还是普通状态
OK,先贴上touchevent代码:
- @Override
- public boolean onTouchEvent(MotionEvent ev) {
- // TODO Auto-generated method stub
- if (mOnRefreshListener != null)
- {
- switch (ev.getAction()) {
- case MotionEvent.ACTION_DOWN:
- doActionDown(ev);
- break;
- case MotionEvent.ACTION_MOVE:
- doActionMove(ev);
- break;
- case MotionEvent.ACTION_UP:
- doActionUp(ev);
- break;
- default:
- break;
- }
- }
- return super.onTouchEvent(ev);
- }
- private void doActionDown(MotionEvent ev)
- {
- if(mIsRecord == false && mFirstItemIndex == 0)
- {
- mStartY = (int) ev.getY();
- mIsRecord = true;
- }
- }
- private void doActionMove(MotionEvent ev)
- {
- mMoveY = (int) ev.getY();
- if(mIsRecord == false && mFirstItemIndex == 0)
- {
- mStartY = (int) ev.getY();
- mIsRecord = true;
- }
- if (mIsRecord == false || mViewState == IListViewState.LVS_LOADING)
- {
- return ;
- }
- int offset = (mMoveY - mStartY) / RATIO;
- switch(mViewState)
- {
- case IListViewState.LVS_NORMAL:
- {
- if (offset > 0)
- {
- mHeadView.setPadding(0, offset - mHeadContentHeight, 0, 0);
- switchViewState(IListViewState.LVS_PULL_REFRESH);
- }
- }
- break;
- case IListViewState.LVS_PULL_REFRESH:
- {
- setSelection(0);
- mHeadView.setPadding(0, offset - mHeadContentHeight, 0, 0);
- if (offset < 0)
- {
- switchViewState(IListViewState.LVS_NORMAL);
- }else if (offset > mHeadContentHeight)
- {
- switchViewState(IListViewState.LVS_RELEASE_REFRESH);
- }
- }
- break;
- case IListViewState.LVS_RELEASE_REFRESH:
- {
- setSelection(0);
- mHeadView.setPadding(0, offset - mHeadContentHeight, 0, 0);
- if (offset >= 0 && offset <= mHeadContentHeight)
- {
- mBack = true;
- switchViewState(IListViewState.LVS_PULL_REFRESH);
- }else if (offset < 0)
- {
- switchViewState(IListViewState.LVS_NORMAL);
- }else{
- }
- }
- break;
- default:
- return;
- };
- }
- private void doActionUp(MotionEvent ev)
- {
- mIsRecord = false;
- mBack = false;
- if (mViewState == IListViewState.LVS_LOADING)
- {
- return ;
- }
- switch(mViewState)
- {
- case IListViewState.LVS_NORMAL:
- break;
- case IListViewState.LVS_PULL_REFRESH:
- mHeadView.setPadding(0, -1 * mHeadContentHeight, 0, 0);
- switchViewState(IListViewState.LVS_NORMAL);
- break;
- case IListViewState.LVS_RELEASE_REFRESH:
- mHeadView.setPadding(0, 0, 0, 0);
- switchViewState(IListViewState.LVS_LOADING);
- onRefresh();
- break;
- }
- }
看下面这个代码段
- if(mIsRecord == false && mFirstItemIndex == 0)
- {
- mStartY = (int) ev.getY();
- mIsRecord = true;
- }
其实就是在一次完整的手势过程中,当headview第一次出现时,记录当前触摸点Y坐标值,而在之后的MotionEvent.ACTION_MOVE过程中,我们当前的触摸点与原始记录值会有一个偏移量offset
int offset = (mMoveY - mStartY) / RATIO;
这个RATIO值姑且不管它,把它当成一先,假设headview原始高度为size,那么offset与size就有以下三种关系:
Offset < 0 headview不可见 NORMAL状态
Offset > 0 && offset < size headview可见且不超过原始高度 PULL_REFRESH状态
Offset > size headview可见且超过原始高度 RELEASE_REFRESH
我们要做的就是根据当前状态再结合offset值来改变headview的尺寸以及确定是否需要切换headview状态
Switch的这段代码已经描述的很明朗了
- switch(mViewState)
- {
- case IListViewState.LVS_NORMAL:
- {
- if (offset > 0)
- {
- mHeadView.setPadding(0, offset - mHeadContentHeight, 0, 0);
- switchViewState(IListViewState.LVS_PULL_REFRESH);
- }
- }
- break;
- case IListViewState.LVS_PULL_REFRESH:
- {
- setSelection(0);
- mHeadView.setPadding(0, offset - mHeadContentHeight, 0, 0);
- if (offset < 0)
- {
- switchViewState(IListViewState.LVS_NORMAL);
- }else if (offset > mHeadContentHeight)
- {
- switchViewState(IListViewState.LVS_RELEASE_REFRESH);
- }
- }
- break;
- case IListViewState.LVS_RELEASE_REFRESH:
- {
- setSelection(0);
- mHeadView.setPadding(0, offset - mHeadContentHeight, 0, 0);
- if (offset >= 0 && offset <= mHeadContentHeight)
- {
- mBack = true;
- switchViewState(IListViewState.LVS_PULL_REFRESH);
- }else if (offset < 0)
- {
- switchViewState(IListViewState.LVS_NORMAL);
- }else{
- }
- }
- break;
- default:
- return;
- };
部分童鞋可能会好奇,为神马在PULL_REFRESH以及RELEASE_REFRESH状态为何要设置setselection(0)
主要是因为当列表长度超过屏幕时候,在往上推动时候会感觉好像是推动速度比下拉速度快,实际上是因为,当列表高度超过屏幕时候,我们在往上推动时候,除了执行我们自己的方法之外,列表也在向下滑动,这样就产生了“推动比下拉速度快”的错觉,也导致了headview部分视图被滚出屏外而不会被完全显示,所以需要在 PULL_REFRESH和RELEASE_REFRESH状态setSelection(0)让headview按当前尺寸完全显示,不信大家把它注释掉试一试就知道了。
那么在switchViewState里就只是更改一些显示元素而已了
- private void switchViewState(int state)
- {
- switch(state)
- {
- case IListViewState.LVS_NORMAL:
- {
- Log.e("!!!!!!!!!!!", "convert to IListViewState.LVS_NORMAL");
- mArrowImageView.clearAnimation();
- mArrowImageView.setImageResource(R.drawable.arrow);
- }
- break;
- case IListViewState.LVS_PULL_REFRESH:
- {
- Log.e("!!!!!!!!!!!", "convert to IListViewState.LVS_PULL_REFRESH");
- mHeadProgressBar.setVisibility(View.GONE);
- mArrowImageView.setVisibility(View.VISIBLE);
- mRefreshTextview.setText("下拉可以刷新");
- mArrowImageView.clearAnimation();
- // 是由RELEASE_To_REFRESH状态转变来的
- if (mBack)
- {
- mBack = false;
- mArrowImageView.clearAnimation();
- mArrowImageView.startAnimation(reverseAnimation);
- }
- }
- break;
- case IListViewState.LVS_RELEASE_REFRESH:
- {
- Log.e("!!!!!!!!!!!", "convert to IListViewState.LVS_RELEASE_REFRESH");
- mHeadProgressBar.setVisibility(View.GONE);
- mArrowImageView.setVisibility(View.VISIBLE);
- mRefreshTextview.setText("松开获取更多");
- mArrowImageView.clearAnimation();
- mArrowImageView.startAnimation(animation);
- }
- break;
- case IListViewState.LVS_LOADING:
- {
- Log.e("!!!!!!!!!!!", "convert to IListViewState.LVS_LOADING");
- mHeadProgressBar.setVisibility(View.VISIBLE);
- mArrowImageView.clearAnimation();
- mArrowImageView.setVisibility(View.GONE);
- mRefreshTextview.setText("载入中...");
- }
- break;
- default:
- return;
- }
- mViewState = state;
- }
再回到刚刚RATIO值的问题,其实就是为了更良好的用户体验,本例设置该值为2,也就意味着虽然移动了100距离,但实际headview尺寸值只改变了50,这样就有一种橡皮筋的感觉
下面再看看activity里面的使用
- public void initData()
- {
- data = new LinkedList<String>();
- String string = "";
- for(int i = 0; i < 6; i++)
- {
- string = "genius" + i;
- data.addFirst(string);
- }
- adapter = new MyListViewAdapter(this, data);
- mListView.setAdapter(adapter);
- mListView.setOnRefreshListener(this);
- mListView.setOnLoadMoreListener(this);
- }
- public void OnRefresh() {
- // TODO Auto-generated method stub
- mRefreshAsynTask = new RefreshDataAsynTask();
- mRefreshAsynTask.execute(null);
- }
- class RefreshDataAsynTask extends AsyncTask<Void , Void, Void>
- {
- @Override
- protected Void doInBackground(Void... arg0) {
- try {
- Thread.sleep(2000);
- } catch (InterruptedException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- index++;
- data.addFirst("genius" + index);
- return null;
- }
- @Override
- protected void onPostExecute(Void result) {
- // TODO Auto-generated method stub
- adapter.refreshData(data);
- mListView.onRefreshComplete();
- }
- }
加载完数据后更新adapter并刷新listview状态就可以了
如果不需要下拉效果就把 mListView.setOnRefreshListener(this);屏蔽掉
如果不需要footview,就调用listview.removeFootView移除掉它
很智能有木有~
其它就没啥好说的了,footview的处理比较简单,童鞋们自个儿下代码看吧
下面附上代码工程链接:
http://download.csdn.net/detail/geniuseoe2012/4446532
温馨提示:学而不思则罔,思而不学则殆,拿来主义固然要发挥,不过童鞋们也要有自己的思考,不然很难进步的哦
欲知更多Android-UI技巧,请关注窝的下一堂课,更多精彩尽在http://blog.csdn.net/geniuseoe2012