今天藍老師要講的是關於騰訊微博下拉刷新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