Maxwin-z/XListView-Android(下拉重新整理上拉載入)源碼解析(一)

來源:互聯網
上載者:User

標籤:

本次解析的內容,是github上一個用於下拉重新整理上拉載入的控制項xlistview,這個功能相信大家在開發的過程中會經常用到。

控制項的源碼地址是https://github.com/Maxwin-z/XListView-Android

在這個控制項之前,我看過一些相同功能的控制項,挑選後覺得XListView功能比較完善,而且易於理解。在android-open-project裡面,有提到一個DropDownListView,個人使用過以後,覺得功能是具備了,但是操作體驗不好,原因就是沒有使用到Scroller來處理滑動問題,導致下拉和復原時的速度都是一樣的(很快) ,原則上來說,復原時應該先快後慢,而下拉則是越拉越要用力(feeling)。

以上是我沒有選中DropDownListView的原因,下面我們具體來看一下XListView。


我們知道拉重新整理上拉載入這個功能,最經常就是用在ListView上,所以我們需要繼承ListView,給它加上頭部和尾部

                                             

對於下拉重新整理上拉載入,我們分開來討論(雖然原理是大同小異)

下拉重新整理:

我們很自然想到給listview加上一個永遠在第一位的頭部,首先自訂一個頭部,然後添加到listview就可以了,這樣解決了繪製的問題。

怎麼保證這個header永遠在頭部呢?listview為我們提供了一個方法addHeaderView()

再來考慮動畫的問題,我們知道,下拉的時候箭頭向下(這裡有一次旋轉),鬆手以後,箭頭會改變方向(這裡有個旋轉動畫)

我們怎麼是箭頭旋轉呢,箭頭明顯是一個imageview,那麼我們只要設定兩個動畫RotateAnimation,一個順時針180,一個逆時針180

然後在下拉(action_mov)時調用第一個,鬆手後(action_up)調用第二個。

再來考慮拉動的問題,XListView給我們的辦法是,header是一個layout,裡面還有一個layout包裹著所有布局(稱為Container),我們通過設定這個Container為Gravity.BOTTOM,也就是讓它永遠在header的底部。另外我們記錄header的高度真實height,然後將header高度設定為0,用於隱藏header。

每次拖動,計算Y方向的offset(利用action_down和action_up事件),然後記錄這個offset(非常重要,接下來要根據offset處理各種情況)。因為header是加在listview裡面的,所以下拉拖動的效果不必擔心。

接下來考慮下拉過程的各種情況:

1,首先我們記錄了offset,每次move,都有一個offset,然後根據這個offset我們可以增加header的高度,從而是header展示出來。

當offset<height(header的全部高度),也就是說header沒有完全展示出來,就鬆手,沒有必要回調更新函數(我們會有這樣一個函數的)

2,當offset>height,鬆手以後,應該回到載入狀態,如。這時header縮小的高度,就是offset-height。

最後在資料載入完成,才回縮至不可見。

上述過程的回縮,都是手指離開螢幕以後發生的,顯然我們要使用Scroller來處理。


下拉重新整理原理就講到這裡,上拉載入更多的原來是一樣的,只是header改變的height,而footer改變的是margin-bottom


下面我們來看源碼

先看XListViewHeader,也就是自訂的頭部

頭部布局

<?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>

頭部java類

