編輯:關於Android編程
前段時間項目中用到了下拉刷新功能,之前在網上也找到過類似的demo,但這些demo的質量參差不齊,用戶體驗也不好,接口設計也不行。最張沒辦法,終於忍不了了,自己就寫了一個下拉刷新的框架,這個框架是一個通用的框架,效果和設計感覺都還不錯,現在分享給各位看官。
一. 關於下拉刷新
下拉刷新這種用戶交互最早由twitter創始人洛倫•布裡切特(Loren Brichter)發明,有理論認為,下拉刷新是一種適用於按照從新到舊的時間順序排列feeds的應用,在這種應用場景中看完舊的內容時,用戶會很自然地下拉查找更新的內容,因此下拉刷新就顯得非常合理。大家可以參考這篇文章:有趣的下拉刷新,下面我貼出一個有趣的下拉刷新的案例。
圖一、有趣的下拉刷新案例(一)
圖二、有趣的下拉刷新案例(二)
二. 實現原理
上面這些例子,外觀做得再好看,他的本質上都一樣,那就是一個下拉刷新控件通常由以下幾部分組成:
【1】Header
Header通常有下拉箭頭,文字,進度條等元素,根據下拉的距離來改變它的狀態,從而顯示不同的樣式
【2】Content
這部分是內容區域,網上有很多例子都是直接在ListView裡面添加Header,但這就有局限性,因為好多情況下並不一定是用ListView來顯示數據。我們把要顯示內容的View放置在我們的一個容器中,如果你想實現一個用ListView顯示數據的下拉刷新,你需要創建一個ListView旋轉到我的容器中。我們處理這個容器的事件(down, move, up),如果向下拉,則把整個布局向下滑動,從而把header顯示出來。
【3】Footer
Footer可以用來顯示向上拉的箭頭,自動加載更多的進度條等。
以上三部分總結的說來,就是如下圖所示的這種布局結構:
圖三,下拉刷新的布局結構
關於上圖,需要說明幾點:
1、這個布局擴展於LinearLayout,垂直排列
2、從上到下的順序是:Header, Content, Footer
3、Content填充滿父控件,通過設置top, bottom的padding來使Header和Footer不可見,也就是讓它超出屏幕外
4、下拉時,調用scrollTo方法來將整個布局向下滑動,從而把Header顯示出來,上拉正好與下拉相反。
5、派生類需要實現的是:將Content View填充到父容器中,比如,如果你要使用的話,那麼你需要把ListView, ScrollView, WebView等添加到容器中。
6、上圖中的紅色區域就是屏的大小(嚴格來說,這裡說屏幕大小並不准確,應該說成內容區域更加准確)
三. 具體實現
明白了實現原理與過程,我們嘗試來具體實現,首先,為了以後更好地擴展,設計更加合理,我們把下拉刷新的功能抽象成一個接口:
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); }
這個接口是一個泛型的,它接受View的派生類,因為要放到我們的容器中的不就是一個View嗎?
2、PullToRefreshBase<T extends View>
這個類實現了IPullToRefresh接口,它是從LinearLayout繼承過來,作為下拉刷新的一個抽象基類,如果你想實現ListView的下拉刷新,只需要擴展這個類,實現一些必要的方法就可以了。這個類的職責主要有以下幾點:
3、PullToRefreshBase<T extends View>繼承關系
這裡我實現了三個下拉刷新的派生類,分別是ListView、ScrollView、WebView三個,它們的繼承關系如下:
圖四、PullToRefreshBase類的繼承關系
關於PullToRefreshBase類及其派和類,有幾點需要說明:
對於ListView,ScrollView,WebView這三種情況,他們是否滑動到最頂部或是最底部的實現是不一樣的,所以,在PullToRefreshBase類中需要調用兩個抽象方法來判斷當前的位置是否在頂部或底部,而其派生類必須要實現這兩個方法。比如對於ListView,它滑動到最頂部的條件就是第一個child完全可見並且first postion是0。這兩個抽象方法是:
/** * 判斷刷新的View是否滑動到頂部 * * @return true表示已經滑動到頂部,否則false */ protected abstract boolean isReadyForPullDown(); /** * 判斷刷新的View是否滑動到底 * * @return true表示已經滑動到底部,否則false */ protected abstract boolean isReadyForPullUp();
創建可下拉刷新的View(也就是content view)的抽象方法是
/** * 創建可以刷新的View * * @param context context * @param attrs 屬性 * @return View */ protected abstract T createRefreshableView(Context context, AttributeSet attrs);
4、LoadingLayout
LoadingLayout是刷新Layout的一個抽象,它是一個抽象基類。Header和Footer都擴展於這個類。這類抽象類,提供了兩個抽象方法:
getContentSize
這個方法返回當前這個刷新Layout的大小,通常返回的是布局的高度,為了以後可以擴展為水平拉動,所以方法名字沒有取成getLayoutHeight()之類的,這個返回值,將會作為松手後是否可以刷新的臨界值,如果下拉的偏移值大於這個值,就認為可以刷新,否則不刷新,這個方法必須由派生類來實現。
setState
這個方法用來設置當前刷新Layout的狀態,PullToRefreshBase類會調用這個方法,當進入下拉,松手等動作時,都會調用這個方法,派生類裡面只需要根據這些狀態實現不同的界面顯示,如下拉狀態時,就顯示出箭頭,刷新狀態時,就顯示loading的圖標。
可能的狀態值有:RESET, PULL_TO_REFRESH, RELEASE_TO_REFRESH, REFRESHING, NO_MORE_DATA
LoadingLayout及其派生類的繼承關系如下圖所示:
圖五、LoadingLayout及其派生類的類圖
我們可以隨意地制定自己的Header和Footer,我們也可以實現如圖一和圖二中顯示的各種下拉刷新案例中的Header和Footer,只要重寫上述兩個方法getContentSize()和setState()就行了。HeaderLoadingLayout,它默認是顯示箭頭式樣的布局,而RotateLoadingLayout則是顯示一個旋轉圖標的式樣。
5、事件處理
我們必須重寫PullToRefreshBase類的兩個事件相關的方法onInterceptTouchEvent()和onTouchEvent()方法。由於ListView,ScrollView,WebView它們是放到PullToRefreshBase內部的,所在事件先是傳遞到PullToRefreshBase#onInterceptTouchEvent()方法中,所以我們應該在這個方法中去處理ACTION_MOVE事件,判斷如果當前ListView,ScrollView,WebView是否在最頂部或最底部,如果是,則開始截斷事件,一旦事件被截斷,後續的事件就會傳遞到PullToRefreshBase#onInterceptTouchEvent()方法中,我們再在ACTION_MOVE事件中去移動整個布局,從而實現下拉或上拉動作。
6、滾動布局(scrollTo)
如圖三的布局結構可知,默認情況下Header和Footer是放置在Content View的最上面和最下面,通過設置padding來讓他跑到屏幕外面去了,如果我們將整個布局向下滾動(scrollTo)一定距離,那麼Header就會被顯示出來,基於這種情況,所以在我的實現中,最終我是調用scrollTo來實現下拉動作的。
總的說來,實現的重要的點就這些,具體的一些細節在實現在會碰到很多,可以參考代碼。
四. 如何使用
使用下拉刷新的代碼如下
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mPullListView = new PullToRefreshListView(this); setContentView(mPullListView); // 上拉加載不可用 mPullListView.setPullLoadEnabled(false); // 滾動到底自動加載可用 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); // 得到實際的ListView mListView = mPullListView.getRefreshableView(); // 綁定數據 mListView.setAdapter(mAdapter); // 設置下拉刷新的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(); // 自動刷新 mPullListView.doPullRefreshing(true, 500); }
這是初始化一個下拉刷新的布局,並且調用setContentView來設置到Activity中。
在下拉刷新完成後,我們可以調用onPullDownRefreshComplete()和onPullUpRefreshComplete()方法來停止刷新和加載
五. 運行效果
這裡列出了demo的運行效果圖。
圖六、ListView下拉刷新,注意Header和Footer的樣式
六. Bug修復
已知bug修復情況如下,發現了代碼bug的看官也可以給我反饋,謝謝~~~
1,對於ListView的下拉刷新,當啟用滾動到底自動加載時,如果footer由隱藏變為顯示時,出現顯示異常的情況
這個問題已經修復了,修正的代碼如下:
PullToRefreshListView#setScrollLoadEnabled方法,修正後的代碼如下: @Override public void setScrollLoadEnabled(boolean scrollLoadEnabled) { if (isScrollLoadEnabled() == scrollLoadEnabled) { return; } super.setScrollLoadEnabled(scrollLoadEnabled); if (scrollLoadEnabled) { // 設置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方法,修正後的代碼如下:
/** * 顯示或隱藏這個布局 * * @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後,調用requestLayout()方法。
圖片旋轉兼容2.x系統
我之前想的是這個只需要兼容3.x以上的系統,但發現有很多網友在使用過程中遇到過兼容性問題,這次抽空將這個兼容性一並實現了。
onPull的修改如下:
@Override public void onPull(float scale) { if (null == mRotationHelper) { mRotationHelper = new ImageViewRotationHelper(mArrowImageView); } float angle = scale * 180f; // SUPPRESS CHECKSTYLE mRotationHelper.setRotation(angle); }
ImageViewRotationHelper主要的作用就是實現了ImageView的旋轉功能,內部作了版本的區分,實現代碼如下:
/** * 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(); // 計算旋轉的中心點 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的版本上,旋轉ImageView使用Matrix。
PullToRefreshBase構造方法兼容2.x
在三個參數的構造方法聲明如下標注:
@SuppressLint("NewApi")
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持本站。
選擇繪制三角形作為OpenGL ES 2.0的第一個實例,是因為前文中提到的,點、線、三角形是OpenGL ES世界的圖形基礎。無論多麼復雜的幾何物體,在OpenGL E
SQLite數據庫是android系統內嵌的數據庫,小巧強大,能夠滿足大多數SQL語句的處理工作,而SQLite數據庫僅僅是個文件而已。雖然SQLite的有點很多,但並不
一.前言(1).由於MIUI等部分國產定制系統也有權限管理,沒有相關api,故無法判斷用戶是否允許獲取聯系人等隱私。在Android 6.0之後,新增權限管理可以通過官方
在之前的第一篇文章: android自動化測試中hierarchyviewer和uiautomatorviewer獲取控件信息的方式比對(1) 我說uiauto