您好,登錄后才能下訂單哦!
這篇文章主要介紹XListView如何實現下拉刷新和上拉加載,文中介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們一定要看完!
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" 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" 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); }
在移開手指之后,會調用下面的resetXXX來初始化header和footer的狀態
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(); } }
我們可以看到,滾動操作不是通過直接的設置高度來實現的,而是通過Scroller.startScroll()來實現的,通過調用此方法,computeScroll()就會被調用,然后在這個里面,根據mScrollBack區分是哪一個滾動,然后再通過設置高度和間隔,就可以完成收縮的效果了。
至此,整個XListView的實現原理就完全的搞明白了,以后如果做滾動類的自定義控件,應該也有思路了。
以上是“XListView如何實現下拉刷新和上拉加載”這篇文章的所有內容,感謝各位的閱讀!希望分享的內容對大家有幫助,更多相關知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。