public class XListViewHeader extends LinearLayout {/** * 下拉布局主體 */private LinearLayout mContainer;/** * 下拉式箭頭 */private ImageView mArrowImageView;/** * 環形進度條 */private ProgressBar mProgressBar;/** * 提示文本 */private TextView mHintTextView;private int mState = STATE_NORMAL;private Animation mRotateUpAnim;private Animation mRotateDownAnim;/** * 動畫時間 */private final int ROTATE_ANIM_DURATION = 180;public final static int STATE_NORMAL = 0;//普通狀態public final static int STATE_READY = 1;//下拉準備重新整理public final static int STATE_REFRESHING = 2;//正在載入public XListViewHeader(Context context) {super(context);initView(context);}/** * @param context * @param attrs */public XListViewHeader(Context context, AttributeSet attrs) {super(context, attrs);initView(context);}private void initView(Context context) {// 初始情況,設定下拉重新整理view高度為0LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LayoutParams.FILL_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);}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;}/** * 設定下拉頭有效高度 * @param height */public void setVisiableHeight(int height) {if (height < 0)height = 0;LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mContainer.getLayoutParams();lp.height = height;mContainer.setLayoutParams(lp);}/** * 獲得下拉頭有效高度 * @return */public int getVisiableHeight() {return mContainer.getLayoutParams().height;}}
上面注釋已經說得非常清楚了,我再進行一些解釋。

首先是初始化函數initView()這裡獲得了布局中的控制項,設定了箭頭旋轉動畫,將header的高度設定為0

然後是setState()函數,根據傳入的state,判斷是否隱藏控制項,調用哪個旋轉動畫等,這個函數將會被外部調用

另外還有setVisiableHeight(int height)函數,用於記錄下拉的距離(其實就是傳入的height),這距離在之前說得很清楚,用於判斷下拉的狀態,非常重要

getVisiableHeight()函數沒有什麼好說的。


OK,完成了header,我們來看Xlistview

首先是一些基本屬性,用於大家在接下來的源碼中,做參考,大家可以忽略掉,但遇到不明意思的屬性時,回頭再找出來看

public class XListView extends ListView implements OnScrollListener {private float mLastY = -1; // save event y/** * 用於下拉後,滑動返回 */private Scroller mScroller; // used for scroll backprivate OnScrollListener mScrollListener; // user's scroll listener// the interface to trigger refresh and load more.private IXListViewListener mListViewListener;/** * 下拉頭部 */private XListViewHeader mHeaderView;/** * 下拉頭主體,用於計算頭部的高度 * 當不能重新整理時,被隱藏 */private RelativeLayout mHeaderViewContent;/** * 下拉式箭頭 */private TextView mHeaderTimeView;/** * 下拉頭部的高度 */private int mHeaderViewHeight;/** * 能否下拉重新整理 */private boolean mEnablePullRefresh = true;/** * 是否正在重新整理,false表示正在重新整理 */private boolean mPullRefreshing = false; // is refreashing.// -- footer viewprivate XListViewFooter mFooterView;private boolean mEnablePullLoad;private boolean mPullLoading;private boolean mIsFooterReady = false;// total list items, used to detect is at the bottom of listview.private int mTotalItemCount;// for mScroller, scroll back from header or footer.private int mScrollBack;/** * 頭部滑動返回 */private final static int SCROLLBACK_HEADER = 0;/** * footer滑動返回 */private final static int SCROLLBACK_FOOTER = 1;private final static int SCROLL_DURATION = 400; // scroll back durationprivate final static int PULL_LOAD_MORE_DELTA = 50; // when pull up >= 50px// at bottom, trigger// load more.private final static float OFFSET_RADIO = 1.8f; // support iOS like pull// feature.

接下來是初始化

        public XListView(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);initWithContext(context);}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);//初始化下拉頭mHeaderView = new XListViewHeader(context);mHeaderViewContent = (RelativeLayout) mHeaderView.findViewById(R.id.xlistview_header_content);mHeaderTimeView = (TextView) mHeaderView.findViewById(R.id.xlistview_header_time);addHeaderView(mHeaderView);//初始化底部mFooterView = new XListViewFooter(context);//初始化下拉頭高度mHeaderView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {@Overridepublic void onGlobalLayout() {mHeaderViewHeight = mHeaderViewContent.getHeight();//獲得下拉頭的高度getViewTreeObserver().removeGlobalOnLayoutListener(this);}});}
初始化函數做了一個非常重要的操作,就是獲得了下拉頭的實際高度mHeaderViewHeight,這個是我們用於判斷下拉狀態的另外一個重要指標

控制項繪製好以後,我們來處理下拉問題

