Android XListView實現原理講解及分析

轉載自:http://blog.csdn.net/zhaokaiqiang1992/article/details/42392731

XListview是一個非常受歡迎的下拉刷新控件,但是已經停止維護了。之前寫過一篇XListview的使用介紹,用起來非常簡單,這兩天放假無聊,研究了下XListview的實現原理,學到了很多,今天分享給大家。

    提前聲明,爲了讓代碼更好的理解,我對代碼進行了部分刪減和重構,如果大家想看原版代碼,請去github自行下載。

    Xlistview項目主要是三部分:XlistView,XListViewHeader,XListViewFooter,分別是XListView主體、header、footer的實現。下面我們分開來介紹。

    下面是修改之後的XListViewHeader代碼

[java] view plaincopy
  1. public class XListViewHeader extends LinearLayout {  
  2.   
  3.     private static final String HINT_NORMAL = "下拉刷新";  
  4.     private static final String HINT_READY = "鬆開刷新數據";  
  5.     private static final String HINT_LOADING = "正在加載...";  
  6.   
  7.     // 正常狀態  
  8.     public final static int STATE_NORMAL = 0;  
  9.     // 準備刷新狀態,也就是箭頭方向發生改變之後的狀態  
  10.     public final static int STATE_READY = 1;  
  11.     // 刷新狀態,箭頭變成了progressBar  
  12.     public final static int STATE_REFRESHING = 2;  
  13.     // 佈局容器,也就是根佈局  
  14.     private LinearLayout container;  
  15.     // 箭頭圖片  
  16.     private ImageView mArrowImageView;  
  17.     // 刷新狀態顯示  
  18.     private ProgressBar mProgressBar;  
  19.     // 說明文本  
  20.     private TextView mHintTextView;  
  21.     // 記錄當前的狀態  
  22.     private int mState;  
  23.     // 用於改變箭頭的方向的動畫  
  24.     private Animation mRotateUpAnim;  
  25.     private Animation mRotateDownAnim;  
  26.     // 動畫持續時間  
  27.     private final int ROTATE_ANIM_DURATION = 180;  
  28.   
  29.     public XListViewHeader(Context context) {  
  30.         super(context);  
  31.         initView(context);  
  32.     }  
  33.   
  34.     public XListViewHeader(Context context, AttributeSet attrs) {  
  35.         super(context, attrs);  
  36.         initView(context);  
  37.     }  
  38.   
  39.     private void initView(Context context) {  
  40.         mState = STATE_NORMAL;  
  41.         // 初始情況下,設置下拉刷新view高度爲0  
  42.         LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(  
  43.                 LayoutParams.MATCH_PARENT, 0);  
  44.         container = (LinearLayout) LayoutInflater.from(context).inflate(  
  45.                 R.layout.xlistview_header, null);  
  46.         addView(container, lp);  
  47.         // 初始化控件  
  48.         mArrowImageView = (ImageView) findViewById(R.id.xlistview_header_arrow);  
  49.         mHintTextView = (TextView) findViewById(R.id.xlistview_header_hint_textview);  
  50.         mProgressBar = (ProgressBar) findViewById(R.id.xlistview_header_progressbar);  
  51.         // 初始化動畫  
  52.         mRotateUpAnim = new RotateAnimation(0.0f, -180.0f,  
  53.                 Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,  
  54.                 0.5f);  
  55.         mRotateUpAnim.setDuration(ROTATE_ANIM_DURATION);  
  56.         mRotateUpAnim.setFillAfter(true);  
  57.         mRotateDownAnim = new RotateAnimation(-180.0f, 0.0f,  
  58.                 Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,  
  59.                 0.5f);  
  60.         mRotateDownAnim.setDuration(ROTATE_ANIM_DURATION);  
  61.         mRotateDownAnim.setFillAfter(true);  
  62.     }  
  63.   
  64.     // 設置header的狀態  
  65.     public void setState(int state) {  
  66.         if (state == mState)  
  67.             return;  
  68.   
  69.         // 顯示進度  
  70.         if (state == STATE_REFRESHING) {  
  71.             mArrowImageView.clearAnimation();  
  72.             mArrowImageView.setVisibility(View.INVISIBLE);  
  73.             mProgressBar.setVisibility(View.VISIBLE);  
  74.         } else {  
  75.             // 顯示箭頭  
  76.             mArrowImageView.setVisibility(View.VISIBLE);  
  77.             mProgressBar.setVisibility(View.INVISIBLE);  
  78.         }  
  79.   
  80.         switch (state) {  
  81.         case STATE_NORMAL:  
  82.             if (mState == STATE_READY) {  
  83.                 mArrowImageView.startAnimation(mRotateDownAnim);  
  84.             }  
  85.             if (mState == STATE_REFRESHING) {  
  86.                 mArrowImageView.clearAnimation();  
  87.             }  
  88.             mHintTextView.setText(HINT_NORMAL);  
  89.             break;  
  90.         case STATE_READY:  
  91.             if (mState != STATE_READY) {  
  92.                 mArrowImageView.clearAnimation();  
  93.                 mArrowImageView.startAnimation(mRotateUpAnim);  
  94.                 mHintTextView.setText(HINT_READY);  
  95.             }  
  96.             break;  
  97.         case STATE_REFRESHING:  
  98.             mHintTextView.setText(HINT_LOADING);  
  99.             break;  
  100.         }  
  101.   
  102.         mState = state;  
  103.     }  
  104.   
  105.     public void setVisiableHeight(int height) {  
  106.         if (height < 0)  
  107.             height = 0;  
  108.         LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) container  
  109.                 .getLayoutParams();  
  110.         lp.height = height;  
  111.         container.setLayoutParams(lp);  
  112.     }  
  113.   
  114.     public int getVisiableHeight() {  
  115.         return container.getHeight();  
  116.     }  
  117.   
  118.     public void show() {  
  119.         container.setVisibility(View.VISIBLE);  
  120.     }  
  121.   
  122.     public void hide() {  
  123.         container.setVisibility(View.INVISIBLE);  
  124.     }  
  125.   
  126. }  

    XListViewHeader繼承自linearLayout,用來實現下拉刷新時的界面展示,可以分爲三種狀態:正常、準備刷新、正在加載。

    在Linearlayout佈局裏面,主要有指示箭頭、說明文本、圓形加載條三個控件。在構造函數中,調用了initView()進行控件的初始化操作。在添加布局文件的時候,指定高度爲0,這是爲了隱藏header,然後初始化動畫,是爲了完成箭頭的旋轉動作。

    setState()是設置header的狀態,因爲header需要根據不同的狀態,完成控件隱藏、顯示、改變文字等操作,這個方法主要是在XListView裏面調用。除此之外,還有setVisiableHeight()和getVisiableHeight(),這兩個方法是爲了設置和獲取Header中根佈局文件的高度屬性,從而完成拉伸和收縮的效果,而show()和hide()則顯然就是完成顯示和隱藏的效果。

    下面是Header的佈局文件

