XListView 的使用及源码分析
2015-11-05 10:30
363 查看
万事开头难,终于要开始写csdn上的第一篇博客了。其实之前在印象笔记里也写了一些,但是因为是只供自己记录,所以写的毕竟比较随意。写博客的话,还是希望对读者尽量表达的清楚,那对自己的要求也会高一些。好了废话不说,第一篇先从一个比较简单,但也很实用的XListView的使用及源码分析开始吧。
XListView 是Github上一个比较流行的下拉刷新的组件,虽然现在已经停止维护,但是在项目中用到了,而且感觉使用方便代码也比较清晰,所以这里简单记录一下项目在Github上的地址:https://github.com/Maxwin-z/XListView-Android
XListView的使用
1. 使用前的准备把三个类XListView.java,XListViewHeader.java,XListViewFooter.java拷贝到项目的src目录下
把两个布局文件xlistview_header,xlistview_footer拷贝到项目的layout目录下
在string.xml中添加以下条目
<string name="xlistview_header_hint_normal">下拉刷新</string> <string name="xlistview_header_hint_ready">松开刷新数据</string> <string name="xlistview_header_hint_loading">正在加载...</string> <string name="xlistview_header_last_time">上次更新时间:</string> <string name="xlistview_footer_hint_normal">查看更多</string> <string name="xlistview_footer_hint_ready">松开载入更多</string>
使用时可以根据需求对布局进行修改,比如下拉刷新的箭头图案,progressbar的样式等。但注意不要修改其内部View的id,不然在代码中就引用不到了。
2. XListView的初始化设置
把布局中原本的ListView替换成XListView,在Activity或者Fragment里面获取到XListView之后,还要实现IXListViewListener这个接口,它包含两个方法:
public void onRefresh(); //对应下拉刷新时进行的操作
public void onLoadMore(); //对应上拉加载时进行的操作
一般都是去进行网络数据的请求。实现这个接口后再调用XListView.setXListViewListener(this); 就完成了XListView的最基本设置。但是这样还不行,因为会发现下拉刷新完成后,header一直无法收起,footer也是一样的。这就需要在网络请求完成后的回调里再进行一下配置,通常会调用下面三个方法:
XListView.stopRefresh();
XListView.stopLoadMore();
XListView.setRefreshTime(String time);
这样在网络请求完成后就会自动的收起header和footer了,并且会更新刷新的时间。
XListVIew源码分析
XListView 继承自 ListView,XListViewHeader 和 XListViewFooter 继承自 LinearLayout。XListView 初始化时分别添加 XListViewHeader 和 XListViewFooter 为 listview 的 header 和 footer。下面以header为例进行分析,footer的原理基本一致。1. header的初始化
header 的布局如下所示
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="wrap_content" android:gravity="bottom" > <RelativeLayout android:id="@+id/xlistview_header_content" android:layout_width="fill_parent" android:layout_height="60dp" > <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:gravity="center" android:orientation="vertical" android:id="@+id/xlistview_header_text"> <TextView android:id="@+id/xlistview_header_hint_textview" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/xlistview_header_hint_normal" /> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="3dp" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/xlistview_header_last_time" android:textSize="12sp" /> <TextView android:id="@+id/xlistview_header_time" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="12sp" /> </LinearLayout> </LinearLayout> <ImageView android:id="@+id/xlistview_header_arrow" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignLeft="@id/xlistview_header_text" android:layout_centerVertical="true" android:layout_marginLeft="-35dp" android:src="@drawable/xlistview_arrow" /> <ProgressBar android:id="@+id/xlistview_header_progressbar" android:layout_width="30dp" android:layout_height="30dp" android:layout_alignLeft="@id/xlistview_header_text" android:layout_centerVertical="true" android:layout_marginLeft="-40dp" android:visibility="invisible" /> </RelativeLayout> </LinearLayout>
header的初始化代码如下:
private void initView(Context context) { // 初始情况,设置下拉刷新view高度为0 LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, 0); mContainer = (LinearLayout) LayoutInflater.from(context).inflate(R.layout.xlistview_header, null); addView(mContainer, lp); setGravity(Gravity.BOTTOM); mArrowImageView = (ImageView)findViewById(R.id.xlistview_header_arrow); mHintTextView = (TextView)findViewById(R.id.xlistview_header_hint_textview); mProgressBar = (ProgressBar)findViewById(R.id.xlistview_header_progressbar); mRotateUpAnim = new RotateAnimation(0.0f, -180.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); mRotateUpAnim.setDuration(ROTATE_ANIM_DURATION); mRotateUpAnim.setFillAfter(true); mRotateDownAnim = new RotateAnimation(-180.0f, 0.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); mRotateDownAnim.setDuration(ROTATE_ANIM_DURATION); mRotateDownAnim.setFillAfter(true); }可以看到这里将header的初始高度设置为0,然后设置了箭头旋转的动画。
再看XListView的初始化代码:
private void initWithContext(Context context) { mScroller = new Scroller(context, new DecelerateInterpolator()); // XListView need the scroll event, and it will dispatch the event to // user's listener (as a proxy). super.setOnScrollListener(this); // init header view mHeaderView = new XListViewHeader(context); mHeaderViewConte 4000 nt = (RelativeLayout) mHeaderView.findViewById(R.id.xlistview_header_content); mHeaderTimeView = (TextView) mHeaderView.findViewById(R.id.xlistview_header_time); addHeaderView(mHeaderView); // init footer view mFooterView = new XListViewFooter(context); // init header height mHeaderView.getViewTreeObserver().addOnGlobalLayoutListener( new OnGlobalLayoutListener() { @Override public void onGlobalLayout() { mHeaderViewHeight = mHeaderViewContent.getHeight(); getViewTreeObserver().removeGlobalOnLayoutListener(this); } }); }可以看到第9行取到了header的唯一的childview,然后在OnGlobalLayoutListener里面取到了这个childview的初始高度,应该是一个非0值,即header的实际高度,但是前面在header的初始化代码里已经看到,其外层容器的高度被设为了0,所以是看不到header的。
2. XListViewHeader 的几种状态及其转移:
headerview 主要有以下几种状态
从0开始下拉,到刚刚到达释放可以刷新的位置,状态从 STATE_NORMAL --> STATE_READY
从下拉临界点,到之后继续下拉,状态保持 STATE_READY
到达下拉临界点之后释放,状态从 STATE_READY --> STATE_REFRESHING
到达临界点之后不释放,又推回原点, 状态 STATE_READY --> STATE_NORMAL
对应的代码就是header中的setState()函数
public void setState(int state) { if (state == mState) return ; if (state == STATE_REFRESHING) { // 显示进度 mArrowImageView.clearAnimation(); mArrowImageView.setVisibility(View.INVISIBLE); mProgressBar.setVisibility(View.VISIBLE); } else { // 显示箭头图片 mArrowImageView.setVisibility(View.VISIBLE); mProgressBar.setVisibility(View.INVISIBLE); } switch(state){ case STATE_NORMAL: if (mState == STATE_READY) { mArrowImageView.startAnimation(mRotateDownAnim); } if (mState == STATE_REFRESHING) { mArrowImageView.clearAnimation(); } mHintTextView.setText(R.string.xlistview_header_hint_normal); break; case STATE_READY: if (mState != STATE_READY) { mArrowImageView.clearAnimation(); mArrowImageView.startAnimation(mRotateUpAnim); mHintTextView.setText(R.string.xlistview_header_hint_ready); } break; case STATE_REFRESHING: mHintTextView.setText(R.string.xlistview_header_hint_loading); break; default: } mState = state; }
通过设置header的不同状态,就能改变其显示的效果。那么根据什么来改变这个状态呢,很容易想到通过不断获取header的高度来设置相应的状态。这部分的代码就在XListView的onTouchEvent里。
3.下拉刷新的手势判断:
XListView 里通过重写 onTouchEvent 方法来设置header不同的状态。
在按下的时候获取按下位置的y值
在移动的时候,计算相对按下y值的差值,用这个差值去更新headerview的container的高度(前面已经说了,初始为0 )。当container的高度大于mHeaderViewContent 的高度的时候,将 headeview 的状态设为STATE_READY (见 updateHeaderHeight ( float delta) ),反之则设为 STATE_NORMAL
当手抬起的时候,若 container 的高度大于 mHeaderViewContent 的高度,则设置状态为 STATE_REFRESHING,并调用IXListViewListener接口的onRefresh()方法。反之,则调用 resetHeaderHeight () 使header回滚。注意240行调用了Scoller时ListView回滚带有减速效果
重写onTouchEvent()的源码如下:
public boolean onTouchEvent(MotionEvent ev) { if (mLastY == -1) { mLastY = ev.getRawY(); } switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: mLastY = ev.getRawY(); break; case MotionEvent.ACTION_MOVE: final float deltaY = ev.getRawY() - mLastY; mLastY = ev.getRawY(); if (getFirstVisiblePosition() == 0 && (mHeaderView.getVisiableHeight() > 0 || deltaY > 0)) { // the first item is showing, header has shown or pull down. updateHeaderHeight(deltaY / OFFSET_RADIO); invokeOnScrolling(); } else if (getLastVisiblePosition() == mTotalItemCount - 1 && (mFooterView.getBottomMargin() > 0 || deltaY < 0)) { // last item, already pulled up or want to pull up. updateFooterHeight(-deltaY / OFFSET_RADIO); } break; default: mLastY = -1; // reset if (getFirstVisiblePosition() == 0) { // invoke refresh if (mEnablePullRefresh && mHeaderView.getVisiableHeight() > mHeaderViewHeight) { mPullRefreshing = true; mHeaderView.setState(XListViewHeader.STATE_REFRESHING); if (mListViewListener != null) { mListViewListener.onRefresh(); } } resetHeaderHeight(); } if (getLastVisiblePosition() == mTotalItemCount - 1) { // invoke load more. if (mEnablePullLoad && mFooterView.getBottomMargin() > PULL_LOAD_MORE_DELTA) { startLoadMore(); } resetFooterHeight(); } break; } return super.onTouchEvent(ev); }
这里只注意两个地方,一个是move的时候会不断调用updateHeaderHeight()。代码如下:
<span style="white-space:pre"> </span>private void updateHeaderHeight(float delta) { mHeaderView.setVisiableHeight((int) delta + mHeaderView.getVisiableHeight()); if (mEnablePullRefresh && !mPullRefreshing) { // 未处于刷新状态,更新箭头 if (mHeaderView.getVisiableHeight() > mHeaderViewHeight) { mHeaderView.setState(XListViewHeader.STATE_READY); } else { mHeaderView.setState(XListViewHeader.STATE_NORMAL); } } setSelection(0); // scroll to top each time }可以看到在里面根据高度对header设置了不同的状态
第二个需要注意的地方是resetHeaderHeight()。代码如下:
private void resetHeaderHeight() { int height = mHeaderView.getVisiableHeight(); if (height == 0) // not visible. return; // refreshing and header isn't shown fully. do nothing. if (mPullRefreshing && height <= mHeaderViewHeight) { return; } int finalHeight = 0; // default: scroll back to dismiss header. // is refreshing, just scroll back to show all the header. if (mPullRefreshing && height > mHeaderViewHeight) { finalHeight = mHeaderViewHeight; } mScrollBack = SCROLLBACK_HEADER; mScroller.startScroll(0, height, 0, finalHeight - height, SCROLL_DURATION); // trigger computeScroll invalidate(); }
在前面的onTouchEvent()里,只要手抬起的时候列表是位于屏幕顶端,都会调用resetHeaderHeight()。这个时候有几种情况需要分别处理:
如果header高度已经是0,那么什么也不做直接返回
如果header高度虽大于0但是小于其实际高度,并且是处于正在刷新状态的,那么也是什么也不做。这种情况发生在正在刷新的时候,用户又网上滑动了一点listview,就会出现这种情况
如果header的高度大于其实际的高度,并且处于正在刷新状态的,那么设置最终的高度为其实际高度,然后开始滚动
如果header的高度大于其实际的高度,并且不处于刷新状态的,那么设置其最终高度为0,然后开始滚动
到这里我们就理解了为什么在第一部分介绍使用的时候,要在网络请求完成的回调里再调用XListView.stopRefresh()。它会将listview的刷新状态置为false再调用resetHeaderHeight(),这个时候header就会滚动到高度为0的状态了。
当然,为了实现滚动效果,还必须要实现computeScroll(),代码如下:
public void computeScroll() { if (mScroller.computeScrollOffset()) { if (mScrollBack == SCROLLBACK_HEADER) { mHeaderView.setVisiableHeight(mScroller.getCurrY()); } else { mFooterView.setBottomMargin(mScroller.getCurrY()); } postInvalidate(); invokeOnScrolling(); } super.computeScroll(); }
太简单,就是不断的从scroller取当前的y坐标,然后不断更新header的高度就好了。
到此,XListView的分析基本完成了。footer的原理跟header大同小异,区别就是footer本来就是可见的,只是上拉的时候不断改变其bottom margin,状态的迁移和header类似,这里就不再赘述了。
下一篇准备写一下如何在XListView的基础上再整合滑动删除item的效果,敬请期待!
相关文章推荐
- 使用C++实现JNI接口需要注意的事项
- Android IPC进程间通讯机制
- Android Manifest 用法
- [转载]Activity中ConfigChanges属性的用法
- Android之获取手机上的图片和视频缩略图thumbnails
- Android之使用Http协议实现文件上传功能
- Android学习笔记(二九):嵌入浏览器
- android string.xml文件中的整型和string型代替
- i-jetty环境搭配与编译
- android之定时器AlarmManager
- android wifi 无线调试
- Android Native 绘图方法
- Android java 与 javascript互访(相互调用)的方法例子
- android 代码实现控件之间的间距
- android FragmentPagerAdapter的“标准”配置
- Android"解决"onTouch和onClick的冲突问题
- android:installLocation简析
- android searchView的关闭事件
- SourceProvider.getJniDirectories