標籤:
本次解析的內容,是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(下拉重新整理上拉載入)源碼解析(一)