[html] view plaincopy
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     xmlns:tools="http://schemas.android.com/tools"  
  4.     android:layout_width="match_parent"  
  5.     android:layout_height="wrap_content"  
  6.     android:gravity="bottom" >  
  7.   
  8.     <RelativeLayout  
  9.         android:id="@+id/xlistview_header_content"  
  10.         android:layout_width="match_parent"  
  11.         android:layout_height="60dp"  
  12.         tools:ignore="UselessParent" >  
  13.   
  14.         <TextView  
  15.             android:id="@+id/xlistview_header_hint_textview"  
  16.             android:layout_width="100dp"  
  17.             android:layout_height="wrap_content"  
  18.             android:layout_centerInParent="true"  
  19.             android:gravity="center"  
  20.             android:text="正在加載"  
  21.             android:textColor="@android:color/black"  
  22.             android:textSize="14sp" />  
  23.   
  24.         <ImageView  
  25.             android:id="@+id/xlistview_header_arrow"  
  26.             android:layout_width="30dp"  
  27.             android:layout_height="wrap_content"  
  28.             android:layout_centerVertical="true"  
  29.             android:layout_toLeftOf="@id/xlistview_header_hint_textview"  
  30.             android:src="@drawable/xlistview_arrow" />  
  31.   
  32.         <ProgressBar  
  33.             android:id="@+id/xlistview_header_progressbar"  
  34.             style="@style/progressbar_style"  
  35.             android:layout_width="30dp"  
  36.             android:layout_height="30dp"  
  37.             android:layout_centerVertical="true"  
  38.             android:layout_toLeftOf="@id/xlistview_header_hint_textview"  
  39.             android:visibility="invisible" />  
  40.     </RelativeLayout>  
  41.   
  42. </LinearLayout>  

    說完了Header,我們再看看Footer。Footer是爲了完成加載更多功能時候的界面展示,基本思路和Header是一樣的,下面是Footer的代碼

