實現RecyclerView下拉重新整理的功能,網上有很多文章,但大多文章都是將RecyclerView外面套了一層SwipRefreshLayout以此來達到下拉重新整理的目的!個人覺得這種方式不太優雅,於是通過查詢資料及閱讀原始碼,找到了一個比較優雅的方式實現RecyclerView的下拉重新整理,本文將帶你以不一樣的方式實現RecyclerView的下拉重新整理。
從基礎入手
萬丈高樓平地起,做什麼事都不是一蹴而就的,再偉大的工程也是一點點完成的。這裡就先從簡單的入手,先為RecyclerView新增頭部。
為RecyclerView新增頭部View
我們都知道實現RecyclerView的資料和檢視的繫結是通過繼承RecyclerView.Adapter類,同樣,為RecyclerView新增頭部也是需要繼承RecyclerView.Adapter類,然後,在子類裡面做文章。在新增頭部之前有必要先了解一下RecyclerView.Adapter的幾個常用的方法,分別為以下幾個方法
onCreateViewHolder(ViewGroup parent, int viewType)
複製程式碼
getItemViewType(int position)
複製程式碼
onBindViewHolder(MyViewHolder holder, int position)
複製程式碼
getItemCount() 複製程式碼
使用過ListView的都知道,為了優化ListView的效能,我們在寫Adapter時需要自己寫一個ViewHolder類來重用ListView中的item檢視,而在為RecyclerView實現Adapter時,強制我們要有一個ViewHolder。
onCreateViewHolder方法就是用來建立新View;
getItemViewType方法是可以根據不同的position可以返回不同的型別;
onBindViewHolder是將資料與檢視繫結;
getItemCount方法就是獲得需要顯示資料的總數。
瞭解了Adapter中的幾個方法,下面就利用這幾個方法為RecyclerView新增頭部View,下面上程式碼
@Override
public RefreshViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
//這裡的viewType即為getItemViewType返回的type
if (viewType == TYPE_REFRESH_HEADER) {
return new RefreshViewHolder(mInflater.inflate(R.layout.refresh_header_item, parent, false));
}
return new RefreshViewHolder(mInflater.inflate(R.layout.normal_item, parent, false));
}
@Override
public void onBindViewHolder(RefreshViewHolder holder, int position) {
}
@Override
public int getItemCount() {
return mLists.size();
}
@Override
public int getItemViewType(int position) {
//當position位置為0時,返回為頭部的型別
if (position == 0) {
return TYPE_REFRESH_HEADER;
}
return TYPE_NORMAL;
}
複製程式碼
可以看出,在onCreateViewHolder方法中,為不同的viewType設定了不同的型別,即為RecyclerView新增了頭部,看下效果圖
好了,現在已經為RecyclerView新增了頭部,下一步就是將頭部變成重新整理的View。新增下拉重新整理的View
現在將頭部View修改一下,直接上程式碼
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="bottom">
<RelativeLayout
android:id="@+id/header_content"
android:layout_width="match_parent"
android:layout_height="80dp"
android:paddingTop="10dip">
<TextView
android:id="@+id/tvRefreshStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="@string/pullRefresh"
android:textColor="#B5B5B5" />
<ImageView
android:id="@+id/ivHeaderArrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="35dp"
android:layout_marginRight="10dp"
android:layout_toLeftOf="@id/tvRefreshStatus"
android:src="@drawable/ic_pulltorefresh_arrow" />
</RelativeLayout>
</LinearLayout>
複製程式碼
好了,看下效果圖
現在這個效果顯然不是我們想要的,我們想要的效果是在正常狀態下頭部的下拉重新整理不可見,當下拉到一定程度再顯示。這裡有兩點需要注意:
- 在正常的狀態下,下拉重新整理的頭部是不可見的;
- 當下拉到一定程度再將頭部重新整理顯示出來。
現在,來實現正常狀態下的效果,正常狀態下不可見,這時可以將頭部重新整理的View高度設定為0,下面看下程式碼
public ArrowRefreshHeader(Context context) {
this(context,null);
}
public ArrowRefreshHeader(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public ArrowRefreshHeader(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();//初始化檢視
}
private void init() {
LinearLayout.LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
layoutParams.setMargins(0, 0, 0, 0);
this.setLayoutParams(layoutParams);
this.setPadding(0, 0, 0, 0);
//將refreshHeader高度設定為0
mContentLayout = (LinearLayout) LayoutInflater.from(getContext()).inflate(R.layout.refresh_header_item, null);
addView(mContentLayout, new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0));
//初始化控制元件
mArrowImageView = findViewById(R.id.ivHeaderArrow);
mStatusTextView = findViewById(R.id.tvRefreshStatus);
mProgressBar = findViewById(R.id.refreshProgress);
//初始化動畫
mRotateUpAnim = new RotateAnimation(0.0f, -180.0f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
mRotateUpAnim.setDuration(200);
mRotateUpAnim.setFillAfter(true);
mRotateDownAnim = new RotateAnimation(-180.0f, 0.0f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
mRotateDownAnim.setDuration(200);
mRotateDownAnim.setFillAfter(true);
//將mContentLayout的LayoutParams高度和寬度設為自動包裹並重新測量
measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
mMeasuredHeight = getMeasuredHeight();//獲得測量後的高度
}
複製程式碼
這裡使用了自定義View,寫了一個ArrowRefreshHeader繼承至LinearLayout,在構造方法中將頭部重新整理的View進行了初始化,這裡這句程式碼是關鍵
mContentLayout = (LinearLayout) LayoutInflater.from(getContext()).inflate(R.layout.refresh_header_item, null);
addView(mContentLayout, new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0));
複製程式碼
可以看到,這裡將頭部重新整理的View的高度設定成了0,這樣,就實現了在正常狀態下,頭部重新整理不顯示的效果。將RefreshHeaderAdapter的onCreateViewHolder方法修改一下,如下
@Override
public RefreshViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == TYPE_REFRESH_HEADER) {
// return new RefreshViewHolder(mInflater.inflate(R.layout.refresh_header_item, parent, false));
return new RefreshViewHolder(new ArrowRefreshHeader(mContext));
}
return new RefreshViewHolder(mInflater.inflate(R.layout.normal_item, parent, false));
}
複製程式碼
再看下效果
成功實現在正常狀態下不顯示頭部重新整理的效果,下面繼續實現第2步,當下拉到一定程度進行顯示。既然這裡有下拉,肯定涉及到了手勢監聽,自然是需要一個類繼承自RecyclerView,然後重寫onTouchEvent方法,下面看程式碼@Override
public boolean onTouchEvent(MotionEvent e) {
if (mLastY == -1) {
mLastY = e.getRawY();
}
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastY = e.getRawY();
sumOffSet = 0;
break;
case MotionEvent.ACTION_MOVE:
float deltaY = (e.getRawY() - mLastY) / 2;//為了防止滑動幅度過大,將實際手指滑動的距離除以2
mLastY = e.getRawY();
sumOffSet += deltaY;//計算總的滑動的距離
if (isOnTop() && !mRefreshing) {
mRefreshHeader.onMove(deltaY, sumOffSet);//介面回撥,移動重新整理的頭部View
if (mRefreshHeader.getVisibleHeight() > 0) {
return false;
}
}
break;
default:
mLastY = -1; // reset
if (isOnTop()&& !mRefreshing) {
if (mRefreshHeader.onRelease()) {
//手指鬆開
}
}
break;
}
return super.onTouchEvent(e);
}
複製程式碼
這部分程式碼就是獲取手指滑動的距離,然後利用介面回撥,將下拉的距離傳遞到自定義的View中,讓頭部重新整理的View改變距離及改變狀態。先看下定義的介面,介面中主要就是集中重新整理的狀態以及代表各個狀態的變數,程式碼如下
public interface IRefreshHeader {
int STATE_NORMAL = 0;//正常狀態
int STATE_RELEASE_TO_REFRESH = 1;//下拉的狀態
int STATE_REFRESHING = 2;//正在重新整理的狀態
int STATE_DONE = 3;//重新整理完成的狀態
void onReset();
/**
* 處於可以重新整理的狀態,已經過了指定距離
*/
void onPrepare();
/**
* 正在重新整理
*/
void onRefreshing();
/**
* 下拉移動
*/
void onMove(float offSet, float sumOffSet);
/**
* 下拉鬆開
*/
boolean onRelease();
/**
* 下拉重新整理完成
*/
void refreshComplete();
/**
* 獲取HeaderView
*/
View getHeaderView();
/**
* 獲取Header的顯示高度
*/
int getVisibleHeight();
}
複製程式碼
我們的自定義View即ArrowRefreshHeader這個類實現了上面的介面,上面說道,通過介面回撥的形式將移動的距離通過onMove方法回傳給了ArrowRefreshHeader,現在看下ArrowRefreshHeader中onMove方法的具體實現,如下
if (getVisibleHeight() > 0 || offSet > 0) {
setVisibleHeight((int) offSet + getVisibleHeight());
if (mState <= STATE_RELEASE_TO_REFRESH) { // 未處於重新整理狀態,更新箭頭
if (getVisibleHeight() > mMeasuredHeight) {
onPrepare();
} else {
onReset();
}
}
}
複製程式碼
這裡將移動的距離通過setVisibleHeight方法進行顯示,再看下這個方法的實現
public void setVisibleHeight(int height) {
if (height < 0) height = 0;
LayoutParams lp = (LayoutParams) mContentLayout.getLayoutParams();
lp.height = height;
mContentLayout.setLayoutParams(lp);
}
複製程式碼
這個方法的主要功能就是將距離設定給了LayoutParams中的height欄位,然後再重新設定mContentLayout的佈局屬性。
通過以上的方法,便可以實現當下拉到一定距離時顯示頭部重新整理View的功能了,下面看下實現的效果
到了這一步已經可以實現下拉以及重新整理的功能了,但是隻會一直重新整理,根本停不下來!這裡是因為我們沒有呼叫重新整理完成的回撥方法,下面開始實現重新整理完成是的功能。重新整理完成的實現及設定重新整理時的監聽
實現完成重新整理的功能
現在看下當重新整理的狀態變為重新整理完成,做了什麼
@Override
public void refreshComplete() {
setState(STATE_DONE);//設定重新整理的狀態為已完成
//延遲200ms後復位,主要是為了顯示“重新整理完成”的字樣,不延遲的話由於時間太短就看不見“重新整理完成”的字樣
new Handler().postDelayed(new Runnable() {
public void run() {
reset();
}
}, 200);
}
複製程式碼
可以看到refreshComplete方法主要是將狀態標誌位設定為已完成,同時延遲200ms將下來重新整理的狀態復位,下面分別看下setState方法和reset方法都做了什麼
public void setState(int state) {
//狀態沒有改變時什麼也不做
if (state == mState) return;
switch (state) {
case STATE_NORMAL:
if (mState == STATE_RELEASE_TO_REFRESH) {
mArrowImageView.startAnimation(mRotateDownAnim);
}
if (mState == STATE_REFRESHING) {
mArrowImageView.clearAnimation();
}
mStatusTextView.setText("下拉重新整理");
break;
case STATE_RELEASE_TO_REFRESH:
mArrowImageView.setVisibility(View.VISIBLE);
mProgressBar.setVisibility(View.INVISIBLE);
if (mState != STATE_RELEASE_TO_REFRESH) {
mArrowImageView.clearAnimation();
mArrowImageView.startAnimation(mRotateUpAnim);
mStatusTextView.setText("釋放立即重新整理");
}
break;
case STATE_REFRESHING:
mArrowImageView.clearAnimation();
mArrowImageView.setVisibility(View.INVISIBLE);
mProgressBar.setVisibility(View.VISIBLE);
smoothScrollTo(mMeasuredHeight);
mStatusTextView.setText("正在重新整理...");
break;
case STATE_DONE:
mArrowImageView.setVisibility(View.INVISIBLE);
mProgressBar.setVisibility(View.INVISIBLE);
mStatusTextView.setText("重新整理完成");
break;
default:
}
mState = state;//儲存當前重新整理的狀態
}
複製程式碼
在setState方法裡,主要及時根據不同的重新整理狀態的標誌,設定檢視的顯示隱藏以及文字的改變。
public void reset() {
smoothScrollTo(0);
setState(STATE_NORMAL);
}
複製程式碼
reset方法就是將頭部重新整理View的高度還設定為0,就是將頭部重新整理View隱藏通知將重新整理的狀態設定為STATE_NORMAL。
看完了方法,下面就在自己實現的RecyclerView中呼叫重新整理完成的方法,程式碼如下
public void refreshComplete() {
if (mRefreshing) {
mRefreshing = false;
mRefreshHeader.refreshComplete();
}
}
複製程式碼
設定重新整理時的監聽
這裡,將重新整理時的監聽放在當重新整理檢視顯示正在重新整理時,即當觸發了重新整理並且手指抬起時,可能說的難懂,相信看下程式碼就明白了
mLastY = -1; // reset
if (isOnTop()&& !mRefreshing) {
if (mRefreshHeader.onRelease()) {
//手指鬆開
if (mRefreshListener != null) {
mRefreshing = true;
mRefreshListener.onRefresh();//呼叫正在重新整理的監聽,在此方法中實現網路的請求操作。
}
}
}
複製程式碼
下拉重新整理的使用
下拉重新整理的功能已經全部完成了,下面看下怎麼使用
public class MainActivity extends AppCompatActivity {
private List<String> mStringList = new ArrayList<>();
private RefreshHeaderRecyclerView mRecyclerView;
private RefreshHeaderAdapter mAdapter;
private static final int FINISH = 1;
@SuppressLint("HandlerLeak")
private Handler sHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == FINISH) {
Toast.makeText(MainActivity.this,"重新整理完成!",Toast.LENGTH_SHORT).show();
mRecyclerView.refreshComplete();
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initData();
setupRecyclerView();
}
private void setupRecyclerView() {
mRecyclerView = findViewById(R.id.recyclerView);
mAdapter = new RefreshHeaderAdapter(mStringList, this);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mRecyclerView.setAdapter(mAdapter);
mRecyclerView.setOnRefreshListener(new OnRefreshListener() {
@Override
public void onRefresh() {
requestData();//模擬資料的請求
}
});
}
private void requestData() {
new Thread(new Runnable() {
@Override
public void run() {
MainActivity.this.toString();
try {
Thread.sleep(1500);
Message message = Message.obtain();
message.what = FINISH;
sHandler.sendMessage(message);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
private void initData() {
for (int i = 0; i < 15; i++) {
mStringList.add("");
}
}
@Override
protected void onDestroy() {
super.onDestroy();
sHandler.removeCallbacks(null);
}
}
複製程式碼
上面的程式碼不難,相信你一看就懂,這裡就不講解了,看下最終的效果,如圖
實現下拉重新整理的步驟總結
- 自定義Adapter繼承至RecyclerView的Adapter
- 重寫getItemType()方法
- 在onCreateViewHolder()方法中例項化RefreshHeader物件。
- 新建一個類繼承至LinearLayout並實現IRehreshView介面
- 在初始化時將佈局屬性設定為0,既隱藏頭部重新整理。
- 重寫RecyclerView主要是重寫RecyclerView的onTouchEvent方法,根據滑動的距離來顯示頭部重新整理的View
- 設定監聽
結束語
相信按照上面的步驟,你一定可以自己動手實現RecyclerView的下拉重新整理功能。原始碼點選這裡獲取
轉載請註明出處:www.wizardev.com