TRecyclerView-仿頭條、搜狐新聞对扶,實現(xiàn)RecyclerView的下拉更新区赵,上滑加載

1. 前言

我們先看看頭條、搜狐新聞的下拉更新效果(視頻轉(zhuǎn)gif時浪南,有些frame失真笼才,上滑加載的效果沒貼,太占地了??):


頭條-下拉更新
搜狐-下拉更新.gif

看過頭條络凿、搜狐新聞的下拉更新效果后骡送,我們看看自個寫的的TRecyclerView的下拉更新、上滑加載的效果圖絮记,下面也給出TRecyclerView的下載地址:

TRecyclerView-下拉更新
TRecyclerView-上滑加載

附:TRecyclerView項目地址TRecyclerView摔踱。

實現(xiàn)上面的效果,我們肯定得有一個托盤到千,假設是TRecyclerView,然后拖盤上面放了一個RecyclerView赴穗,下拉托盤超過一定距離后憔四,LoadingView顯示出來了,數(shù)據(jù)更新完后有一個更新多少條的提示般眉,假設是TipView了赵。

TRecyclerView包括LoadingViewRecyclerView甸赃、TipView柿汛,下面來講講這三個View的層次。下拉TRecyclerView,會露出LoadingView络断,可知LoadingView所處的層次是最下面裁替。

在試頭條、搜狐新聞下拉更新時貌笨,當列表正處在更新狀態(tài)弱判,這個時候,我們上推RecyclerView到頂锥惋,這個時候更新多少條的提示TipView會蓋在RecyclerView上面昌腰,可知TIpView所處的層次是最上面。

通過上面分析TRecyclerView中各個View的層次從上到下依次是:
TipView(頂部) 膀跌、 RecyclerView(中間) 遭商、 LoadingView(底部)

知道View的層次后捅伤,我們看看TRecyclerView下拉更新是怎么實現(xiàn)的劫流。

2. 下拉更新

我們結(jié)合TRecyclerView的header結(jié)構(gòu)圖,來分析下拉更新數(shù)據(jù)時暑认,RecyclerView的三個動作行為:

header結(jié)果圖

1) 下拉高度超過mHeaderHeight困介,松手之后,RecyclerView回到mHeaderHeight位置蘸际,同時請求網(wǎng)絡數(shù)據(jù)座哩;

2)網(wǎng)絡數(shù)據(jù)回來之后,RecyclerView回到mTipHeight位置粮彤,同時展示tips更新提示動畫;

3) tips更新提示動畫結(jié)束后根穷,RecyclerView回到頂部位置。

由此可知:RecyclerView整個下拉更新的動畫從時序上可以分為下面三個部分:

animToHeader (更新數(shù)據(jù)) -> animToTip (展示tips動畫) -> animToStart (回頂)

因此导坟,我們要在TRecyclerView的onInterceptTouchEvent屿良、onTouchEvent方法做一些事情:

1)onInterceptTouchEvent:判斷是否攔截MotionEvent事件,事件交給TRecyclerView或者RecyclerView處理惫周。

2)onTouchEvent:處理RecyclerView的下拉動畫尘惧,RecyclerView下拉是否觸發(fā)更新的邏輯。

下面還是看看TRecyclerView的onInterceptTouchEvent方法和onTouchEvent方法。

onInterceptTouchEvent(MotionEvent ev) 方法:

public boolean onInterceptTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        //if the recycleView can scroll, 
        //then the TRecyclerView doesn't intercept the event.
        if (isUnIntercept() || mRefresh) {
            return false;
        }
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mIsDrag = false;
                mInitY = ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                float y = ev.getY();
                //if the distance of moving is over the touchSlop, 
                //then The TRecyclerView is dragged.
                if (y - mInitY >= mTouchSlop && !mIsDrag) {
                    mIsDrag = true;
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mIsDrag = false;
                break;
            default:
                break;
        }

        return mIsDrag;
    }

看onInterceptTouchEvent的代碼盏求,其實是處理了兩個邏輯:

