天天看點

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

實作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 == ) {
            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>
           

好了,看下效果圖

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

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

  這裡有兩點需要注意:

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

現在,來實作正常狀态下的效果,正常狀态下不可見,這時可以将頭部重新整理的View高度設定為0,下面看下代碼

public ArrowRefreshHeader(Context context) {
        this(context,null);
    }

    public ArrowRefreshHeader(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,);
    }

    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(, , , );
        this.setLayoutParams(layoutParams);
        this.setPadding(, , , );

        //将refreshHeader高度設定為0
        mContentLayout = (LinearLayout) LayoutInflater.from(getContext()).inflate(R.layout.refresh_header_item, null);
        addView(mContentLayout, new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ));

        //初始化控件
        mArrowImageView = findViewById(R.id.ivHeaderArrow);
        mStatusTextView =  findViewById(R.id.tvRefreshStatus);
        mProgressBar = findViewById(R.id.refreshProgress);

        //初始化動畫
        mRotateUpAnim = new RotateAnimation(f, -f,
                Animation.RELATIVE_TO_SELF, f, Animation.RELATIVE_TO_SELF, f);
        mRotateUpAnim.setDuration();
        mRotateUpAnim.setFillAfter(true);
        mRotateDownAnim = new RotateAnimation(-f, f,
                Animation.RELATIVE_TO_SELF, f, Animation.RELATIVE_TO_SELF, f);
        mRotateDownAnim.setDuration();
        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, ));
           

可以看到,這裡将頭部重新整理的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));
    }
           

再看下效果

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

成功實作在正常狀态下不顯示頭部重新整理View的效果,下面繼續實作第2步,當下拉到一定程度進行顯示。既然這裡有下拉,肯定涉及到了手勢監聽,自然是需要一個類繼承自RecyclerView,然後重寫onTouchEvent方法,下面看代碼

@Override
    public boolean onTouchEvent(MotionEvent e) {
        if (mLastY == -) {
            mLastY = e.getRawY();
        }
        switch (e.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastY = e.getRawY();
                sumOffSet = ;
                break;
            case MotionEvent.ACTION_MOVE:
                float deltaY = (e.getRawY() - mLastY) / ;//為了防止滑動幅度過大,将實際手指滑動的距離除以2
                mLastY = e.getRawY();
                sumOffSet += deltaY;//計算總的滑動的距離
                if (isOnTop() && !mRefreshing) {
                    mRefreshHeader.onMove(deltaY, sumOffSet);//接口回調,移動重新整理的頭部View
                    if (mRefreshHeader.getVisibleHeight() > ) {
                        return false;
                    }
                }
                break;
                default:
                    mLastY = -; // reset
                    if (isOnTop()&& !mRefreshing) {
                        if (mRefreshHeader.onRelease()) {
                            //手指松開
                        }
                    }
                    break;
        }
        return super.onTouchEvent(e);
    }
           

這部分代碼就是擷取手指滑動的距離,然後利用接口回調,将下拉的距離傳遞到自定義的View中,讓頭部重新整理的View改變距離及改變狀态。先看下定義的接口,接口中主要就是集中重新整理的狀态以及代表各個狀态的變量,代碼如下

public interface IRefreshHeader {
    int STATE_NORMAL = ;//正常狀态
    int STATE_RELEASE_TO_REFRESH = ;//下拉的狀态
    int STATE_REFRESHING = ;//正在重新整理的狀态
    int STATE_DONE = ;//重新整理完成的狀态

    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() >  || offSet > ) {
            setVisibleHeight((int) offSet + getVisibleHeight());
            if (mState <= STATE_RELEASE_TO_REFRESH) { // 未處于重新整理狀态,更新箭頭
                if (getVisibleHeight() > mMeasuredHeight) {
                    onPrepare();
                } else {
                    onReset();
                }
            }
        }
           

這裡将移動的距離通過setVisibleHeight方法進行顯示,再看下這個方法的實作

public void setVisibleHeight(int height) {
        if (height < ) height = ;
        LayoutParams lp = (LayoutParams) mContentLayout.getLayoutParams();
        lp.height = height;
        mContentLayout.setLayoutParams(lp);
    }
           

這個方法的主要功能就是将距離設定給了LayoutParams中的height字段,然後再重新設定mContentLayout的布局屬性。

  通過以上的方法,便可以實作當下拉到一定距離時顯示頭部重新整理View的功能了,下面看下實作的效果

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

到了這一步已經可以實作下拉以及重新整理的功能了,但是隻會一直重新整理,根本停不下來!這裡是因為我們沒有調用重新整理完成的回調方法,下面開始實作重新整理完成是的功能。

重新整理完成的實作及設定重新整理時的監聽

實作完成重新整理的功能

現在看下當重新整理的狀态變為重新整理完成,做了什麼

@Override
    public void refreshComplete() {
        setState(STATE_DONE);//設定重新整理的狀态為已完成
        //延遲200ms後複位,主要是為了顯示“重新整理完成”的字樣,不延遲的話由于時間太短就看不見“重新整理完成”的字樣
        new Handler().postDelayed(new Runnable() {
            public void run() {
                reset();
            }
        }, );
    }
           

可以看到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();
        setState(STATE_NORMAL);
    }
           

reset方法就是将頭部重新整理View的高度還設定為0,就是将頭部重新整理View隐藏通知将重新整理的狀态設定為STATE_NORMAL。

看完了方法,下面就在自己實作的RecyclerView中調用重新整理完成的方法,代碼如下

public void refreshComplete() {
        if (mRefreshing) {
            mRefreshing = false;
            mRefreshHeader.refreshComplete();
        }
    }
           

設定重新整理時的監聽

這裡,将重新整理時的監聽放在當重新整理視圖顯示正在重新整理時,即當觸發了重新整理并且手指擡起時,可能說的難懂,相信看下代碼就明白了

mLastY = -; // 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 = ;

    @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();
                    Message message = Message.obtain();
                    message.what = FINISH;
                    sHandler.sendMessage(message);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }).start();
    }

    private void initData() {
        for (int i = ; i < ; i++) {
            mStringList.add("");
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        sHandler.removeCallbacks(null);
    }
}
           

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

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

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

  1. 自定義Adapter繼承至RecyclerView的Adapter
  2. 重寫getItemType()方法
  3. 在onCreateViewHolder()方法中執行個體化RefreshHeader對象。
  4. 建立一個類繼承至LinearLayout并實作IRehreshView接口
  5. 在初始化時将布局屬性設定為0,既隐藏頭部重新整理。
  6. 重寫RecyclerView主要是重寫RecyclerView的onTouchEvent方法,根據滑動的距離來顯示頭部重新整理的View
  7. 設定監聽

結束語

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

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