[java] view plaincopy
  1. public class XListViewFooter extends LinearLayout {  
  2.   
  3.     // 正常狀態  
  4.     public final static int STATE_NORMAL = 0;  
  5.     // 準備狀態  
  6.     public final static int STATE_READY = 1;  
  7.     // 加載狀態  
  8.     public final static int STATE_LOADING = 2;  
  9.   
  10.     private View mContentView;  
  11.     private View mProgressBar;  
  12.     private TextView mHintView;  
  13.   
  14.     public XListViewFooter(Context context) {  
  15.         super(context);  
  16.         initView(context);  
  17.     }  
  18.   
  19.     public XListViewFooter(Context context, AttributeSet attrs) {  
  20.         super(context, attrs);  
  21.         initView(context);  
  22.     }  
  23.   
  24.     private void initView(Context context) {  
  25.   
  26.         LinearLayout moreView = (LinearLayout) LayoutInflater.from(context)  
  27.                 .inflate(R.layout.xlistview_footer, null);  
  28.         addView(moreView);  
  29.         moreView.setLayoutParams(new LinearLayout.LayoutParams(  
  30.                 LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));  
  31.   
  32.         mContentView = moreView.findViewById(R.id.xlistview_footer_content);  
  33.         mProgressBar = moreView.findViewById(R.id.xlistview_footer_progressbar);  
  34.         mHintView = (TextView) moreView  
  35.                 .findViewById(R.id.xlistview_footer_hint_textview);  
  36.     }  
  37.   
  38.     /** 
  39.      * 設置當前的狀態 
  40.      *  
  41.      * @param state 
  42.      */  
  43.     public void setState(int state) {  
  44.   
  45.         mProgressBar.setVisibility(View.INVISIBLE);  
  46.         mHintView.setVisibility(View.INVISIBLE);  
  47.   
  48.         switch (state) {  
  49.         case STATE_READY:  
  50.             mHintView.setVisibility(View.VISIBLE);  
  51.             mHintView.setText(R.string.xlistview_footer_hint_ready);  
  52.             break;  
  53.   
  54.         case STATE_NORMAL:  
  55.             mHintView.setVisibility(View.VISIBLE);  
  56.             mHintView.setText(R.string.xlistview_footer_hint_normal);  
  57.             break;  
  58.   
  59.         case STATE_LOADING:  
  60.             mProgressBar.setVisibility(View.VISIBLE);  
  61.             break;  
  62.   
  63.         }  
  64.   
  65.     }  
  66.   
  67.     public void setBottomMargin(int height) {  
  68.         if (height > 0) {  
  69.   
  70.             LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mContentView  
  71.                     .getLayoutParams();  
  72.             lp.bottomMargin = height;  
  73.             mContentView.setLayoutParams(lp);  
  74.         }  
  75.     }  
  76.   
  77.     public int getBottomMargin() {  
  78.         LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mContentView  
  79.                 .getLayoutParams();  
  80.         return lp.bottomMargin;  
  81.     }  
  82.   
  83.     public void hide() {  
  84.         LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mContentView  
  85.                 .getLayoutParams();  
  86.         lp.height = 0;  
  87.         mContentView.setLayoutParams(lp);  
  88.     }  
  89.   
  90.     public void show() {  
  91.         LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mContentView  
  92.                 .getLayoutParams();  
  93.         lp.height = LayoutParams.WRAP_CONTENT;  
  94.         mContentView.setLayoutParams(lp);  
  95.     }  
  96.   
  97. }  

    從上面的代碼裏面,我們可以看出,footer和header的思路是一樣的,只不過,footer的拉伸和顯示效果不是通過高度來模擬的,而是通過設置BottomMargin來完成的。

    下面是Footer的佈局文件

[html] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     xmlns:tools="http://schemas.android.com/tools"  
  4.     android:layout_width="fill_parent"  
  5.     android:layout_height="wrap_content" >  
  6.   
  7.     <RelativeLayout  
  8.         android:id="@+id/xlistview_footer_content"  
  9.         android:layout_width="fill_parent"  
  10.         android:layout_height="wrap_content"  
  11.         android:padding="5dp"  
  12.         tools:ignore="UselessParent" >  
  13.   
  14.         <ProgressBar  
  15.             android:id="@+id/xlistview_footer_progressbar"  
  16.             style="@style/progressbar_style"  
  17.             android:layout_width="30dp"  
  18.             android:layout_height="30dp"  
  19.             android:layout_centerInParent="true"  
  20.             android:visibility="invisible" />  
  21.   
  22.         <TextView  
  23.             android:id="@+id/xlistview_footer_hint_textview"  
  24.             android:layout_width="wrap_content"  
  25.             android:layout_height="wrap_content"  
  26.             android:layout_centerInParent="true"  
  27.             android:text="@string/xlistview_footer_hint_normal"  
  28.             android:textColor="@android:color/black"  
  29.             android:textSize="14sp" />  
  30.     </RelativeLayout>  
  31.   
  32. </LinearLayout>  

    在瞭解了Header和footer之後,我們就要介紹最核心的XListView的代碼實現了。

    在介紹代碼實現之前,我先介紹一下XListView的實現原理。

    首先,一旦使用XListView,Footer和Header就已經添加到我們的ListView上面了,XListView就是通過繼承ListView,然後處理了屏幕點擊事件和控制滑動實現效果的。所以,如果我們的Adapter中getCount()返回的值是20,那麼其實XListView裏面是有20+2個item的,這個數量即使我們關閉了XListView的刷新和加載功能,也是不會變化的。Header和Footer通過addHeaderView和addFooterView添加上去之後,如果想實現下拉刷新和上拉加載功能,那麼就必須有拉伸效果,所以就像上面的那樣,Header是通過設置height,Footer是通過設置BottomMargin來模擬拉伸效果。那麼回彈效果呢?僅僅通過設置高度或者是間隔是達不到模擬回彈效果的,因此,就需要用Scroller來實現模擬回彈效果。在說明原理之後,我們開始介紹XListView的核心實現原理。

    再次提示,下面的代碼經過我重構了,只是爲了看起來更好的理解。

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. public class XListView extends ListView {  
  2.   
  3.     private final static int SCROLLBACK_HEADER = 0;  
  4.     private final static int SCROLLBACK_FOOTER = 1;  
  5.     // 滑動時長  
  6.     private final static int SCROLL_DURATION = 400;  
  7.     // 加載更多的距離  
  8.     private final static int PULL_LOAD_MORE_DELTA = 100;  
  9.     // 滑動比例  
  10.     private final static float OFFSET_RADIO = 2f;  
  11.     // 記錄按下點的y座標  
  12.     private float lastY;  
  13.     // 用來回滾  
  14.     private Scroller scroller;  
  15.     private IXListViewListener mListViewListener;  
  16.     private XListViewHeader headerView;  
  17.     private RelativeLayout headerViewContent;  
  18.     // header的高度  
  19.     private int headerHeight;  
  20.     // 是否能夠刷新  
  21.     private boolean enableRefresh = true;  
  22.     // 是否正在刷新  
  23.     private boolean isRefreashing = false;  
  24.     // footer  
  25.     private XListViewFooter footerView;  
  26.     // 是否可以加載更多  
  27.     private boolean enableLoadMore;  
  28.     // 是否正在加載  
  29.     private boolean isLoadingMore;  
  30.     // 是否footer準備狀態  
  31.     private boolean isFooterAdd = false;  
  32.     // total list items, used to detect is at the bottom of listview.  
  33.     private int totalItemCount;  
  34.     // 記錄是從header還是footer返回  
  35.     private int mScrollBack;  
  36.   
  37.     private static final String TAG = "XListView";  
  38.   
  39.     public XListView(Context context) {  
  40.         super(context);  
  41.         initView(context);  
  42.     }  
  43.   
  44.     public XListView(Context context, AttributeSet attrs) {  
  45.         super(context, attrs);  
  46.         initView(context);  
  47.     }  
  48.   
  49.     public XListView(Context context, AttributeSet attrs, int defStyle) {  
  50.         super(context, attrs, defStyle);  
  51.         initView(context);  
  52.     }  
  53.   
  54.     private void initView(Context context) {  
  55.   
  56.         scroller = new Scroller(context, new DecelerateInterpolator());  
  57.   
  58.         headerView = new XListViewHeader(context);  
  59.         footerView = new XListViewFooter(context);  
  60.   
  61.         headerViewContent = (RelativeLayout) headerView  
  62.                 .findViewById(R.id.xlistview_header_content);  
  63.         headerView.getViewTreeObserver().addOnGlobalLayoutListener(  
  64.                 new OnGlobalLayoutListener() {  
  65.                     @SuppressWarnings("deprecation")  
  66.                     @Override  
  67.                     public void onGlobalLayout() {  
  68.                         headerHeight = headerViewContent.getHeight();  
  69.                         getViewTreeObserver()  
  70.                                 .removeGlobalOnLayoutListener(this);  
  71.                     }  
  72.                 });  
  73.         addHeaderView(headerView);  
  74.   
  75.     }  
  76.   
  77.     @Override  
  78.     public void setAdapter(ListAdapter adapter) {  
  79.         // 確保footer最後添加並且只添加一次  
  80.         if (isFooterAdd == false) {  
  81.             isFooterAdd = true;  
  82.             addFooterView(footerView);  
  83.         }  
  84.         super.setAdapter(adapter);  
  85.   
  86.     }  
  87.   
  88.     @Override  
  89.     public boolean onTouchEvent(MotionEvent ev) {  
  90.   
  91.         totalItemCount = getAdapter().getCount();  
  92.         switch (ev.getAction()) {  
  93.         case MotionEvent.ACTION_DOWN:  
  94.             // 記錄按下的座標  
  95.             lastY = ev.getRawY();  
  96.             break;  
  97.         case MotionEvent.ACTION_MOVE:  
  98.             // 計算移動距離  
  99.             float deltaY = ev.getRawY() - lastY;  
  100.             lastY = ev.getRawY();  
  101.             // 是第一項並且標題已經顯示或者是在下拉  
  102.             if (getFirstVisiblePosition() == 0  
  103.                     && (headerView.getVisiableHeight() > 0 || deltaY > 0)) {  
  104.                 updateHeaderHeight(deltaY / OFFSET_RADIO);  
  105.             } else if (getLastVisiblePosition() == totalItemCount - 1  
  106.                     && (footerView.getBottomMargin() > 0 || deltaY < 0)) {  
  107.                 updateFooterHeight(-deltaY / OFFSET_RADIO);  
  108.             }  
  109.             break;  
  110.   
  111.         case MotionEvent.ACTION_UP:  
  112.   
  113.             if (getFirstVisiblePosition() == 0) {  
  114.                 if (enableRefresh  
  115.                         && headerView.getVisiableHeight() > headerHeight) {  
  116.                     isRefreashing = true;  
  117.                     headerView.setState(XListViewHeader.STATE_REFRESHING);  
  118.                     if (mListViewListener != null) {  
  119.                         mListViewListener.onRefresh();  
  120.                     }  
  121.                 }  
  122.                 resetHeaderHeight();  
  123.             } else if (getLastVisiblePosition() == totalItemCount - 1) {  
  124.                 if (enableLoadMore  
  125.                         && footerView.getBottomMargin() > PULL_LOAD_MORE_DELTA) {  
  126.                     startLoadMore();  
  127.                 }  
  128.                 resetFooterHeight();  
  129.             }  
  130.             break;  
  131.         }  
  132.         return super.onTouchEvent(ev);  
  133.     }  
  134.   
  135.     @Override  
  136.     public void computeScroll() {  
  137.   
  138.         // 鬆手之後調用  
  139.         if (scroller.computeScrollOffset()) {  
  140.   
  141.             if (mScrollBack == SCROLLBACK_HEADER) {  
  142.                 headerView.setVisiableHeight(scroller.getCurrY());  
  143.             } else {  
  144.                 footerView.setBottomMargin(scroller.getCurrY());  
  145.             }  
  146.             postInvalidate();  
  147.         }  
  148.         super.computeScroll();  
  149.   
  150.     }  
  151.   
  152.     public void setPullRefreshEnable(boolean enable) {  
  153.         enableRefresh = enable;  
  154.   
  155.         if (!enableRefresh) {  
  156.             headerView.hide();  
  157.         } else {  
  158.             headerView.show();  
  159.         }  
  160.     }  
  161.   
  162.     public void setPullLoadEnable(boolean enable) {  
  163.         enableLoadMore = enable;  
  164.         if (!enableLoadMore) {  
  165.             footerView.hide();  
  166.             footerView.setOnClickListener(null);  
  167.         } else {  
  168.             isLoadingMore = false;  
  169.             footerView.show();  
  170.             footerView.setState(XListViewFooter.STATE_NORMAL);  
  171.             footerView.setOnClickListener(new OnClickListener() {  
  172.                 @Override  
  173.                 public void onClick(View v) {  
  174.                     startLoadMore();  
  175.                 }  
  176.             });  
  177.         }  
  178.     }  
  179.   
  180.     public void stopRefresh() {  
  181.         if (isRefreashing == true) {  
  182.             isRefreashing = false;  
  183.             resetHeaderHeight();  
  184.         }  
  185.     }  
  186.   
  187.     public void stopLoadMore() {  
  188.         if (isLoadingMore == true) {  
  189.             isLoadingMore = false;  
  190.             footerView.setState(XListViewFooter.STATE_NORMAL);  
  191.         }  
  192.     }  
  193.   
  194.     private void updateHeaderHeight(float delta) {  
  195.         headerView.setVisiableHeight((int) delta  
  196.                 + headerView.getVisiableHeight());  
  197.         // 未處於刷新狀態,更新箭頭  
  198.         if (enableRefresh && !isRefreashing) {  
  199.             if (headerView.getVisiableHeight() > headerHeight) {  
  200.                 headerView.setState(XListViewHeader.STATE_READY);  
  201.             } else {  
  202.                 headerView.setState(XListViewHeader.STATE_NORMAL);  
  203.             }  
  204.         }  
  205.   
  206.     }  
  207.   
  208.     private void resetHeaderHeight() {  
  209.         // 當前的可見高度  
  210.         int height = headerView.getVisiableHeight();  
  211.         // 如果正在刷新並且高度沒有完全展示  
  212.         if ((isRefreashing && height <= headerHeight) || (height == 0)) {  
  213.             return;  
  214.         }  
  215.         // 默認會回滾到header的位置  
  216.         int finalHeight = 0;  
  217.         // 如果是正在刷新狀態,則回滾到header的高度  
  218.         if (isRefreashing && height > headerHeight) {  
  219.             finalHeight = headerHeight;  
  220.         }  
  221.         mScrollBack = SCROLLBACK_HEADER;  
  222.         // 回滾到指定位置  
  223.         scroller.startScroll(0, height, 0, finalHeight - height,  
  224.                 SCROLL_DURATION);  
  225.         // 觸發computeScroll  
  226.         invalidate();  
  227.     }  
  228.   
  229.     private void updateFooterHeight(float delta) {  
  230.         int height = footerView.getBottomMargin() + (int) delta;  
  231.         if (enableLoadMore && !isLoadingMore) {  
  232.             if (height > PULL_LOAD_MORE_DELTA) {  
  233.                 footerView.setState(XListViewFooter.STATE_READY);  
  234.             } else {  
  235.                 footerView.setState(XListViewFooter.STATE_NORMAL);  
  236.             }  
  237.         }  
  238.         footerView.setBottomMargin(height);  
  239.   
  240.     }  
  241.   
  242.     private void resetFooterHeight() {  
  243.         int bottomMargin = footerView.getBottomMargin();  
  244.         if (bottomMargin > 0) {  
  245.             mScrollBack = SCROLLBACK_FOOTER;  
  246.             scroller.startScroll(0, bottomMargin, 0, -bottomMargin,  
  247.                     SCROLL_DURATION);  
  248.             invalidate();  
  249.         }  
  250.     }  
  251.   
  252.     private void startLoadMore() {  
  253.         isLoadingMore = true;  
  254.         footerView.setState(XListViewFooter.STATE_LOADING);  
  255.         if (mListViewListener != null) {  
  256.             mListViewListener.onLoadMore();  
  257.         }  
  258.     }  
  259.   
  260.     public void setXListViewListener(IXListViewListener l) {  
  261.         mListViewListener = l;  
  262.     }  
  263.   
  264.     public interface IXListViewListener {  
  265.   
  266.         public void onRefresh();  
  267.   
  268.         public void onLoadMore();  
  269.     }  
  270. }  

    在三個構造函數中,都調用initView進行了header和footer的初始化,並且定義了一個Scroller,並傳入了一個減速的插值器,爲了模仿回彈效果。在initView方法裏面,因爲header可能還沒初始化完畢,所以通過GlobalLayoutlistener來獲取了header的高度,然後addHeaderView添加到了listview上面。

    通過重寫setAdapter方法,保證Footer最後天假,並且只添加一次。

    最重要的,要屬onTouchEvent了。在方法開始之前,通過getAdapter().getCount()獲取到了item的總數,便於計算位置。這個操作在源代碼中是通過scrollerListener完成的,因爲ScrollerListener在這裏沒大有用,所以我直接去掉了,然後把位置改到了這裏。如果在setAdapter裏面獲取的話,只能獲取到沒有header和footer的item數量。

    在ACTION_DOWN裏面,進行了lastY的初始化,lastY是爲了判斷移動方向的,因爲在ACTION_MOVE裏面,通過ev.getRawY()-lastY可以計算出手指的移動趨勢,如果>0,那麼就是向下滑動,反之向上。getRowY()是獲取元Y座標,意思就是和Window和View座標沒有關係的座標,代表在屏幕上的絕對位置。然後在下面的代碼裏面,如果第一項可見並且header的可見高度>0或者是向下滑動,就說明用戶在向下拉動或者是向上拉動header,也就是指示箭頭顯示的時候的狀態,這時候調用了updateHeaderHeight,來更新header的高度,實現header可以跟隨手指動作上下移動。這裏有個OFFSET_RADIO,這個值是一個移動比例,就是說,你手指在Y方向上移動400px,如果比例是2,那麼屏幕上的控件移動就是400px/2=200px,可以通過這個值來控制用戶的滑動體驗。下面的關於footer的判斷與此類似,不再贅述。

   當用戶移開手指之後,ACTION_UP方法就會被調用。在這裏面,只對可見位置是0和item總數-1的位置進行了處理,其實正好對應header和footer。如果位置是0,並且可以刷新,然後當前的header可見高度>原始高度的話,就說明用戶確實是要進行刷新操作,所以通過setState改變header的狀態,如果有監聽器的話,就調用onRefresh方法,然後調用resetHeaderHeight初始化header的狀態,因爲footer的操作如出一轍,所以不再贅述。但是在footer中有一個PULL_LOAD_MORE_DELTA,這個值是加載更多觸發條件的臨界值,只有footer的間隔超過這個值之後,才能夠觸發加載更多的功能,因此我們可以修改這個值來改變用戶體驗。

    說到現在,大家應該明白基本的原理了,其實XListView就是通過對用戶手勢的方向和距離的判斷,來動態的改變Header和Footer實現的功能,所以如果我們也有類似的需求,就可以參照這種思路進行自定義。

    下面再說幾個比較重要的方法。

    前面我們說道,在ACTION_MOVE裏面,會不斷的調用下面的updateXXXX方法,來動態的改變header和fooer的狀態,

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. private void updateHeaderHeight(float delta) {  
  2.         headerView.setVisiableHeight((int) delta  
  3.                 + headerView.getVisiableHeight());  
  4.         // 未處於刷新狀態,更新箭頭  
  5.         if (enableRefresh && !isRefreashing) {  
  6.             if (headerView.getVisiableHeight() > headerHeight) {  
  7.                 headerView.setState(XListViewHeader.STATE_READY);  
  8.             } else {  
  9.                 headerView.setState(XListViewHeader.STATE_NORMAL);  
  10.             }  
  11.         }  
  12.   
  13.     }  
  14.   
  15. private void updateFooterHeight(float delta) {  
  16.         int height = footerView.getBottomMargin() + (int) delta;  
  17.         if (enableLoadMore && !isLoadingMore) {  
  18.             if (height > PULL_LOAD_MORE_DELTA) {  
  19.                 footerView.setState(XListViewFooter.STATE_READY);  
  20.             } else {  
  21.                 footerView.setState(XListViewFooter.STATE_NORMAL);  
  22.             }  
  23.         }  
  24.         footerView.setBottomMargin(height);  
  25.   
  26.     }  
    在移開手指之後,會調用下面的resetXXX來初始化header和footer的狀態

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. private void resetHeaderHeight() {  
  2.         // 當前的可見高度  
  3.         int height = headerView.getVisiableHeight();  
  4.         // 如果正在刷新並且高度沒有完全展示  
  5.         if ((isRefreashing && height <= headerHeight) || (height == 0)) {  
  6.             return;  
  7.         }  
  8.         // 默認會回滾到header的位置  
  9.         int finalHeight = 0;  
  10.         // 如果是正在刷新狀態,則回滾到header的高度  
  11.         if (isRefreashing && height > headerHeight) {  
  12.             finalHeight = headerHeight;  
  13.         }  
  14.         mScrollBack = SCROLLBACK_HEADER;  
  15.         // 回滾到指定位置  
  16.         scroller.startScroll(0, height, 0, finalHeight - height,  
  17.                 SCROLL_DURATION);  
  18.         // 觸發computeScroll  
  19.         invalidate();  
  20.     }  
  21.   
  22. private void resetFooterHeight() {  
  23.         int bottomMargin = footerView.getBottomMargin();  
  24.         if (bottomMargin > 0) {  
  25.             mScrollBack = SCROLLBACK_FOOTER;  
  26.             scroller.startScroll(0, bottomMargin, 0, -bottomMargin,  
  27.                     SCROLL_DURATION);  
  28.             invalidate();  
  29.         }  
  30.     }  
    我們可以看到,滾動操作不是通過直接的設置高度來實現的,而是通過Scroller.startScroll()來實現的,通過調用此方法,computeScroll()就會被調用,然後在這個裏面,根據mScrollBack區分是哪一個滾動,然後再通過設置高度和間隔,就可以完成收縮的效果了。
    至此,整個XListView的實現原理就完全的搞明白了,以後如果做滾動類的自定義控件,應該也有思路了。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章