开发者社区> 一叶飘舟> 正文

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

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
【Android 异步操作】Handler 机制 ( Android 提供的 Handler 源码解析 | Handler 构造与消息分发 | MessageQueue 消息队列相关方法 )
【Android 异步操作】Handler 机制 ( Android 提供的 Handler 源码解析 | Handler 构造与消息分发 | MessageQueue 消息队列相关方法 )
15 0
NVIDIA GPU Operator分析六:NVIDIA GPU Operator原理分析
背景我们知道,如果在Kubernetes中支持GPU设备调度,需要做如下的工作:节点上安装nvidia驱动节点上安装nvidia-docker集群部署gpu device plugin,用于为调度到该节点的pod分配GPU设备。除此之外,如果你需要监控集群GPU资源使用情况,你可能还需要安装DCCM exporter结合Prometheus输出GPU资源监控信息。要安装和管理这么多的组件,对于运维
500 0
Spring源码剖析7:AOP实现原理详解
参考http://www.linkedkeeper.com/detail/blog.action?bid=1048 前言 前面写了六篇文章详细地分析了Spring Bean加载流程,这部分完了之后就要进入一个比较困难的部分了,就是AOP的实现原理分析。
2438 0
微软轻量级系统监控工具sysmon原理与实现完全分析(上篇)
作者:浪子_三少 Sysmon是微软的一款轻量级的系统监控工具,最开始是由Sysinternals开发的,后来Sysinternals被微软收购,现在属于Sysinternals系列工具。它通过系统服务和驱动程序实现记录进程创建、文件访问以及网络信息的记录,并把相关的信息写入并展示在windows的日志事件里。经常有安全人员使用这款工具去记录并分析系统进程的活动来识别恶意或者异常活动。而本文讨论
1635 0
Java NIO三组件——Selecotr/Channel实现原理解析
Java NIO三组件——Selecotr/Channel实现原理解析
132 0
ArrayList源码分析(基于JDK1.6)
不积跬步,无以至千里;不积小流,无以成江海。从基础做起,一点点积累,加油!     《Java集合类》中讲述了ArrayList的基础使用,本文将深入剖析ArrayList的内部结构及实现原理,以便更好的、更高效的使用它。
509 0
+关注
1635
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
JS零基础入门教程(上册)
立即下载
性能优化方法论
立即下载
手把手学习日志服务SLS,云启实验室实战指南
立即下载