1)某些情況下不攔截event锣笨,把事件交給RecyclerView處理,只要RecyclerView 能夠滑動贰逾,就不攔截event;

2)如果RecyclerView已經(jīng)處在頂部菠秒,不能再向下滾動時疙剑,這個時候,事件交由TRecyclerView處理。

onTouchEvent(MotionEvent event) 方法:

public boolean onTouchEvent(MotionEvent event) {
       if (isUnIntercept()) {
           return false;
       }

       float dist = 0f;
       switch (event.getAction()) {
           case MotionEvent.ACTION_DOWN:
               mIsDrag = false;
               break;
           case MotionEvent.ACTION_MOVE:
               if (mIsDrag) {
                   float y = event.getY();
                   dist = (y - mInitY) * TRecycleViewConst.PULL_DRAG_RATE;

                   if(mCurrentTargetOffsetTop >= mOriginalOffsetTop) {
                     //如果下次移動的距離加上當前的距離頂部的距離
                    //小于header的初始位置言缤,則RecyclerView回頂,
                   // 同時檢查SuperSwipe是否移動頂部嚼蚀,RecycleView滑到頂部,
                   //則造一個down事件轧简,交給RecycleView處理驰坊,讓其可以繼續(xù)上滑。
                       if(dist  <  mOriginalOffsetTop ){
                           quickToStart();
                           buildDownEvent(event);
                       }else {
                           setTargetOffsetTopAndBottom(dist);

                       }
                   }else{
                       buildDownEvent(event);
                   }

                   //the distance of pull can trigger off refresh
                   if (mPullRefresh != null) {
                       mPullRefresh.pullRefreshEnable(dist >= mHeaderHeight);
                   }
               }

               break;
           case MotionEvent.ACTION_UP:
           case MotionEvent.ACTION_CANCEL:
               dist = (event.getY() - mInitY) *     
               TRecycleViewConst.PULL_DRAG_RATE;
               if (mIsDrag) {
                   //if the distance of moving is over the header height ,
                   // then show the anim which moves to header position, 
                   //else show the anim which moves to start position.
                   if (dist >= mHeaderHeight) {
                       animToHeader();
                   } else {
                       animToStart();
                   }
               }
               mIsDrag = false;
               break;
       }
       return true;
   }

我們庖丁解牛哮独,看看onTouchEvent的ACTION_UP和ACTION_MOVE的邏輯拳芙。

onTouchEvent - ACTION_UP

      ......      
      case MotionEvent.ACTION_CANCEL:
               dist = (event.getY() - mInitY) * TRecycleViewConst.PULL_DRAG_RATE;
               if (mIsDrag) {
                   //if the distance of moving is over the header height ,
                   // then show the anim which moves to header position, 
                  //else show the anim which moves to start position.
                   if (dist >= mHeaderHeight) {
                       animToHeader();
                   } else {
                       animToStart();
                   }
               }
      ......

說明:
1)當TRecyclerView攔截了event事件后,如果下拉距離超過mHeaderHeight皮璧,松手則觸發(fā)刷新邏輯舟扎,反之,觸發(fā)RecyclerView的回頂動畫悴务。

2)觸發(fā)刷新的邏輯是在animToHeader動畫結(jié)束之后做的睹限,onAnimationEnd回調(diào)里面調(diào)用了 mPullRefresh.pullRefresh(),業(yè)務邏輯可以通過該接口處理數(shù)據(jù)請求的邏輯讯檐。

animToHeader

      //the anim which moves to header position,
    //when the anim is end, start to refresh data
    private void animToHeader() {
        ObjectAnimator animator = ObjectAnimator.ofFloat(mRecyclerView, "translationY", mHeaderHeight);
        animator.addListener(mToHeaderListener);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mCurrentTargetOffsetTop = (float) animation.getAnimatedValue();
                Log.d(TAG, "animToHeader():" + "mCurrentTargetOffsetTop:" + mCurrentTargetOffsetTop);
            }
        });
        animator.setDuration(AnimDurConst.ANIM_TO_HEADER_DUR);
        animator.start();

    }

  private Animator.AnimatorListener mToHeaderListener
                       = new Animator.AnimatorListener() {
        @Override
        public void onAnimationStart(Animator animation) {

        }

        @Override
        public void onAnimationEnd(Animator animation) {
            //when the anim of move to header is end, start to refresh data
            if (mPullRefresh != null) {
                mRefresh = true;
                mPullRefresh.pullRefresh();
            }
        }

        @Override
        public void onAnimationCancel(Animator animation) {

        }

        @Override
        public void onAnimationRepeat(Animator animation) {

        }
    };

