Android下拉刷新框架實(shí)現(xiàn)代碼實(shí)例
前段時(shí)間項(xiàng)目中用到了下拉刷新功能,之前在網(wǎng)上也找到過(guò)類(lèi)似的demo,但這些demo的質(zhì)量參差不齊,用戶(hù)體驗(yàn)也不好,接口設(shè)計(jì)也不行。最張沒(méi)辦法,終于忍不了了,自己就寫(xiě)了一個(gè)下拉刷新的框架,這個(gè)框架是一個(gè)通用的框架,效果和設(shè)計(jì)感覺(jué)都還不錯(cuò),現(xiàn)在分享給各位看官。
一. 關(guān)于下拉刷新
下拉刷新這種用戶(hù)交互最早由twitter創(chuàng)始人洛倫•布里切特(Loren Brichter)發(fā)明,有理論認(rèn)為,下拉刷新是一種適用于按照從新到舊的時(shí)間順序排列feeds的應(yīng)用,在這種應(yīng)用場(chǎng)景中看完舊的內(nèi)容時(shí),用戶(hù)會(huì)很自然地下拉查找更新的內(nèi)容,因此下拉刷新就顯得非常合理。大家可以參考這篇文章:有趣的下拉刷新,下面我貼出一個(gè)有趣的下拉刷新的案例。
圖一、有趣的下拉刷新案例(一)
圖二、有趣的下拉刷新案例(二)
二. 實(shí)現(xiàn)原理
上面這些例子,外觀做得再好看,他的本質(zhì)上都一樣,那就是一個(gè)下拉刷新控件通常由以下幾部分組成:
【1】Header
Header通常有下拉箭頭,文字,進(jìn)度條等元素,根據(jù)下拉的距離來(lái)改變它的狀態(tài),從而顯示不同的樣式
【2】Content
這部分是內(nèi)容區(qū)域,網(wǎng)上有很多例子都是直接在ListView里面添加Header,但這就有局限性,因?yàn)楹枚嗲闆r下并不一定是用ListView來(lái)顯示數(shù)據(jù)。我們把要顯示內(nèi)容的View放置在我們的一個(gè)容器中,如果你想實(shí)現(xiàn)一個(gè)用ListView顯示數(shù)據(jù)的下拉刷新,你需要?jiǎng)?chuàng)建一個(gè)ListView旋轉(zhuǎn)到我的容器中。我們處理這個(gè)容器的事件(down, move, up),如果向下拉,則把整個(gè)布局向下滑動(dòng),從而把header顯示出來(lái)。
【3】Footer
Footer可以用來(lái)顯示向上拉的箭頭,自動(dòng)加載更多的進(jìn)度條等。
以上三部分總結(jié)的說(shuō)來(lái),就是如下圖所示的這種布局結(jié)構(gòu):
圖三,下拉刷新的布局結(jié)構(gòu)
關(guān)于上圖,需要說(shuō)明幾點(diǎn):
1、這個(gè)布局?jǐn)U展于LinearLayout,垂直排列
2、從上到下的順序是:Header, Content, Footer
3、Content填充滿(mǎn)父控件,通過(guò)設(shè)置top, bottom的padding來(lái)使Header和Footer不可見(jiàn),也就是讓它超出屏幕外
4、下拉時(shí),調(diào)用scrollTo方法來(lái)將整個(gè)布局向下滑動(dòng),從而把Header顯示出來(lái),上拉正好與下拉相反。
5、派生類(lèi)需要實(shí)現(xiàn)的是:將Content View填充到父容器中,比如,如果你要使用的話,那么你需要把ListView, ScrollView, WebView等添加到容器中。
6、上圖中的紅色區(qū)域就是屏的大小(嚴(yán)格來(lái)說(shuō),這里說(shuō)屏幕大小并不準(zhǔn)確,應(yīng)該說(shuō)成內(nèi)容區(qū)域更加準(zhǔn)確)
三. 具體實(shí)現(xiàn)
明白了實(shí)現(xiàn)原理與過(guò)程,我們嘗試來(lái)具體實(shí)現(xiàn),首先,為了以后更好地?cái)U(kuò)展,設(shè)計(jì)更加合理,我們把下拉刷新的功能抽象成一個(gè)接口:
1、IPullToRefresh<T extends View>
它具體的定義方法如下:
public interface IPullToRefresh<T extends View> { public void setPullRefreshEnabled(boolean pullRefreshEnabled); public void setPullLoadEnabled(boolean pullLoadEnabled); public void setScrollLoadEnabled(boolean scrollLoadEnabled); public boolean isPullRefreshEnabled(); public boolean isPullLoadEnabled(); public boolean isScrollLoadEnabled(); public void setOnRefreshListener(OnRefreshListener<T> refreshListener); public void onPullDownRefreshComplete(); public void onPullUpRefreshComplete(); public T getRefreshableView(); public LoadingLayout getHeaderLoadingLayout(); public LoadingLayout getFooterLoadingLayout(); public void setLastUpdatedLabel(CharSequence label); }
這個(gè)接口是一個(gè)泛型的,它接受View的派生類(lèi),因?yàn)橐诺轿覀兊娜萜髦械牟痪褪且粋€(gè)View嗎?
2、PullToRefreshBase<T extends View>
這個(gè)類(lèi)實(shí)現(xiàn)了IPullToRefresh接口,它是從LinearLayout繼承過(guò)來(lái),作為下拉刷新的一個(gè)抽象基類(lèi),如果你想實(shí)現(xiàn)ListView的下拉刷新,只需要擴(kuò)展這個(gè)類(lèi),實(shí)現(xiàn)一些必要的方法就可以了。這個(gè)類(lèi)的職責(zé)主要有以下幾點(diǎn):
- 處理onInterceptTouchEvent()和onTouchEvent()中的事件:當(dāng)內(nèi)容的View(比如ListView)正如處于最頂部,此時(shí)再向下拉,我們必須截?cái)嗍录?,然后move事件就會(huì)把后續(xù)的事件傳遞到onTouchEvent()方法中,然后再在這個(gè)方法中,我們根據(jù)move的距離再進(jìn)行scroll整個(gè)View。
- 負(fù)責(zé)創(chuàng)建Header、Footer和Content View:在構(gòu)造方法中調(diào)用方法去創(chuàng)建這三個(gè)部分的View,派生類(lèi)可以重寫(xiě)這些方法,以提供不同式樣的Header和Footer,它會(huì)調(diào)用createHeaderLoadingLayout和createFooterLoadingLayout方法來(lái)創(chuàng)建Header和Footer創(chuàng)建Content View的方法是一個(gè)抽象方法,必須讓派生類(lèi)來(lái)實(shí)現(xiàn),返回一個(gè)非null的View,然后容器再把這個(gè)View添加到自己里面。
- 設(shè)置各種狀態(tài):這里面有很多狀態(tài),如下拉、上拉、刷新、加載中、釋放等,它會(huì)根據(jù)用戶(hù)拉動(dòng)的距離來(lái)更改狀態(tài),狀態(tài)的改變,它也會(huì)把Header和Footer的狀態(tài)改變,然后Header和Footer會(huì)根據(jù)狀態(tài)去顯示相應(yīng)的界面式樣。
3、PullToRefreshBase<T extends View>繼承關(guān)系
這里我實(shí)現(xiàn)了三個(gè)下拉刷新的派生類(lèi),分別是ListView、ScrollView、WebView三個(gè),它們的繼承關(guān)系如下:
圖四、PullToRefreshBase類(lèi)的繼承關(guān)系
關(guān)于PullToRefreshBase類(lèi)及其派和類(lèi),有幾點(diǎn)需要說(shuō)明:
對(duì)于ListView,ScrollView,WebView這三種情況,他們是否滑動(dòng)到最頂部或是最底部的實(shí)現(xiàn)是不一樣的,所以,在PullToRefreshBase類(lèi)中需要調(diào)用兩個(gè)抽象方法來(lái)判斷當(dāng)前的位置是否在頂部或底部,而其派生類(lèi)必須要實(shí)現(xiàn)這兩個(gè)方法。比如對(duì)于ListView,它滑動(dòng)到最頂部的條件就是第一個(gè)child完全可見(jiàn)并且first postion是0。這兩個(gè)抽象方法是:
/** * 判斷刷新的View是否滑動(dòng)到頂部 * * @return true表示已經(jīng)滑動(dòng)到頂部,否則false */ protected abstract boolean isReadyForPullDown(); /** * 判斷刷新的View是否滑動(dòng)到底 * * @return true表示已經(jīng)滑動(dòng)到底部,否則false */ protected abstract boolean isReadyForPullUp();
創(chuàng)建可下拉刷新的View(也就是content view)的抽象方法是
/** * 創(chuàng)建可以刷新的View * * @param context context * @param attrs 屬性 * @return View */ protected abstract T createRefreshableView(Context context, AttributeSet attrs);
4、LoadingLayout
LoadingLayout是刷新Layout的一個(gè)抽象,它是一個(gè)抽象基類(lèi)。Header和Footer都擴(kuò)展于這個(gè)類(lèi)。這類(lèi)抽象類(lèi),提供了兩個(gè)抽象方法:
getContentSize
這個(gè)方法返回當(dāng)前這個(gè)刷新Layout的大小,通常返回的是布局的高度,為了以后可以擴(kuò)展為水平拉動(dòng),所以方法名字沒(méi)有取成getLayoutHeight()之類(lèi)的,這個(gè)返回值,將會(huì)作為松手后是否可以刷新的臨界值,如果下拉的偏移值大于這個(gè)值,就認(rèn)為可以刷新,否則不刷新,這個(gè)方法必須由派生類(lèi)來(lái)實(shí)現(xiàn)。
setState
這個(gè)方法用來(lái)設(shè)置當(dāng)前刷新Layout的狀態(tài),PullToRefreshBase類(lèi)會(huì)調(diào)用這個(gè)方法,當(dāng)進(jìn)入下拉,松手等動(dòng)作時(shí),都會(huì)調(diào)用這個(gè)方法,派生類(lèi)里面只需要根據(jù)這些狀態(tài)實(shí)現(xiàn)不同的界面顯示,如下拉狀態(tài)時(shí),就顯示出箭頭,刷新?tīng)顟B(tài)時(shí),就顯示loading的圖標(biāo)。
可能的狀態(tài)值有:RESET, PULL_TO_REFRESH, RELEASE_TO_REFRESH, REFRESHING, NO_MORE_DATA
LoadingLayout及其派生類(lèi)的繼承關(guān)系如下圖所示:
圖五、LoadingLayout及其派生類(lèi)的類(lèi)圖
我們可以隨意地制定自己的Header和Footer,我們也可以實(shí)現(xiàn)如圖一和圖二中顯示的各種下拉刷新案例中的Header和Footer,只要重寫(xiě)上述兩個(gè)方法getContentSize()和setState()就行了。HeaderLoadingLayout,它默認(rèn)是顯示箭頭式樣的布局,而RotateLoadingLayout則是顯示一個(gè)旋轉(zhuǎn)圖標(biāo)的式樣。
5、事件處理
我們必須重寫(xiě)PullToRefreshBase類(lèi)的兩個(gè)事件相關(guān)的方法onInterceptTouchEvent()和onTouchEvent()方法。由于ListView,ScrollView,WebView它們是放到PullToRefreshBase內(nèi)部的,所在事件先是傳遞到PullToRefreshBase#onInterceptTouchEvent()方法中,所以我們應(yīng)該在這個(gè)方法中去處理ACTION_MOVE事件,判斷如果當(dāng)前ListView,ScrollView,WebView是否在最頂部或最底部,如果是,則開(kāi)始截?cái)嗍录?,一旦事件被截?cái)?,后續(xù)的事件就會(huì)傳遞到PullToRefreshBase#onInterceptTouchEvent()方法中,我們?cè)僭贏CTION_MOVE事件中去移動(dòng)整個(gè)布局,從而實(shí)現(xiàn)下拉或上拉動(dòng)作。
6、滾動(dòng)布局(scrollTo)
如圖三的布局結(jié)構(gòu)可知,默認(rèn)情況下Header和Footer是放置在Content View的最上面和最下面,通過(guò)設(shè)置padding來(lái)讓他跑到屏幕外面去了,如果我們將整個(gè)布局向下滾動(dòng)(scrollTo)一定距離,那么Header就會(huì)被顯示出來(lái),基于這種情況,所以在我的實(shí)現(xiàn)中,最終我是調(diào)用scrollTo來(lái)實(shí)現(xiàn)下拉動(dòng)作的。
總的說(shuō)來(lái),實(shí)現(xiàn)的重要的點(diǎn)就這些,具體的一些細(xì)節(jié)在實(shí)現(xiàn)在會(huì)碰到很多,可以參考代碼。
四. 如何使用
使用下拉刷新的代碼如下
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mPullListView = new PullToRefreshListView(this); setContentView(mPullListView); // 上拉加載不可用 mPullListView.setPullLoadEnabled(false); // 滾動(dòng)到底自動(dòng)加載可用 mPullListView.setScrollLoadEnabled(true); mCurIndex = mLoadDataCount; mListItems = new LinkedList<String>(); mListItems.addAll(Arrays.asList(mStrings).subList(0, mCurIndex)); mAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, mListItems); // 得到實(shí)際的ListView mListView = mPullListView.getRefreshableView(); // 綁定數(shù)據(jù) mListView.setAdapter(mAdapter); // 設(shè)置下拉刷新的listener mPullListView.setOnRefreshListener(new OnRefreshListener<ListView>() { @Override public void onPullDownToRefresh(PullToRefreshBase<ListView> refreshView) { mIsStart = true; new GetDataTask().execute(); } @Override public void onPullUpToRefresh(PullToRefreshBase<ListView> refreshView) { mIsStart = false; new GetDataTask().execute(); } }); setLastUpdateTime(); // 自動(dòng)刷新 mPullListView.doPullRefreshing(true, 500); }
這是初始化一個(gè)下拉刷新的布局,并且調(diào)用setContentView來(lái)設(shè)置到Activity中。
在下拉刷新完成后,我們可以調(diào)用onPullDownRefreshComplete()和onPullUpRefreshComplete()方法來(lái)停止刷新和加載
五. 運(yùn)行效果
這里列出了demo的運(yùn)行效果圖。
圖六、ListView下拉刷新,注意Header和Footer的樣式
六. Bug修復(fù)
已知bug修復(fù)情況如下,發(fā)現(xiàn)了代碼bug的看官也可以給我反饋,謝謝~~~
1,對(duì)于ListView的下拉刷新,當(dāng)啟用滾動(dòng)到底自動(dòng)加載時(shí),如果footer由隱藏變?yōu)轱@示時(shí),出現(xiàn)顯示異常的情況
這個(gè)問(wèn)題已經(jīng)修復(fù)了,修正的代碼如下:
PullToRefreshListView#setScrollLoadEnabled方法,修正后的代碼如下: @Override public void setScrollLoadEnabled(boolean scrollLoadEnabled) { if (isScrollLoadEnabled() == scrollLoadEnabled) { return; } super.setScrollLoadEnabled(scrollLoadEnabled); if (scrollLoadEnabled) { // 設(shè)置Footer if (null == mLoadMoreFooterLayout) { mLoadMoreFooterLayout = new FooterLoadingLayout(getContext()); mListView.addFooterView(mLoadMoreFooterLayout, null, false); } mLoadMoreFooterLayout.show(true); } else { if (null != mLoadMoreFooterLayout) { mLoadMoreFooterLayout.show(false); } } }
LoadingLayout#show方法,修正后的代碼如下:
/** * 顯示或隱藏這個(gè)布局 * * @param show flag */ public void show(boolean show) { // If is showing, do nothing. if (show == (View.VISIBLE == getVisibility())) { return; } ViewGroup.LayoutParams params = mContainer.getLayoutParams(); if (null != params) { if (show) { params.height = ViewGroup.LayoutParams.WRAP_CONTENT; } else { params.height = 0; } requestLayout(); setVisibility(show ? View.VISIBLE : View.INVISIBLE); } }
在更改LayoutParameter后,調(diào)用requestLayout()方法。
圖片旋轉(zhuǎn)兼容2.x系統(tǒng)
我之前想的是這個(gè)只需要兼容3.x以上的系統(tǒng),但發(fā)現(xiàn)有很多網(wǎng)友在使用過(guò)程中遇到過(guò)兼容性問(wèn)題,這次抽空將這個(gè)兼容性一并實(shí)現(xiàn)了。
onPull的修改如下:
@Override public void onPull(float scale) { if (null == mRotationHelper) { mRotationHelper = new ImageViewRotationHelper(mArrowImageView); } float angle = scale * 180f; // SUPPRESS CHECKSTYLE mRotationHelper.setRotation(angle); }
ImageViewRotationHelper主要的作用就是實(shí)現(xiàn)了ImageView的旋轉(zhuǎn)功能,內(nèi)部作了版本的區(qū)分,實(shí)現(xiàn)代碼如下:
/** * The image view rotation helper * * @author lihong06 * @since 2014-5-2 */ static class ImageViewRotationHelper { /** The imageview */ private final ImageView mImageView; /** The matrix */ private Matrix mMatrix; /** Pivot X */ private float mRotationPivotX; /** Pivot Y */ private float mRotationPivotY; /** * The constructor method. * * @param imageView the image view */ public ImageViewRotationHelper(ImageView imageView) { mImageView = imageView; } /** * Sets the degrees that the view is rotated around the pivot point. Increasing values * result in clockwise rotation. * * @param rotation The degrees of rotation. * * @see #getRotation() * @see #getPivotX() * @see #getPivotY() * @see #setRotationX(float) * @see #setRotationY(float) * * @attr ref android.R.styleable#View_rotation */ public void setRotation(float rotation) { if (APIUtils.hasHoneycomb()) { mImageView.setRotation(rotation); } else { if (null == mMatrix) { mMatrix = new Matrix(); // 計(jì)算旋轉(zhuǎn)的中心點(diǎn) Drawable imageDrawable = mImageView.getDrawable(); if (null != imageDrawable) { mRotationPivotX = Math.round(imageDrawable.getIntrinsicWidth() / 2f); mRotationPivotY = Math.round(imageDrawable.getIntrinsicHeight() / 2f); } } mMatrix.setRotate(rotation, mRotationPivotX, mRotationPivotY); mImageView.setImageMatrix(mMatrix); } } }
最核心的就是,如果在2.x的版本上,旋轉(zhuǎn)ImageView使用Matrix。
PullToRefreshBase構(gòu)造方法兼容2.x
在三個(gè)參數(shù)的構(gòu)造方法聲明如下標(biāo)注:
@SuppressLint("NewApi")
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android簡(jiǎn)易圖片瀏覽器的實(shí)現(xiàn)
最近做了一個(gè)圖片瀏覽小程序,本文主要介紹了Android簡(jiǎn)易圖片瀏覽器的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-03-03Android開(kāi)發(fā)筆記 Handler使用總結(jié)
當(dāng)應(yīng)用程序啟動(dòng)時(shí),Android首先會(huì)開(kāi)啟一個(gè)主線程(也就是UI線程),主線程為管理界面中的UI控件,進(jìn)行事件分發(fā)2012-11-11Ubuntu 14.04下創(chuàng)建Genymotion安卓虛擬機(jī)的步驟詳解
Android 模擬器一直以速度奇慢無(wú)比著稱(chēng),基本慢到不可用。本文介紹我一直在用的 Genymotion,速度不亞于真機(jī)。而且功能齊全,使用簡(jiǎn)單。下面這篇文章主要介紹了Ubuntu 14.04下創(chuàng)建Genymotion虛擬機(jī)的步驟,需要的朋友可以參考下。2017-03-03使用SharedPreferences在Android存儲(chǔ)對(duì)象詳細(xì)代碼
這篇文章主要介紹了使用SharedPreferences在Android存儲(chǔ)對(duì)象并附上詳細(xì)代碼,下面文章內(nèi)容較少,大多以代碼的形式體現(xiàn),需要的小伙伴可以參考一下,希望對(duì)你有所幫助2021-11-11Android實(shí)現(xiàn)讀取相機(jī)(相冊(cè))圖片并進(jìn)行剪裁
在 Android應(yīng)用中,很多時(shí)候我們需要實(shí)現(xiàn)上傳圖片,或者直接調(diào)用手機(jī)上的拍照功能拍照處理然后直接顯示并上傳功能,下面將講述調(diào)用相機(jī)拍照處理圖片然后顯示和調(diào)用手機(jī)相冊(cè)中的圖片處理然后顯示的功能2015-08-08Android ListView與getView調(diào)用卡頓問(wèn)題解決辦法
這篇文章主要介紹了Android ListView與getView調(diào)用卡頓問(wèn)題解決辦法的相關(guān)資料,這里提供實(shí)例及解決辦法幫助大家解決這種問(wèn)題,需要的朋友可以參考下2017-08-08Android實(shí)現(xiàn)Banner界面廣告圖片循環(huán)輪播(包括實(shí)現(xiàn)手動(dòng)滑動(dòng)循環(huán))
這篇文章主要介紹了Android實(shí)現(xiàn)Banner界面廣告圖片循環(huán)輪播(包括實(shí)現(xiàn)手動(dòng)滑動(dòng)循環(huán))的相關(guān)資料,需要的朋友可以參考下2016-02-02Android編程實(shí)現(xiàn)文件瀏覽功能的方法【類(lèi)似于FileDialog的功能】
這篇文章主要介紹了Android編程實(shí)現(xiàn)文件瀏覽功能的方法,可實(shí)現(xiàn)類(lèi)似于FileDialog的功能,涉及Android針對(duì)文件與目錄操作的相關(guān)技巧,需要的朋友可以參考下2016-11-11Android微信右滑退出功能的實(shí)現(xiàn)代碼
這篇文章主要介紹了Android微信右滑退出功能的實(shí)現(xiàn)代碼,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2018-01-01