TRecyclerView-仿头条、搜狐新闻,实现RecyclerView的下拉更新,上滑加载

1. 前言

我们先看看头条、搜狐新闻的下拉更新效果(视频转gif时,有些frame失真,上滑加载的效果没贴,太占地了😅):


头条-下拉更新
搜狐-下拉更新.gif

看过头条、搜狐新闻的下拉更新效果后,我们看看自个写的的TRecyclerView的下拉更新、上滑加载的效果图,下面也给出TRecyclerView的下载地址:

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

附:TRecyclerView项目地址TRecyclerView

实现上面的效果,我们肯定得有一个托盘,假设是TRecyclerView,然后拖盘上面放了一个RecyclerView,下拉托盘超过一定距离后,LoadingView显示出来了,数据更新完后有一个更新多少条的提示,假设是TipView

TRecyclerView包括LoadingViewRecyclerViewTipView,下面来讲讲这三个View的层次。下拉TRecyclerView,会露出LoadingView,可知LoadingView所处的层次是最下面。

在试头条、搜狐新闻下拉更新时,当列表正处在更新状态,这个时候,我们上推RecyclerView到顶,这个时候更新多少条的提示TipView会盖在RecyclerView上面,可知TIpView所处的层次是最上面。

通过上面分析TRecyclerView中各个View的层次从上到下依次是:
TipView(顶部) 、 RecyclerView(中间) 、 LoadingView(底部)

知道View的层次后,我们看看TRecyclerView下拉更新是怎么实现的。

2. 下拉更新

我们结合TRecyclerView的header结构图,来分析下拉更新数据时,RecyclerView的三个动作行为:

header结果图

1) 下拉高度超过mHeaderHeight,松手之后,RecyclerView回到mHeaderHeight位置,同时请求网络数据;

2)网络数据回来之后,RecyclerView回到mTipHeight位置,同时展示tips更新提示动画;

3) tips更新提示动画结束后,RecyclerView回到顶部位置。

由此可知:RecyclerView整个下拉更新的动画从时序上可以分为下面三个部分:

animToHeader (更新数据) -> animToTip (展示tips动画) -> animToStart (回顶)

因此,我们要在TRecyclerView的onInterceptTouchEvent、onTouchEvent方法做一些事情:

1)onInterceptTouchEvent:判断是否拦截MotionEvent事件,事件交给TRecyclerView或者RecyclerView处理。

2)onTouchEvent:处理RecyclerView的下拉动画,RecyclerView下拉是否触发更新的逻辑。

下面还是看看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已经处在顶部,不能再向下滚动时,这个时候,事件交由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处理,让其可以继续上滑。
                       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,松手则触发刷新逻辑,反之,触发RecyclerView的回顶动画。

2)触发刷新的逻辑是在animToHeader动画结束之后做的,onAnimationEnd回调里面调用了 mPullRefresh.pullRefresh(),业务逻辑可以通过该接口处理数据请求的逻辑。

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处理,让其可以继续上滑。

下拉刷新讲的差不多了,我们来看看上滑加载的实现。

3. TRecyclerView构成

下面会结合这TRecyclerView的结构、TRecyclerAdapter的实现来讲讲TRecyclerView上滑加载数据的原理。

TRecyclerView 的结构:
TRecycleView是一个FrameLayout主要包括两部分,Header View和RecycleView,而RecycleView的View类型大体分为两部分:Normal View和Footer View。

TRecyclerView中有一个TRecyclerAdapter,是用来加载RecyclerView的Item View,是TRecyclerView中真正加载数据的Adapter,其中包括两大类的数据类型,即正常的Normal View和Header View,Normal View是通过RecyclerView.Adapter来加载,就是我们需要写的Adapter。

TRecyclerView的结构图

TRecyclerView的初始化
下面结合TRecyclerView的结构图,我们看看具体的代码实现,首先是TRecycleView的构造方法:

 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的实现

我们知道TRecyclerView中真正加载数据的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注册了一个观察者,调用RecyclerView.Adapter的数据更新方法时,会通知TRecyclerAdapter去更新数据数据,代码如下:

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方法的实现。

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类型,则直接返回,否则调用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,但是真正加载数据的Adapter是TRecyclerAdapter。

4. TRecyclerView上滑加载数据

看上面的结构图,我们知道Footer View并不是直接作为TRecyclerView的一个View,而是RecyclerView的一个Item View。
因此,当RecyclerView上滑到最后一个Item View,即Footer View可见时,我们可以通过 mPushRefresh.loadMore()来处理上滑加载数据的逻辑,代码的实现如下:

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
              //是否已经滑动到底部,如果是则执行loadMore方法回调
                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
是否可见,可见则触发加载更多的回调。

5. 总结

在写TRecyclerView遇到TRecyclerView中的RecyclerView没有滚动条,这是因为我们是直接new RecyclerView,RecyclerView的一些初始化方法没有执行到,如受保护的initializeScrollbars 方法,在外部无法调用到的。

解法方法:RecyclerView通过inflate的方式去加载一个xml文件。

工程用到的其它文件NewsRecyclerAdapterLoadingView等等,大家可以去github地址下载,下面附有项目地址。

TRecyclerView项目地址TRecyclerView

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容