美文网首页Android开发Android技术知识Android开发
TRecyclerView-仿头条、搜狐新闻,实现Recycle

TRecyclerView-仿头条、搜狐新闻,实现Recycle

作者: 湘北南 | 来源:发表于2018-11-09 17:05 被阅读61次

    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,w hen 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);
            }
    
        }
    
    

    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();
                          }
                        }
                    }
    
                }
    
                @Override
                public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                    super.onScrolled(recyclerView, dx, dy);
                }
            });
        }
    
    //滑动到底部,且最后一个元素可见,则认为到达底部
    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;
        }
    
    

    我们自个写了一个NewsRecyclerAdapter,通过TRecyclerView实行了数据的下拉更新,上滑加载的逻辑,下面给出NewsRecyclerAdapter的源码:

    
    public class NewsRecyclerAdapter extends BaseRecyclerAdapter<NewsItem> {
    
        private Context mCtx;
    
        private static final int NEWS_ITEM_TYPE_PIC = 1;
        private static final int NEWS_ITEM_TYPE_NORMAL = 2;
    
    
        public NewsRecyclerAdapter(Context context) {
            super(context);
            init(context);
        }
    
        private void init(Context ctx) {
            mCtx = ctx;
        }
    
    
        @Override
        protected BaseViewHolder createHolder(ViewGroup parent, int viewType, Context context) {
            return buildHolder(parent, viewType, context);
        }
    
        @Override
        protected void bindData(BaseViewHolder holder, int position) {
            initData(holder, position);
        }
    
        private BaseViewHolder buildHolder(ViewGroup parent, int viewType, Context context) {
            BaseViewHolder holder = null;
            switch (viewType) {
                case NEWS_ITEM_TYPE_PIC:
                    View itemView = LayoutInflater.from(context).inflate(
                            R.layout.item_pic_layout, parent, false);
                    holder = new BaseViewHolder(itemView);
                    break;
                case NEWS_ITEM_TYPE_NORMAL:
                    View normalItemView = LayoutInflater.from(context).inflate(
                            R.layout.item_normal_layout, parent, false);
                    holder = new BaseViewHolder(normalItemView);
                    break;
                default:
                    break;
            }
            return holder;
        }
    
        private void initData(BaseViewHolder holder, final int position) {
            final int type = getItemViewType(position);
            NewsItem item = getItem(position);
            switch (type) {
                case NEWS_ITEM_TYPE_PIC:
                    if (item != null) {
                        ((TextView) holder.getView(R.id.title)).setText(item.mTitle);
                        ((TextView) holder.getView(R.id.content)).setText(item.mContent);
                        ((ImageView) holder.getView(R.id.img)).setImageResource(item.mResId);
                    }
                    break;
                case NEWS_ITEM_TYPE_NORMAL:
                    if (item != null) {
                        ((TextView) holder.getView(R.id.title)).setText(item.mTitle);
                        ((TextView) holder.getView(R.id.content)).setText(item.mContent);
                    }
                    break;
                default:
                    break;
    
            }
        }
    
    
        @Override
        public int getItemViewType(int position) {
            int itemType = position % 2;
            if(itemType == 0){
                return NEWS_ITEM_TYPE_NORMAL;
            }else{
                return NEWS_ITEM_TYPE_PIC;
            }
        }
    
    
    }
    
    

    5. 总结

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

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

    TRecyclerView项目地址TRecyclerView

    相关文章

      网友评论

      • 湘北南:欢迎大家指正,以此共勉 😁。

      本文标题:TRecyclerView-仿头条、搜狐新闻,实现Recycle

      本文链接:https://www.haomeiwen.com/subject/dfjwxqtx.html