自己動手寫RecyclerView的下拉重新整理

wizardev發表於2017-12-03

實現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新增頭部檢視
好了,現在已經為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>
複製程式碼

好了,看下效果圖

頭部修改為下拉重新整理
現在這個效果顯然不是我們想要的,我們想要的效果是在正常狀態下頭部的下拉重新整理不可見,當下拉到一定程度再顯示。

  這裡有兩點需要注意:

  1. 在正常的狀態下,下拉重新整理的頭部是不可見的;
  2. 當下拉到一定程度再將頭部重新整理顯示出來。

現在,來實現正常狀態下的效果,正常狀態下不可見,這時可以將頭部重新整理的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);
    }
}
複製程式碼

上面的程式碼不難,相信你一看就懂,這裡就不講解了,看下最終的效果,如圖

完成的效果圖

實現下拉重新整理的步驟總結

  1. 自定義Adapter繼承至RecyclerView的Adapter
  2. 重寫getItemType()方法
  3. 在onCreateViewHolder()方法中例項化RefreshHeader物件。
  4. 新建一個類繼承至LinearLayout並實現IRehreshView介面
  5. 在初始化時將佈局屬性設定為0,既隱藏頭部重新整理。
  6. 重寫RecyclerView主要是重寫RecyclerView的onTouchEvent方法,根據滑動的距離來顯示頭部重新整理的View
  7. 設定監聽

結束語

  相信按照上面的步驟,你一定可以自己動手實現RecyclerView的下拉重新整理功能。原始碼點選這裡獲取

轉載請註明出處:www.wizardev.com

歡迎關注我的公眾號
歡迎關注我的公眾號

相關文章