您的位置:首页 > 移动开发 > Android开发

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的效果,敬请期待!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Android ListView