XListView 的使用及源码分析

万事开头难,终于要开始写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);
		mHeaderViewContent = (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的效果,敬请期待!







  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值