@Overridepublic boolean onTouchEvent(MotionEvent ev) {if (mLastY == -1) {//獲得觸摸時的y座標mLastY = ev.getRawY();}switch (ev.getAction()) {case MotionEvent.ACTION_DOWN:mLastY = ev.getRawY();break;case MotionEvent.ACTION_MOVE:final float deltaY = ev.getRawY() - mLastY;//下拉或者上拉了多少offsetmLastY = ev.getRawY();if (getFirstVisiblePosition() == 0&& (mHeaderView.getVisiableHeight() > 0 || deltaY > 0)) {//第一個item可見並且頭部部分顯示或者下拉操作,則表示處於下拉重新整理狀態// 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)) {//最後一個item可見並且footer被上拉顯示或者上拉操作,則表示處於上拉重新整理狀態// last item, already pulled up or want to pull up.updateFooterHeight(-deltaY / OFFSET_RADIO);}break;default://action_upmLastY = -1; // resetif (getFirstVisiblePosition() == 0) {// invoke refreshif (mEnablePullRefresh&& mHeaderView.getVisiableHeight() > mHeaderViewHeight) {mPullRefreshing = true;mHeaderView.setState(XListViewHeader.STATE_REFRESHING);if (mListViewListener != null) {mListViewListener.onRefresh();}}resetHeaderHeight();} else if (getLastVisiblePosition() == mTotalItemCount - 1) {// invoke load more.if (mEnablePullLoad    && mFooterView.getBottomMargin() > PULL_LOAD_MORE_DELTA    && !mPullLoading) {startLoadMore();}resetFooterHeight();}break;}return super.onTouchEvent(ev);}
從上面代碼可以看出

action_down:獲得手指觸摸的座標

action_move:這時根據移動座標,計算出offset,我們就可以改變header的高度(當然也可能是上拉,判斷條件看上面的注釋)

判斷是下拉重新整理,首先要檢查listview的第一個item是否可見(也就是header),如果可見,有兩種情況,一頭部部分顯示,一是下拉操作

接著調用了updateHeaderHeight()用於更新header的高度,從而使header顯示出來,同時記錄下拉的距離

/** * 更新頭部高度 * 這個函數用於下拉時,記錄下拉了多少 * @param delta */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}
updateHeaderHeight()中有一個setSelection(0)目的是為了讓下拉的時候感到困難(feeling)

高度一直更新,所以header會被越拉越大

最後我們鬆手

action_up:同樣判斷是下拉還是上拉,這樣我們先只看下拉的部分

if (mEnablePullRefresh&& mHeaderView.getVisiableHeight() > mHeaderViewHeight) {
判斷是否開啟了下拉,並且下拉高度大於下拉頭的實際高度mHeaderViewHeight,我前面說過mHeaderViewHeight是一個重要的指標,它用於判斷下拉頭是否完全顯示,從而判斷是否需要回調操作

mListViewListener.onRefresh();
最後,還調用了resetHeaderHeight()用於使header正確複位(注意,這個時候的複位,不是完全隱藏header,而是是header處於更新狀態)


我們看看resetHeaderHeight()

/** * reset header view's height. * 重設頭部高度 */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; // 預設最終高度,也就是說要讓頭部消失// 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 computeScrollinvalidate();}

這樣有一個重要判斷,就是
if (mPullRefreshing && height > mHeaderViewHeight)
用於判斷是下拉後,到載入資料狀態,還是載入資料完畢,到隱藏頭部狀態

在下拉鬆手後,height(下拉過程中使header增加的高度)是大於mHeaderViewHeight(header的真實高度),所以我們改變了finalHeight,這樣就會使header滑動到載入狀態

而在載入狀態,這時height等於mHeaderViewHeight,所以finalHeight=0,我們再次調用resetHeaderHeight(),就可以使header隱藏

這裡有點繞,但是很關鍵,希望大家仔細理解。

怎麼滑動復原呢,當然是使用Scroller

//從當前位置,返回到頭部被隱藏mScroller.startScroll(0, height, 0, finalHeight - height,SCROLL_DURATION);
真正的復原,是在computeScroll裡面實現的

@Overridepublic void computeScroll() {if (mScroller.computeScrollOffset()) {if (mScrollBack == SCROLLBACK_HEADER) {mHeaderView.setVisiableHeight(mScroller.getCurrY());//改變頭部高度,實現復原} else {mFooterView.setBottomMargin(mScroller.getCurrY());}postInvalidate();invokeOnScrolling();}super.computeScroll();}

通過Scroller通過的位置,改變頭部高度,實現復原。

到此位置,下拉重新整理就解析完畢了,但是我們沒有看到第二次調用resetHeaderHeight(),使下拉頭隱藏的操作啊

當然沒有,因為我們要在載入完資料,才調用這個函數,也就是說調用時機是不確定的,根據具體需求的,所以控制項沒有辦法覺得什麼時候調用,這個調用權在你手上,也就是說我們載入完資料,需要主動調用

xlistview為我們提供了一個public用於主動調用,內部進行了resetHeaderHeight()操作

/** * stop refresh, reset header view. * 停止重新整理,重設頭部 */public void stopRefresh() {if (mPullRefreshing == true) {mPullRefreshing = false;resetHeaderHeight();}}
OK,下拉重新整理相信已經說清楚,接下來我們看上拉載入更多

這還有什麼困難嗎?無非就是拉動的方向不一樣

我還是為大家提幾個重要的點,首先是保證footer永遠在listview的最後一個,怎麼保證呢?看下面

@Overridepublic void setAdapter(ListAdapter adapter) {/* * 將上拉載入更多footer加入listview的底部 * 並且保證只加入一次 */if (mIsFooterReady == false) {mIsFooterReady = true;addFooterView(mFooterView);//listview原生方法}super.setAdapter(adapter);}

在setAdapter裡面,調用addFooterView()保證footer的位置

xlistview的上拉載入功能是預設不開啟的,我們需要主動調用setPullLoadEnable()函數,完成初始化操作

注意,如果不開啟上拉載入,隱藏footer的時候,要將listview內建的分割線也隱藏

/** * 設定上拉載入更多功能是否開啟 * 如果要開啟,必須主動調用這個函數  * @param enable */public void setPullLoadEnable(boolean enable) {mEnablePullLoad = enable;if (!mEnablePullLoad) {//如果不開啟mFooterView.hide();//隱藏footermFooterView.setOnClickListener(null);//取消監聽//make sure "pull up" don't show a line in bottom when listview with one page //保證listview的item之間的分割線消失(最後一條)setFooterDividersEnabled(false);//listview原生方法} else {mPullLoading = false;mFooterView.show();mFooterView.setState(XListViewFooter.STATE_NORMAL);//make sure "pull up" don't show a line in bottom when listview with one page  setFooterDividersEnabled(true);// both "pull up" and "click" will invoke load more.mFooterView.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {startLoadMore();}});}}

其他部分跟header就大同小異了,改變footer的margin-bottom,就可以產生上拉的效果。

xlistview解析完畢,我將在”Maxwin-z/XListView-Android(下拉重新整理上拉載入)源碼解析(二)“貼出幾個類的具體代碼,很xlistview的簡單使用

Maxwin-z/XListView-Android(下拉重新整理上拉載入)源碼解析(一)

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.