轉載自:http://blog.csdn.net/zhaokaiqiang1992/article/details/42392731
XListview是一個非常受歡迎的下拉刷新控件,但是已經停止維護了。之前寫過一篇XListview的使用介紹,用起來非常簡單,這兩天放假無聊,研究了下XListview的實現原理,學到了很多,今天分享給大家。
提前聲明,爲了讓代碼更好的理解,我對代碼進行了部分刪減和重構,如果大家想看原版代碼,請去github自行下載。
Xlistview項目主要是三部分:XlistView,XListViewHeader,XListViewFooter,分別是XListView主體、header、footer的實現。下面我們分開來介紹。
下面是修改之後的XListViewHeader代碼
- public class XListViewHeader extends LinearLayout {
- private static final String HINT_NORMAL = "下拉刷新";
- private static final String HINT_READY = "鬆開刷新數據";
- private static final String HINT_LOADING = "正在加載...";
- // 正常狀態
- public final static int STATE_NORMAL = 0;
- // 準備刷新狀態,也就是箭頭方向發生改變之後的狀態
- public final static int STATE_READY = 1;
- // 刷新狀態,箭頭變成了progressBar
- public final static int STATE_REFRESHING = 2;
- // 佈局容器,也就是根佈局
- private LinearLayout container;
- // 箭頭圖片
- private ImageView mArrowImageView;
- // 刷新狀態顯示
- private ProgressBar mProgressBar;
- // 說明文本
- private TextView mHintTextView;
- // 記錄當前的狀態
- private int mState;
- // 用於改變箭頭的方向的動畫
- private Animation mRotateUpAnim;
- private Animation mRotateDownAnim;
- // 動畫持續時間
- private final int ROTATE_ANIM_DURATION = 180;
- public XListViewHeader(Context context) {
- super(context);
- initView(context);
- }
- public XListViewHeader(Context context, AttributeSet attrs) {
- super(context, attrs);
- initView(context);
- }
- private void initView(Context context) {
- mState = STATE_NORMAL;
- // 初始情況下,設置下拉刷新view高度爲0
- LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
- LayoutParams.MATCH_PARENT, 0);
- container = (LinearLayout) LayoutInflater.from(context).inflate(
- R.layout.xlistview_header, null);
- addView(container, lp);
- // 初始化控件
- 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的狀態
- 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(HINT_NORMAL);
- break;
- case STATE_READY:
- if (mState != STATE_READY) {
- mArrowImageView.clearAnimation();
- mArrowImageView.startAnimation(mRotateUpAnim);
- mHintTextView.setText(HINT_READY);
- }
- break;
- case STATE_REFRESHING:
- mHintTextView.setText(HINT_LOADING);
- break;
- }
- mState = state;
- }
- public void setVisiableHeight(int height) {
- if (height < 0)
- height = 0;
- LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) container
- .getLayoutParams();
- lp.height = height;
- container.setLayoutParams(lp);
- }
- public int getVisiableHeight() {
- return container.getHeight();
- }
- public void show() {
- container.setVisibility(View.VISIBLE);
- }
- public void hide() {
- container.setVisibility(View.INVISIBLE);
- }
- }
XListViewHeader繼承自linearLayout,用來實現下拉刷新時的界面展示,可以分爲三種狀態:正常、準備刷新、正在加載。
在Linearlayout佈局裏面,主要有指示箭頭、說明文本、圓形加載條三個控件。在構造函數中,調用了initView()進行控件的初始化操作。在添加布局文件的時候,指定高度爲0,這是爲了隱藏header,然後初始化動畫,是爲了完成箭頭的旋轉動作。
setState()是設置header的狀態,因爲header需要根據不同的狀態,完成控件隱藏、顯示、改變文字等操作,這個方法主要是在XListView裏面調用。除此之外,還有setVisiableHeight()和getVisiableHeight(),這兩個方法是爲了設置和獲取Header中根佈局文件的高度屬性,從而完成拉伸和收縮的效果,而show()和hide()則顯然就是完成顯示和隱藏的效果。
下面是Header的佈局文件
- <?xml version="1.0" encoding="utf-8"?>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:gravity="bottom" >
- <RelativeLayout
- android:id="@+id/xlistview_header_content"
- android:layout_width="match_parent"
- android:layout_height="60dp"
- tools:ignore="UselessParent" >
- <TextView
- android:id="@+id/xlistview_header_hint_textview"
- android:layout_width="100dp"
- android:layout_height="wrap_content"
- android:layout_centerInParent="true"
- android:gravity="center"
- android:text="正在加載"
- android:textColor="@android:color/black"
- android:textSize="14sp" />
- <ImageView
- android:id="@+id/xlistview_header_arrow"
- android:layout_width="30dp"
- android:layout_height="wrap_content"
- android:layout_centerVertical="true"
- android:layout_toLeftOf="@id/xlistview_header_hint_textview"
- android:src="@drawable/xlistview_arrow" />
- <ProgressBar
- android:id="@+id/xlistview_header_progressbar"
- style="@style/progressbar_style"
- android:layout_width="30dp"
- android:layout_height="30dp"
- android:layout_centerVertical="true"
- android:layout_toLeftOf="@id/xlistview_header_hint_textview"
- android:visibility="invisible" />
- </RelativeLayout>
- </LinearLayout>
說完了Header,我們再看看Footer。Footer是爲了完成加載更多功能時候的界面展示,基本思路和Header是一樣的,下面是Footer的代碼
- public class XListViewFooter extends LinearLayout {
- // 正常狀態
- public final static int STATE_NORMAL = 0;
- // 準備狀態
- public final static int STATE_READY = 1;
- // 加載狀態
- public final static int STATE_LOADING = 2;
- private View mContentView;
- private View mProgressBar;
- private TextView mHintView;
- public XListViewFooter(Context context) {
- super(context);
- initView(context);
- }
- public XListViewFooter(Context context, AttributeSet attrs) {
- super(context, attrs);
- initView(context);
- }
- private void initView(Context context) {
- LinearLayout moreView = (LinearLayout) LayoutInflater.from(context)
- .inflate(R.layout.xlistview_footer, null);
- addView(moreView);
- moreView.setLayoutParams(new LinearLayout.LayoutParams(
- LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
- mContentView = moreView.findViewById(R.id.xlistview_footer_content);
- mProgressBar = moreView.findViewById(R.id.xlistview_footer_progressbar);
- mHintView = (TextView) moreView
- .findViewById(R.id.xlistview_footer_hint_textview);
- }
- /**
- * 設置當前的狀態
- *
- * @param state
- */
- public void setState(int state) {
- mProgressBar.setVisibility(View.INVISIBLE);
- mHintView.setVisibility(View.INVISIBLE);
- switch (state) {
- case STATE_READY:
- mHintView.setVisibility(View.VISIBLE);
- mHintView.setText(R.string.xlistview_footer_hint_ready);
- break;
- case STATE_NORMAL:
- mHintView.setVisibility(View.VISIBLE);
- mHintView.setText(R.string.xlistview_footer_hint_normal);
- break;
- case STATE_LOADING:
- mProgressBar.setVisibility(View.VISIBLE);
- break;
- }
- }
- public void setBottomMargin(int height) {
- if (height > 0) {
- LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mContentView
- .getLayoutParams();
- lp.bottomMargin = height;
- mContentView.setLayoutParams(lp);
- }
- }
- public int getBottomMargin() {
- LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mContentView
- .getLayoutParams();
- return lp.bottomMargin;
- }
- public void hide() {
- LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mContentView
- .getLayoutParams();
- lp.height = 0;
- mContentView.setLayoutParams(lp);
- }
- public void show() {
- LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mContentView
- .getLayoutParams();
- lp.height = LayoutParams.WRAP_CONTENT;
- mContentView.setLayoutParams(lp);
- }
- }
從上面的代碼裏面,我們可以看出,footer和header的思路是一樣的,只不過,footer的拉伸和顯示效果不是通過高度來模擬的,而是通過設置BottomMargin來完成的。
下面是Footer的佈局文件
- <?xml version="1.0" encoding="utf-8"?>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content" >
- <RelativeLayout
- android:id="@+id/xlistview_footer_content"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:padding="5dp"
- tools:ignore="UselessParent" >
- <ProgressBar
- android:id="@+id/xlistview_footer_progressbar"
- style="@style/progressbar_style"
- android:layout_width="30dp"
- android:layout_height="30dp"
- android:layout_centerInParent="true"
- android:visibility="invisible" />
- <TextView
- android:id="@+id/xlistview_footer_hint_textview"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_centerInParent="true"
- android:text="@string/xlistview_footer_hint_normal"
- android:textColor="@android:color/black"
- android:textSize="14sp" />
- </RelativeLayout>
- </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的核心實現原理。
再次提示,下面的代碼經過我重構了,只是爲了看起來更好的理解。
- public class XListView extends ListView {
- private final static int SCROLLBACK_HEADER = 0;
- private final static int SCROLLBACK_FOOTER = 1;
- // 滑動時長
- private final static int SCROLL_DURATION = 400;
- // 加載更多的距離
- private final static int PULL_LOAD_MORE_DELTA = 100;
- // 滑動比例
- private final static float OFFSET_RADIO = 2f;
- // 記錄按下點的y座標
- private float lastY;
- // 用來回滾
- private Scroller scroller;
- private IXListViewListener mListViewListener;
- private XListViewHeader headerView;
- private RelativeLayout headerViewContent;
- // header的高度
- private int headerHeight;
- // 是否能夠刷新
- private boolean enableRefresh = true;
- // 是否正在刷新
- private boolean isRefreashing = false;
- // footer
- private XListViewFooter footerView;
- // 是否可以加載更多
- private boolean enableLoadMore;
- // 是否正在加載
- private boolean isLoadingMore;
- // 是否footer準備狀態
- private boolean isFooterAdd = false;
- // total list items, used to detect is at the bottom of listview.
- private int totalItemCount;
- // 記錄是從header還是footer返回
- private int mScrollBack;
- private static final String TAG = "XListView";
- public XListView(Context context) {
- super(context);
- initView(context);
- }
- public XListView(Context context, AttributeSet attrs) {
- super(context, attrs);
- initView(context);
- }
- public XListView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- initView(context);
- }
- private void initView(Context context) {
- scroller = new Scroller(context, new DecelerateInterpolator());
- headerView = new XListViewHeader(context);
- footerView = new XListViewFooter(context);
- headerViewContent = (RelativeLayout) headerView
- .findViewById(R.id.xlistview_header_content);
- headerView.getViewTreeObserver().addOnGlobalLayoutListener(
- new OnGlobalLayoutListener() {
- @SuppressWarnings("deprecation")
- @Override
- public void onGlobalLayout() {
- headerHeight = headerViewContent.getHeight();
- getViewTreeObserver()
- .removeGlobalOnLayoutListener(this);
- }
- });
- addHeaderView(headerView);
- }
- @Override
- public void setAdapter(ListAdapter adapter) {
- // 確保footer最後添加並且只添加一次
- if (isFooterAdd == false) {
- isFooterAdd = true;
- addFooterView(footerView);
- }
- super.setAdapter(adapter);
- }
- @Override
- public boolean onTouchEvent(MotionEvent ev) {
- totalItemCount = getAdapter().getCount();
- switch (ev.getAction()) {
- case MotionEvent.ACTION_DOWN:
- // 記錄按下的座標
- lastY = ev.getRawY();
- break;
- case MotionEvent.ACTION_MOVE:
- // 計算移動距離
- float deltaY = ev.getRawY() - lastY;
- lastY = ev.getRawY();
- // 是第一項並且標題已經顯示或者是在下拉
- if (getFirstVisiblePosition() == 0
- && (headerView.getVisiableHeight() > 0 || deltaY > 0)) {
- updateHeaderHeight(deltaY / OFFSET_RADIO);
- } else if (getLastVisiblePosition() == totalItemCount - 1
- && (footerView.getBottomMargin() > 0 || deltaY < 0)) {
- updateFooterHeight(-deltaY / OFFSET_RADIO);
- }
- break;
- case MotionEvent.ACTION_UP:
- if (getFirstVisiblePosition() == 0) {
- if (enableRefresh
- && headerView.getVisiableHeight() > headerHeight) {
- isRefreashing = true;
- headerView.setState(XListViewHeader.STATE_REFRESHING);
- if (mListViewListener != null) {
- mListViewListener.onRefresh();
- }
- }
- resetHeaderHeight();
- } else if (getLastVisiblePosition() == totalItemCount - 1) {
- if (enableLoadMore
- && footerView.getBottomMargin() > PULL_LOAD_MORE_DELTA) {
- startLoadMore();
- }
- resetFooterHeight();
- }
- break;
- }
- return super.onTouchEvent(ev);
- }
- @Override
- public void computeScroll() {
- // 鬆手之後調用
- if (scroller.computeScrollOffset()) {
- if (mScrollBack == SCROLLBACK_HEADER) {
- headerView.setVisiableHeight(scroller.getCurrY());
- } else {
- footerView.setBottomMargin(scroller.getCurrY());
- }
- postInvalidate();
- }
- super.computeScroll();
- }
- public void setPullRefreshEnable(boolean enable) {
- enableRefresh = enable;
- if (!enableRefresh) {
- headerView.hide();
- } else {
- headerView.show();
- }
- }
- public void setPullLoadEnable(boolean enable) {
- enableLoadMore = enable;
- if (!enableLoadMore) {
- footerView.hide();
- footerView.setOnClickListener(null);
- } else {
- isLoadingMore = false;
- footerView.show();
- footerView.setState(XListViewFooter.STATE_NORMAL);
- footerView.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- startLoadMore();
- }
- });
- }
- }
- public void stopRefresh() {
- if (isRefreashing == true) {
- isRefreashing = false;
- resetHeaderHeight();
- }
- }
- public void stopLoadMore() {
- if (isLoadingMore == true) {
- isLoadingMore = false;
- footerView.setState(XListViewFooter.STATE_NORMAL);
- }
- }
- private void updateHeaderHeight(float delta) {
- headerView.setVisiableHeight((int) delta
- + headerView.getVisiableHeight());
- // 未處於刷新狀態,更新箭頭
- if (enableRefresh && !isRefreashing) {
- if (headerView.getVisiableHeight() > headerHeight) {
- headerView.setState(XListViewHeader.STATE_READY);
- } else {
- headerView.setState(XListViewHeader.STATE_NORMAL);
- }
- }
- }
- private void resetHeaderHeight() {
- // 當前的可見高度
- int height = headerView.getVisiableHeight();
- // 如果正在刷新並且高度沒有完全展示
- if ((isRefreashing && height <= headerHeight) || (height == 0)) {
- return;
- }
- // 默認會回滾到header的位置
- int finalHeight = 0;
- // 如果是正在刷新狀態,則回滾到header的高度
- if (isRefreashing && height > headerHeight) {
- finalHeight = headerHeight;
- }
- mScrollBack = SCROLLBACK_HEADER;
- // 回滾到指定位置
- scroller.startScroll(0, height, 0, finalHeight - height,
- SCROLL_DURATION);
- // 觸發computeScroll
- invalidate();
- }
- private void updateFooterHeight(float delta) {
- int height = footerView.getBottomMargin() + (int) delta;
- if (enableLoadMore && !isLoadingMore) {
- if (height > PULL_LOAD_MORE_DELTA) {
- footerView.setState(XListViewFooter.STATE_READY);
- } else {
- footerView.setState(XListViewFooter.STATE_NORMAL);
- }
- }
- footerView.setBottomMargin(height);
- }
- private void resetFooterHeight() {
- int bottomMargin = footerView.getBottomMargin();
- if (bottomMargin > 0) {
- mScrollBack = SCROLLBACK_FOOTER;
- scroller.startScroll(0, bottomMargin, 0, -bottomMargin,
- SCROLL_DURATION);
- invalidate();
- }
- }
- private void startLoadMore() {
- isLoadingMore = true;
- footerView.setState(XListViewFooter.STATE_LOADING);
- if (mListViewListener != null) {
- mListViewListener.onLoadMore();
- }
- }
- public void setXListViewListener(IXListViewListener l) {
- mListViewListener = l;
- }
- public interface IXListViewListener {
- public void onRefresh();
- public void onLoadMore();
- }
- }
在三個構造函數中,都調用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的狀態,
- private void updateHeaderHeight(float delta) {
- headerView.setVisiableHeight((int) delta
- + headerView.getVisiableHeight());
- // 未處於刷新狀態,更新箭頭
- if (enableRefresh && !isRefreashing) {
- if (headerView.getVisiableHeight() > headerHeight) {
- headerView.setState(XListViewHeader.STATE_READY);
- } else {
- headerView.setState(XListViewHeader.STATE_NORMAL);
- }
- }
- }
- private void updateFooterHeight(float delta) {
- int height = footerView.getBottomMargin() + (int) delta;
- if (enableLoadMore && !isLoadingMore) {
- if (height > PULL_LOAD_MORE_DELTA) {
- footerView.setState(XListViewFooter.STATE_READY);
- } else {
- footerView.setState(XListViewFooter.STATE_NORMAL);
- }
- }
- footerView.setBottomMargin(height);
- }
- private void resetHeaderHeight() {
- // 當前的可見高度
- int height = headerView.getVisiableHeight();
- // 如果正在刷新並且高度沒有完全展示
- if ((isRefreashing && height <= headerHeight) || (height == 0)) {
- return;
- }
- // 默認會回滾到header的位置
- int finalHeight = 0;
- // 如果是正在刷新狀態,則回滾到header的高度
- if (isRefreashing && height > headerHeight) {
- finalHeight = headerHeight;
- }
- mScrollBack = SCROLLBACK_HEADER;
- // 回滾到指定位置
- scroller.startScroll(0, height, 0, finalHeight - height,
- SCROLL_DURATION);
- // 觸發computeScroll
- invalidate();
- }
- private void resetFooterHeight() {
- int bottomMargin = footerView.getBottomMargin();
- if (bottomMargin > 0) {
- mScrollBack = SCROLLBACK_FOOTER;
- scroller.startScroll(0, bottomMargin, 0, -bottomMargin,
- SCROLL_DURATION);
- invalidate();
- }
- }
至此,整個XListView的實現原理就完全的搞明白了,以後如果做滾動類的自定義控件,應該也有思路了。