onTouchEvent - ACTION_MOVE

    ......
    case MotionEvent.ACTION_MOVE:
                if (mIsDrag) {
                    float y = event.getY();
                    dist = (y - mInitY) * TRecycleViewConst.PULL_DRAG_RATE;
                    if(mCurrentTargetOffsetTop >= mOriginalOffsetTop) {
                        if(dist  <  mOriginalOffsetTop ){
                            quickToStart();
                            buildDownEvent(event);
                        }else {
                            setTargetOffsetTopAndBottom(dist);
                        }
                    }else{
                        buildDownEvent(event);
                    }
                  ......
                }

                break;
    ......

說明:

1)TRecyclerView滿足當前位置 mCurrentTargetOffsetTop大于mOriginalOffsetTop(默認是0)羡疗、下拉距離dist大于mOriginalOffsetTop這兩個條件,則通過setTranslationY來垂直向下移動RecyclerView别洪。

//move the target by setTranslationY
    private void setTargetOffsetTopAndBottom(float offset) {
        mRecyclerView.setTranslationY(offset);
        mCurrentTargetOffsetTop = offset;
    }

2)TRecyclerView如果當前位置mCurrentTargetOffsetTop大于mOriginalOffsetTop叨恨,但是下拉距離dist小于mOriginalOffsetTop或者mCurrentTargetOffsetTop小于mOriginalOffsetTop,則造一個down事件挖垛,交給RecycleView處理痒钝,讓其可以繼續(xù)上滑。

下拉刷新講的差不多了痢毒,我們來看看上滑加載的實現(xiàn)送矩。

3. TRecyclerView構(gòu)成

下面會結(jié)合這TRecyclerView的結(jié)構(gòu)、TRecyclerAdapter的實現(xiàn)來講講TRecyclerView上滑加載數(shù)據(jù)的原理哪替。

TRecyclerView 的結(jié)構(gòu):
TRecycleView是一個FrameLayout主要包括兩部分栋荸,Header View和RecycleView,而RecycleView的View類型大體分為兩部分:Normal View和Footer View凭舶。

TRecyclerView中有一個TRecyclerAdapter晌块,是用來加載RecyclerView的Item View,是TRecyclerView中真正加載數(shù)據(jù)的Adapter库快,其中包括兩大類的數(shù)據(jù)類型摸袁,即正常的Normal View和Header View钥顽,Normal View是通過RecyclerView.Adapter來加載义屏,就是我們需要寫的Adapter。

TRecyclerView的結(jié)構(gòu)圖

TRecyclerView的初始化
下面結(jié)合TRecyclerView的結(jié)構(gòu)圖,我們看看具體的代碼實現(xiàn)闽铐,首先是TRecycleView的構(gòu)造方法:

 public TRecyclerView(Context context) {
        super(context);
        init(context);
    }

private void init(Context ctx) {
        mCtx = ctx;
        mTouchSlop = ViewConfiguration.get(mCtx).getScaledTouchSlop();
        initView();
    }

    private void initView() {
        mHeaderHolder = new HeaderHolder(mCtx);
        mHeaderHolder.setAnimListener(mAnimListener);
        addProgressView();
        addTargetView();
        addTipView();
        linearLayoutManager = new LinearLayoutManager(mCtx);
        mRecyclerView.setLayoutManager(linearLayoutManager);
        mRecyclerView.setVerticalScrollBarEnabled(true);
        initListener();
    }

    //add progress view
    private void addProgressView() {
        mHeaderHeight = (int) mCtx.getResources().getDimension(R.dimen.header_height);
        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, mHeaderHeight);
        params.gravity = Gravity.TOP;
        addView(mHeaderHolder.getProgressView(), params);
    }


    private void addTargetView() {
        // mRecyclerView = new RecyclerView(mCtx);
        mRecyclerView = (RecyclerView) LayoutInflater.from(mCtx).inflate(
                R.layout.recycler_view, this, false);
        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        addView(mRecyclerView, params);
    }

    // add tip view
    private void addTipView() {
        mTipHeight = (int) mCtx.getResources().getDimension(R.dimen.header_tip_height);
        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, mTipHeight);
        params.gravity = Gravity.TOP;
        addView(mHeaderHolder.getTipView(), params);

    }

TRecyclerAdapter的實現(xiàn)

我們知道TRecyclerView中真正加載數(shù)據(jù)的Adapter是TRecyclerAdapter蝶怔,我們看看TRecyclerView設置RecyclerView.Adapter的API,代碼如下:

  public void setAdapter(RecyclerView.Adapter adapter){
        adapter.registerAdapterDataObserver(mDataObserver);
        mTAdapter = new TRecyclerAdapter(mCtx, adapter);
        mRecyclerView.setAdapter(mTAdapter);

    }

我們給RecyclerView.Adapter注冊了一個觀察者兄墅,調(diào)用RecyclerView.Adapter的數(shù)據(jù)更新方法時踢星,會通知TRecyclerAdapter去更新數(shù)據(jù)數(shù)據(jù),代碼如下:

private RecyclerView.AdapterDataObserver mDataObserver 
                      = new RecyclerView.AdapterDataObserver() {
        @Override
        public void onChanged() {
            mTAdapter.notifyDataSetChanged();
        }


        @Override
        public void onItemRangeChanged(int positionStart, int itemCount) {
            mTAdapter.notifyItemRangeChanged(positionStart, itemCount);
        }

        @Override
        public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
            mTAdapter.notifyItemRangeChanged(positionStart , itemCount, payload);
        }

        @Override
        public void onItemRangeInserted(int positionStart, int itemCount) {
            mTAdapter.notifyItemRangeInserted(positionStart , itemCount);
        }

        @Override
        public void onItemRangeRemoved(int positionStart, int itemCount) {
            mTAdapter.notifyItemRangeRemoved(positionStart , itemCount);
        }

        @Override
        public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
            mTAdapter.notifyItemMoved(fromPosition, toPosition );
        }
    };

再看看TRecyclerAdapter的onCreateViewHolderonBindViewHolder方法的實現(xiàn)隙咸。

onCreateViewHolder方法:

public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return buildHolder(parent, viewType);
    }


private RecyclerView.ViewHolder buildHolder(ViewGroup parent, int viewType) {
        RecyclerView.ViewHolder holder = null;
        switch (viewType) {
            case ITEM_TYPE_FOOTER:
                //Footer View的類型
                holder = new BaseViewHolder(mFooterHolder.getFooterView());
                break;
            default:
              //Normal View 的類型
                holder = mAdapter.onCreateViewHolder(parent, viewType);
                break;
        }
        return holder;
    }


    @Override
    public int getItemViewType(int position) {
        if (isFooter(position)) {
            //底部View
            return ITEM_TYPE_FOOTER;
        } else {
            return mAdapter.getItemViewType(position);
        }
    }

onBindViewHolder方法:


//如果是Footer View類型沐悦,則直接返回,否則調(diào)用mAdapter的onBindViewHolder方法
    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        if (isFooter(position)) {
            return;
        }
        initData(holder, position);
    }

   private void initData(RecyclerView.ViewHolder holder, final int position) {
        final int type = getItemViewType(position);
        if (type != ITEM_TYPE_FOOTER) {
            mAdapter.onBindViewHolder(holder, position);
        }

    }

通過上面的代碼五督,我們知道是在create view holder時藏否,通過判斷viewType來判斷:

1)如果viewType是ITEM_TYPE_FOOTER,則認為ViewHolder是Footer類型充包,否則是Normal ViewHolder副签;

2)mAdapter是暴露給外部的RecyclerView.Adapter,但是真正加載數(shù)據(jù)的Adapter是TRecyclerAdapter基矮。

4. TRecyclerView上滑加載數(shù)據(jù)

看上面的結(jié)構(gòu)圖淆储,我們知道Footer View并不是直接作為TRecyclerView的一個View,而是RecyclerView的一個Item View家浇。
因此本砰,當RecyclerView上滑到最后一個Item View,即Footer View可見時蓝谨,我們可以通過 mPushRefresh.loadMore()來處理上滑加載數(shù)據(jù)的邏輯灌具,代碼的實現(xiàn)如下:

private void initListener(){
        mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                //如果RecyclerView的Scroll State是IDLE,我們判斷下RecyclerView
              //是否已經(jīng)滑動到底部譬巫,如果是則執(zhí)行l(wèi)oadMore方法回調(diào)
                if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                    if (targetInBottom()) {
                      if(mPushRefresh != null){
                          mLoadMore = true;
                          mPushRefresh.loadMore();
                      }
                    }
                }

            }
        });
    }

    //滑動到底部咖楣,且最后一個元素可見,則認為到達底部
    private boolean targetInBottom() {
        if (targetInTop()) {
            return false;
        }
        RecyclerView.LayoutManager layoutManager =     
                  mRecyclerView.getLayoutManager();
        int count = mRecyclerView.getAdapter().getItemCount();
        if (layoutManager instanceof LinearLayoutManager && count > 0) {
            LinearLayoutManager linearLayoutManager 
                        = (LinearLayoutManager) layoutManager;
            if (linearLayoutManager.findLastVisibleItemPosition() == count - 1) {
                return true;
            }
        } 
        return false;
    }

說明:
上滑加載更多的原理很簡單芦昔,其實我們就是判斷RecyclerView的Footer View
是否可見诱贿,可見則觸發(fā)加載更多的回調(diào)。

5. 總結(jié)

在寫TRecyclerView遇到TRecyclerView中的RecyclerView沒有滾動條咕缎,這是因為我們是直接new RecyclerView珠十,RecyclerView的一些初始化方法沒有執(zhí)行到,如受保護的initializeScrollbars 方法凭豪,在外部無法調(diào)用到的焙蹭。

解法方法:RecyclerView通過inflate的方式去加載一個xml文件。

工程用到的其它文件NewsRecyclerAdapter嫂伞、LoadingView等等孔厉,大家可以去github地址下載拯钻,下面附有項目地址。

TRecyclerView項目地址TRecyclerView撰豺。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末粪般,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子污桦,更是在濱河造成了極大的恐慌亩歹,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件凡橱,死亡現(xiàn)場離奇詭異小作,居然都是意外死亡,警方通過查閱死者的電腦和手機稼钩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門躲惰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人变抽,你說我怎么就攤上這事础拨。” “怎么了绍载?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵诡宗,是天一觀的道長。 經(jīng)常有香客問我击儡,道長塔沃,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任阳谍,我火速辦了婚禮蛀柴,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘矫夯。我一直安慰自己鸽疾,他們只是感情好,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布训貌。 她就那樣靜靜地躺著制肮,像睡著了一般。 火紅的嫁衣襯著肌膚如雪递沪。 梳的紋絲不亂的頭發(fā)上豺鼻,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天,我揣著相機與錄音款慨,去河邊找鬼儒飒。 笑死,一個胖子當著我的面吹牛檩奠,可吹牛的內(nèi)容都是我干的桩了。 我是一名探鬼主播届良,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼圣猎!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起乞而,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤送悔,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后爪模,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體欠啤,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年屋灌,在試婚紗的時候發(fā)現(xiàn)自己被綠了洁段。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡共郭,死狀恐怖祠丝,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情除嘹,我是刑警寧澤写半,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站尉咕,受9級特大地震影響叠蝇,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜年缎,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一悔捶、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧单芜,春花似錦蜕该、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至坛怪,卻和暖如春淤齐,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背袜匿。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工更啄, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人居灯。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓祭务,卻偏偏與公主長得像内狗,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子义锥,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容