XListView实现原理

XListview是一个非常受欢迎的下拉刷新控件,但是已经停止维护了。之前写过一篇XListview的使用介绍,用起来非常简单,这两天放假无聊,研究了下XListview的实现原理,学到了很多,今天分享给大家。

    提前声明,为了让代码更好的理解,我对代码进行了部分删减和重构,如果大家想看原版代码,请去github自行下载。

    Xlistview项目主要是三部分:XlistView,XListViewHeader,XListViewFooter,分别是XListView主体、header、footer的实现。下面我们分开来介绍。

    下面是修改之后的XListViewHeader代码

[java]  view plain copy
  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 plain copy
  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 plain copy
  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 plain copy
  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 plain copy
  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 plain copy
  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 plain copy
  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的实现原理就完全的搞明白了,以后如果做滚动类的自定义控件,应该也有思路了。

你可能感兴趣的:(XListView实现原理)