Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> android 打造真正的下拉刷新上拉加載recyclerview(三):下拉刷新上拉加載

android 打造真正的下拉刷新上拉加載recyclerview(三):下拉刷新上拉加載

編輯:關於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="\" />

在onLayout中,通過設置RecyclerView的margin,將頭部和尾部偏移出屏幕; 輔助頭部:初始高度為1px;當RecyclerView滑動到頂部時,通過改變高度,造成下拉效果; 輔助尾部:初始高度為1px;當RecyclerView滑動到底部時,通過改變高度,造成上拉的效果

思路就是這樣,但在實際的開發過程中,下拉還好,而上拉會遇到各種各樣的問題,不過好在解決了這些問題後,實際的效果完美符合我的要求,所以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();
    }

}

系不系很簡單?

照例上兩張用爛了的效果圖:

刷新加載

Grid刷新加載

Staggred刷新加載

源碼地址:https://github.com/whichname/WZMRecyclerView

 
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved