編輯:關於Android編程
之前,我們介紹了下拉刷新上拉加載RecyclerView的使用,那麼現在,我們就來說一下這個下拉刷新是怎麼實現的。
在開發過程中,我想了兩種方案。一是使用LinearLayout嵌套頭部、recyclerview、尾部的方式,如下圖:
vcq9oaM8L3A+DQo8cD61q7rzwLSjrM7St8XG+sHL1eK49re9sLijrM6qyrLDtMTYo788L3A+DQo8cD7S8s6qtuC0zrOiytS21HJlY3ljbGVydmlld8Tasr+1xGZsaW5nysK8/r340NC0psDto6zX3MrHtO+yu7W919S8us/r0qq1xNCnufujrM7Sz+vSqrXEysejujxiciAvPg0KscjI57Wxx7DV/dTay6LQwqOsztLP8s/CZmxpbmcgUmVjeWNsZXJWaWV3o6zV4sqxuvJSZWN5Y2xlclZpZXfP8snPufa2r7W9tqWyv7rzo6zKo9Pgy9m2yLzM0PjCtrP2UmVmcmVzaEhlYWRlcqOstvjH0s7SsrvPsru2w7+0zra8yKvCtrP2wLSjrLb4ysfSqrjDwra24MnZvs3Ctrbgydmho7zytaW12Mu1o6y+zcrHztLP69KquPjIy9K71tbLotDCzbeyv77NysfBpcr009pSZWN5Y2xlclZpZXe1xKGisru05tTats+y47XEuNC+9aGjPC9wPg0KPHA+tvejrLauztLS4su8wvCjv6OouNW41cXCse2077K7x+Wz/qOszNi12LDRzazKwr3QwLS/tMv7tq6yu7auo6k8L3A+DQo8cD7X3Nauo6zV4tbWt72wuLSmwO21xNCnufvO0rK7wvrS4qOhxMfU9cO0sOzE2KO/1tjAtLDJo6zJvrT6wuso0MTU2rXO0aopoaM8L3A+DQo8cD7T2srH09DBy7Xatv7W1re9sLijujxzdHJvbmc+uPhSZWN5Y2xlclZpZXfM7bzTwb249s23sr+jrLfWsfDKx6O608PT2tTss8nPwsCt0Ke5+7XEuKjW+s23sr+hosui0MLNt7K/o7vM7bzTwb249s6ysr+jrLfWsfDKx6O6vNPU2M6ysr+jrNPD09rU7LPJyc/ArdCnufu1xLio1vrOsrK/oaO1sbustq+1vbalsr/KsaOsuMSx5Lio1vrNt7K/tcS437bIo6yw0cbky/tpdGVtzfnPws3Go6zU7LPJz8LArbXEuNC+9aO7yc/Arc2swO2hozwvc3Ryb25nPjwvcD4NCjxwPs7Su7nKx9TZu6249s28sMmjujwvcD4NCjxwPjxpbWcgYWx0PQ=="第二種方案" src="/uploadfile/Collfiles/20161105/20161105095117268.png" title="\" />
思路就是這樣,但在實際的開發過程中,下拉還好,而上拉會遇到各種各樣的問題,不過好在解決了這些問題後,實際的效果完美符合我的要求,所以WZMRecyclerView采用了這個方案進行實現。
接下來我們來依次介紹下拉和上拉,以及開發過程中遇到的問題。
其實下拉刷新是比較簡單的,PullToRefreshRecyclerView繼承於HeaderAndFooterRecyclerView,我們按順序來一一介紹PullToRefreshRecyclerView中的幾個主要方法:
首先介紹下全局變量,免得看代碼的時候吃力:// 當前狀態 private int mState = STATE_DEFAULT; // 初始 public final static int STATE_DEFAULT = 0; // 正在下拉 public final static int STATE_PULLING = 1; // 松手刷新 public final static int STATE_RELEASE_TO_REFRESH = 2; // 刷新中 public final static int STATE_REFRESHING = 3; // 下拉阻尼系數 private float mPullRatio = 0.5f; // 輔助頭部 private View topView; // 刷新頭部 private View mRefreshView; // 刷新頭部的高度 private int mRefreshViewHeight = 0; // 觸摸事件輔助,當RecyclerView滑動到頂部時,記錄觸摸事件的y軸坐標 private float mFirstY = 0; // 當前是否正在下拉 private boolean mPulling = false; // 是否可以下拉刷新 private boolean mRefreshEnable = true; // 回彈動畫 private ValueAnimator valueAnimator; // 刷新監聽 private OnRefreshListener mOnRefreshListener; // 刷新頭部構造器 private RefreshHeaderCreator mRefreshHeaderCreator;在構造函數中初始化,獲得默認的刷新頭部:
private void init(Context context) {
if (topView == null) {
topView = new View(context);
// 該view的高度不能為0,否則將無法判斷是否已滑動到頂部
topView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, 1));
// 設置默認LayoutManager
setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false));
// 初始化默認的刷新頭部
mRefreshHeaderCreator = new DefaultRefreshHeaderCreator();
mRefreshView = mRefreshHeaderCreator.getRefreshView(context,this);
}
}
在onLayout方法中,獲得刷新頭部的高度,並偏移RecyclerView:
/**
* 在layout的時候,隱藏刷新頭部
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (mRefreshView != null && mRefreshViewHeight == 0) {
mRefreshViewHeight = mRefreshView.getMeasuredHeight();
ViewGroup.MarginLayoutParams marginLayoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
marginLayoutParams.setMargins(marginLayoutParams.leftMargin, marginLayoutParams.topMargin-mRefreshViewHeight-1, marginLayoutParams.rightMargin, marginLayoutParams.bottomMargin);
setLayoutParams(marginLayoutParams);
}
}
觸摸事件:
@Override
public boolean onTouchEvent(MotionEvent e) {
// 若是不可以下拉
if (!mRefreshEnable) return super.onTouchEvent(e);
// 若刷新頭部為空,不處理
if (mRefreshView == null)
return super.onTouchEvent(e);
// 若回彈動畫正在進行,不處理
if (valueAnimator != null && valueAnimator.isRunning())
return super.onTouchEvent(e);
switch (e.getAction()) {
case MotionEvent.ACTION_MOVE:
if (!mPulling) {
if (isTop()) {
// 當listview滑動到最頂部時,記錄當前y坐標
mFirstY = e.getRawY();
}
// 若listview沒有滑動到最頂部,不處理
else
break;
}
float distance = (int) ((e.getRawY() - mFirstY)*mPullRatio);
// 若向上滑動(此時刷新頭部已隱藏),不處理
if (distance < 0) break;
mPulling = true;
// 若刷新中,距離需加上頭部的高度
if (mState == STATE_REFRESHING) {
distance += mRefreshViewHeight;
}
// 下拉
setState(distance);
return true;
case MotionEvent.ACTION_UP:
// 回彈
replyPull();
break;
}
return super.onTouchEvent(e);
}
判斷是否滑動到了頂部:
private boolean isTop() {
return !ViewCompat.canScrollVertically(this, -1);
}
設置當前下拉狀態:
private void setState(float distance) {
// 刷新中,狀態不變
if (mState == STATE_REFRESHING) {
}
else if (distance == 0) {
mState = STATE_DEFAULT;
}
// 松手刷新
else if (distance >= mRefreshViewHeight) {
int lastState = mState;
mState = STATE_RELEASE_TO_REFRESH;
if (mRefreshHeaderCreator != null)
if (!mRefreshHeaderCreator.onReleaseToRefresh(distance,lastState))
return;
}
// 正在拖動
else if (distance < mRefreshViewHeight) {
int lastState = mState;
mState = STATE_PULLING;
if (mRefreshHeaderCreator != null)
if (!mRefreshHeaderCreator.onStartPull(distance,lastState))
return;
}
// 開始下拉
startPull(distance);
}
這裡可以看到,當頭部構造器的onStartPull和onReleaseToRefresh返回false時,便不再下拉,其實這裡也是為了應對類似“超過多少就不再下拉了”這種需求。
改變輔助頭部的高度,造成下拉的效果:
private void startPull(float distance) {
// 輔助頭部的高度不能為0,否則將無法判斷是否已滑動到頂部
if (distance < 1)
distance = 1;
if (topView != null) {
LayoutParams layoutParams = (LayoutParams) topView.getLayoutParams();
layoutParams.height = (int) distance;
topView.setLayoutParams(layoutParams);
}
}
松手回彈,在這個方法中,我們需要判斷是直接刷新,還是直接回彈到原來位置:
private void replyPull() {
mPulling = false;
// 回彈位置
float destinationY = 0;
// 判斷當前狀態
// 若是刷新中,回彈
if (mState == STATE_REFRESHING) {
destinationY = mRefreshViewHeight;
}
// 若是松手刷新,刷新,回彈
else if (mState == STATE_RELEASE_TO_REFRESH) {
// 改變狀態
mState = STATE_REFRESHING;
// 刷新
if (mRefreshHeaderCreator != null)
mRefreshHeaderCreator.onStartRefreshing();
if (mOnRefreshListener != null)
mOnRefreshListener.onStartRefreshing();
// 若在onStartRefreshing中調用了completeRefresh方法,將不會滾回初始位置,因此這裡需加個判斷
if (mState != STATE_REFRESHING) return;
destinationY = mRefreshViewHeight;
} else if (mState == STATE_DEFAULT || mState == STATE_PULLING) {
mState = STATE_DEFAULT;
}
LayoutParams layoutParams = (RecyclerView.LayoutParams) topView.getLayoutParams();
float distance = layoutParams.height;
if (distance <= 0) return;
valueAnimator = ObjectAnimator.ofFloat(distance, destinationY).setDuration((long) (distance * 0.5));
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float nowDistance = (float) animation.getAnimatedValue();
startPull(nowDistance);
}
});
valueAnimator.start();
}
完成刷新:
public void completeRefresh() {
if (mRefreshHeaderCreator != null)
mRefreshHeaderCreator.onStopRefresh();
mState = STATE_DEFAULT;
replyPull();
mRealAdapter.notifyDataSetChanged();
}
在設置適配器的時候,添加輔助頭部和刷新頭部:
@Override
public void setAdapter(Adapter adapter) {
super.setAdapter(adapter);
if (mRefreshView != null) {
addHeaderView(topView);
addHeaderView(mRefreshView);
}
}
設置自定義的頭部:
public void setRefreshViewCreator(RefreshHeaderCreator refreshHeaderCreator) {
this.mRefreshHeaderCreator = refreshHeaderCreator;
mRefreshView = refreshHeaderCreator.getRefreshView(getContext(),this);
// 若有適配器,添加到頭部
if (mAdapter != null) {
addHeaderView(topView);
addHeaderView(mRefreshView);
}
mRealAdapter.notifyDataSetChanged();
}
以上就是PullToRefreshRecyclerView主要的幾個方法了,介紹得算比較清楚吧,再加上代碼中已經有注釋了,就不再累贅了。核心就一句話:攔截觸摸事件,改變輔助頭部的高度。 就是這麼easy~~~~
本來上拉加載我想單獨用一篇文章來介紹的,但其實上拉加載的處理和下拉刷新的處理邏輯是一致的,因此在這裡便一起介紹了吧,雙飛更開心呦客官~~
咳咳,說正經的,上面我們說過上拉加載會遇到各種問題,具體有哪些呢?
我們知道偏移RecyclerView是在onLayout函數中,但是在這個時候,你是拿不到加載尾部的高度的,measure(0,0)都沒用,為什麼呢?因為這個時候還不到他出場的時候啊,你催他也沒用。這時候你就會說了,那我getViewTreeObserver().addOnPreDrawListener呢?嘿嘿,我也試過了,這樣的確可以拿到高度,但太晚了,已經來不及偏移了,他已經出現在屏幕中了。
滑動到底部時,繼續上拉,改變輔助底部的高度造成上拉的效果,然後現實很骨感,你會發現(通過調試或打印)輔助底部的高度是在改變,但RecyclerView中的item並沒有擠上去啊,根本就沒有上拉的效果出現。
當你添加FooterView的時候,發現你添加的FooterView居然跑到刷新底部的下面去了,坑了個爹…..
以下是我的解決方法:
在開發下拉刷新的時候,我們並沒有這個問題,很明顯,因為我們的刷新頭部其實是第一二個item,在onLayout的時候,肯定會去測量他的寬高(onMeasure方法在onLayout之前),所以我們可以拿到刷新頭部的高度。這麼一來的話,我們可以把加載尾部添加到頭部中去,等得到了高度,我們再卸磨殺驢,把他remove掉,恩,就是這樣。
這個問題我實在沒想到什麼好辦法,因此用了最粗暴的方式:在改變高度後直接調用scrollToPosition滾動到最底部。這樣做有什麼後果呢?效率肯定是不高的,但為了效果,我可以忍….經過測試,StaggredLayoutManager不會有任何影響,效果溜溜哒。但是但是,LinearLayoutManager上拉時會出現卡頓的現象,這個怎麼忍!當然GridLayoutManager也會卡頓,畢竟他是LinearLayoutManager的兒子啊,遺傳病。為什麼呢?因為LinearLayoutManager對item的layout和StaggredLayoutManager的是不一樣的,既然StaggredLayoutManager沒問題,那麼我們用只有一列的StaggredLayoutManager替代LinearLayoutManager就是最粗暴的方法。當然,更好的方式是直接繼承LayoutManager寫一個自己的LinearLayoutManager,但由於時間和水平的限制,就……采用StaggredLayoutManager吧。這就是為什麼我之前說使用PullToLoadRecyclerView的時候,要用WZMLinearLayout和WZMGridLayoutManager。
這個問題其實最好解決,繼承HeaderAndFooterAdapter寫一個PullToLoadAdapter就可以啦。
雖然解決方法比較坑爹,但不管黑貓還是白貓,能抓老鼠的就是好貓。當然,這麼說有點過分了,所以在這裡,希望有大牛有更好的方法,歡迎到github上提交您的代碼,共同構建這個項目。
PullToLoadRecyclerView和PullToRefreshRecyclerView的代碼邏輯其實基本一致,而PullToLoadAdapter的代碼和HeaderAndFooterAdapter也比較像,因此這裡就不再展開了,有興趣的同學可以去github上把項目clone下來看看。
有沒有遇到過這種情況,當你辛辛苦苦找到一個需要的庫時,卻發現他的UI居然不支持自定義!摔!在實際開發中,產品和設計怎麼會允許你使用那個庫默認的UI設計,這是基本不可能的事。因此,支持自定義的刷新頭部和加載尾部是非常非常重要的事!!
之前在介紹使用方法時,我們就已經介紹了如何使用自定義的刷新頭部和加載尾部,而通過上面的代碼,你應該也已經理解了RefreshHeaderCreator和LoadFooterCreator的工作方式。
其實就是使用這兩個抽象類,把刷新頭部和加載尾部的UI與RecyclerView進行解耦,交給用戶自己去實現,項目中的默認刷新頭部和加載尾部就是很好的例子,相信你看完應該就知道怎麼去構造自己的刷新頭部和加載尾部了。
直接上DefaultRefreshHeaderCreator的代碼:
public class DefaultRefreshHeaderCreator extends RefreshHeaderCreator {
private View mRefreshView;
private ImageView iv;
private TextView tv;
private int rotationDuration = 200;
private int loadingDuration = 1000;
private ValueAnimator ivAnim;
@Override
public boolean onStartPull(float distance,int lastState) {
if (lastState == PullToRefreshRecyclerView.STATE_DEFAULT ) {
iv.setImageResource(R.drawable.arrow_down);
iv.setRotation(0f);
tv.setText("下拉刷新");
} else if (lastState == PullToRefreshRecyclerView.STATE_RELEASE_TO_REFRESH) {
startArrowAnim(0);
tv.setText("下拉刷新");
}
return true;
}
@Override
public void onStopRefresh() {
if (ivAnim != null) {
ivAnim.cancel();
}
}
@Override
public boolean onReleaseToRefresh(float distance,int lastState) {
if (lastState == PullToRefreshRecyclerView.STATE_DEFAULT ) {
iv.setImageResource(R.drawable.arrow_down);
iv.setRotation(-180f);
tv.setText("松手立即刷新");
} else if (lastState == PullToRefreshRecyclerView.STATE_PULLING) {
startArrowAnim(-180f);
tv.setText("松手立即刷新");
}
return true;
}
@Override
public void onStartRefreshing() {
iv.setImageResource(R.drawable.loading);
startLoadingAnim();
tv.setText("正在刷新...");
}
@Override
public View getRefreshView(Context context, RecyclerView recyclerView) {
mRefreshView = LayoutInflater.from(context).inflate(R.layout.layout_ptr_ptl,recyclerView,false);
iv = (ImageView) mRefreshView.findViewById(R.id.iv);
tv = (TextView) mRefreshView.findViewById(R.id.tv);
return mRefreshView;
}
private void startArrowAnim(float roration) {
if (ivAnim != null) {
ivAnim.cancel();
}
float startRotation = iv.getRotation();
ivAnim = ObjectAnimator.ofFloat(startRotation,roration).setDuration(rotationDuration);
ivAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
iv.setRotation((Float) animation.getAnimatedValue());
}
});
ivAnim.start();
}
private void startLoadingAnim() {
if (ivAnim != null) {
ivAnim.cancel();
}
ivAnim = ObjectAnimator.ofFloat(0,360).setDuration(loadingDuration);
ivAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
iv.setRotation((Float) animation.getAnimatedValue());
}
});
ivAnim.setRepeatMode(ObjectAnimator.RESTART);
ivAnim.setRepeatCount(ObjectAnimator.INFINITE);
ivAnim.setInterpolator(new LinearInterpolator());
ivAnim.start();
}
}
系不系很簡單?
照例上兩張用爛了的效果圖:



源碼地址:https://github.com/whichname/WZMRecyclerView
九宮圖比較常用的多控件布局(GridView)使用介紹
GridView跟ListView都是比較常用的多控件布局,而GridView更是實現九宮圖的首選!本文就是介紹如何使用GridView實現九宮圖。GridView的用法
Android實戰--簡單的模糊查詢
今天這一篇小案例模擬模糊查詢,即輸入一個字符,顯示手機對應的所有存在該字符的路徑。布局:
Android編程開發之seekBar采用handler消息處理操作的方法
本文實例講述了Android編程開發之seekBar采用handler消息處理操作的方法。分享給大家供大家參考,具體如下:該案例簡單實現進度條可走,可拖拽的功能,下面請看
Android開發之模仿微信打開網頁的進度條效果(高仿)
一,為什麼說是真正的高仿? 闡述這個問題前,先說下之前網上的,各位可以復制這段字,去百度一下 仿微信打開網頁的進度條效果 ,你會看到有很多類似的文章,不過他