本身動手寫RecyclerView的下拉刷新

實現RecyclerView下拉刷新的功能,網上有不少文章,但大多文章都是將RecyclerView外面套了一層SwipRefreshLayout以此來達到下拉刷新的目的!我的以爲這種方式不太優雅,因而經過查找資料及閱讀源碼,找到了一個比較優雅的方式實現RecyclerView的下拉刷新,本文將帶你以不同的方式實現RecyclerView的下拉刷新。java

從基礎入手

  萬丈高樓平地起,作什麼事都不是一蹴而就的,再偉大的工程也是一點點完成的。這裏就先從簡單的入手,先爲RecyclerView添加頭部。android

爲RecyclerView添加頭部View

  咱們都知道實現RecyclerView的數據和視圖的綁定是經過繼承RecyclerView.Adapter類,一樣,爲RecyclerView添加頭部也是須要繼承RecyclerView.Adapter類,而後,在子類裏面作文章。在添加頭部以前有必要先了解一下RecyclerView.Adapter的幾個經常使用的方法,分別爲如下幾個方法git

onCreateViewHolder(ViewGroup parent, int viewType)
複製代碼
getItemViewType(int position)
複製代碼
onBindViewHolder(MyViewHolder holder, int position)
複製代碼
getItemCount()
複製代碼

使用過ListView的都知道,爲了優化ListView的性能,咱們在寫Adapter時須要本身寫一個ViewHolder類來重用ListView中的item視圖,而在爲RecyclerView實現Adapter時,強制咱們要有一個ViewHolder。github

onCreateViewHolder方法就是用來建立新View;markdown

getItemViewType方法是能夠根據不一樣的position能夠返回不一樣的類型;網絡

onBindViewHolder是將數據與視圖綁定;ide

getItemCount方法就是得到須要顯示數據的總數。oop

  瞭解了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添加了頭部,看下效果圖 post

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

歡迎關注個人公衆號
歡迎關注個人公衆號
相關文章
相關標籤/搜索