Head联动RecyclerView

二话不说,先上个效果图

image1.gif

demo已传到了GitHub : https://github.com/MrWangChong/HeadRecyclerView ,如果懒得复制 也可以直接引用过来
传送门:HeadRecyclerView

思路是根据掌阅大神黄老师分享的思路来做的:“ViewPager是整个屏幕大小,里面的RecyleView也是整个屏幕大小,每个RecyleView都有一个head大小的全透明headView,ViewPager的底部有个正真的headView。当RecyleView滑动的时候在ScrollChange中移动正真的headView。当点击事件点中RecyleView的透明head区域时,把该事件发送给底部正真的head”

看似简单的一句话,做起来实际花了我很长的时间

从简到繁,先从实现单个的RecyclerView与Head的联动开始

首先需要一个布局,FrameLayout,把正真的Head放在最下面,上面贴一个RecyclerView

"移动正真的headView"我使用的是ViewCompat.offsetTopAndBottom,但是我在试的时候,不知道为什么锁屏再开锁之后会触发onLayout,它的位置就被还原了,于是我把FrameLayout的onLayout做了一点调整

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();
                int childTop = getPaddingTop() + lp.topMargin;
                //加上这句话就能解决ViewCompat.offsetTopAndBottom之后锁屏开屏后View位置被还原的问题
                if (child.getTop() != childTop) {
                    childTop = child.getTop();
                }
                int childLeft = getPaddingLeft() + lp.leftMargin;
                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }

看FrameLayout的源码得知,它计算top位置 是使用的
childTop = parentTop + lp.topMargin;
而当child做了offsetTopAndBottom之后 它的getTop的位置是发生了变化的,所以只需要在onLayout里面把getTop的位置传到layout中就行了

然后稍微复杂点的,就是处理RecyclerView的滚动事件那些了
  • 首先是自动设置padding,同时计算整个HeadView的高度,需要滚动的View高度,需要固定的View的高度

其实最开始我是在布局里面设置的paddingTop,但是这样总觉得不是很智能,于是就弄成了自动设置paddingTop了。至于为什么需要设置paddingTop嘛,当初我也是脑袋没转过弯来,问了问大神,当RecyclerView往上滑的时候,是怎么做到的让它的item不把固定到顶部的那个View挡住,结果就是设置一个paddingTop。

   @Override
    protected void onMeasure(int widthSpec, int heightSpec) {
        super.onMeasure(widthSpec, heightSpec);
        getHeadInfo();
    }
    
      //获取Head信息
    private void getHeadInfo() {
        if (mHeadView == null) {
            return;
        }
        if (mHeadViewHeight == 0) {
            mHeadViewHeight = mHeadView.getMeasuredHeight();
//            Log.v(TAG, "mHeadViewHeight=" + mHeadViewHeight);
        }
        if (mSlideViewHeight == 0 || mFixedViewHeight == 0) {
            if (mHeadView instanceof HeadLayout) {
                HeadLayout head = (HeadLayout) mHeadView;
                if (head.getSlideView() != null) {
                    mSlideViewHeight = head.getSlideView().getMeasuredHeight();
                }
                if (head.getFixedView() != null) {
//                    bringChildToFront(head.getFixedView());
                    mFixedViewHeight = head.getFixedView().getMeasuredHeight();
                    //强行把PaddingTop改成FixedViewHeight
                    setPadding(getPaddingLeft(), mFixedViewHeight, getPaddingRight(), getPaddingBottom());
//                    Log.v(TAG, "setPaddingTop=" + mFixedViewHeight);
                }
//                Log.v(TAG, "mSlideViewHeight=" + mSlideViewHeight + "\tmFixedViewHeight=" + mFixedViewHeight + "\tmHeadViewHeight=" + mHeadViewHeight);
            } else if (mHeadView instanceof ViewGroup) {
                ViewGroup group = (ViewGroup) mHeadView;
                if (group.getChildCount() > 0) {
                    mSlideViewHeight = group.getChildAt(0).getMeasuredHeight();
                }
                if (group.getChildCount() > 1) {
                    mFixedViewHeight = group.getChildAt(1).getMeasuredHeight();
                    //强行把PaddingTop改成FixedViewHeight
                    setPadding(getPaddingLeft(), mFixedViewHeight, getPaddingRight(), getPaddingBottom());
//                    Log.v(TAG, "setPaddingTop=" + mFixedViewHeight);
                }
//                Log.v(TAG, "mSlideViewHeight=" + mSlideViewHeight + "\tmFixedViewHeight=" + mFixedViewHeight + "\tmHeadViewHeight=" + mHeadViewHeight);
            } else {
                mSlideViewHeight = mHeadView.getMeasuredHeight();
            }
        }
    }

当然 真正是HeadView是需要手动设置进来的

    /**
     * 设置真正的HeadView
     */
    public void setHeadView(View v) {
        mHeadView = v;
        //把HeadView重置到最上层布局
        //mHeadView.bringToFront();
    }

bringToFront可以把View置于布局最顶层,当时为了让item滑上来不挡住固定的View,但是那样做却达不到想要的效果。

从上面的代码可以看出来,我是取的ViewGroup的第一个和第二个出来作为跟着RecyclerView一起滑动的View以及固定在顶部不动的View

当然推荐使用HeadLayout,这是我为了使用方便而封装的一个ViewGroup,只能装两个View或者ViewGroup,有兴趣可以在demo里面看看,这里就不过多累述

  • 然后是修改设置的适配器

最开始是在写适配器的时候利用getItemViewType添加的一个固定高度的透明HeadView,后来觉得不方便,于是修改了setAdapter的方法,让它能更加智能一点,同时如果数据不满一页的话,需要一个FooterView,这样方便管理

    @Override
    public void setAdapter(Adapter adapter) {
        super.setAdapter(new SimpleAdapter(adapter));
//        super.setAdapter(adapter);
    }

SimpleAdapter是封装到RecyclerView内部的一个内部类,在SimpleAdapter中 主要是添加一个HeadView和一个FooterView
重写onAttachedToRecyclerView是为了支持GridLayoutManager
,暂不支持StaggeredGridLayoutManager

 @Override
        public void onAttachedToRecyclerView(RecyclerView recyclerView) {
            super.onAttachedToRecyclerView(recyclerView);
            LayoutManager manager = recyclerView.getLayoutManager();
            if (manager instanceof GridLayoutManager) {
                final GridLayoutManager gridManager = (GridLayoutManager) manager;
                gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
                    public int getSpanSize(int position) {
                        return position != 0 && position <= adapter.getItemCount() ? 1 : gridManager.getSpanCount();
                    }
                });
            }

        }

然后需要注意的是,自己写SimpleAdapter必须重写unregisterAdapterDataObserver和registerAdapterDataObserver才能把adapter的刷新交给SimpleAdapter

  @Override
    public void unregisterAdapterDataObserver(AdapterDataObserver observer) {
    //            super.unregisterAdapterDataObserver(observer);
        if (this.adapter != null) {
            this.adapter.unregisterAdapterDataObserver(observer);
        }
    }
    
    @Override
    public void registerAdapterDataObserver(AdapterDataObserver observer) {
    //            super.registerAdapterDataObserver(observer);
        if (this.adapter != null) {
            this.adapter.registerAdapterDataObserver(observer);
        }
    }
  • 接下来就是处理onScrolled了

  @Override
    public void onScrolled(int dx, int dy) {
        super.onScrolled(dx, dy);
        mScrollY += dy;
        //顺便加上了一个加载更多的监听
        if (mOnLoadMoreListener != null && !isLaodMore) {
            getThisLayoutManager();
            if (mLayout != null) {
                if (dy > 0 && getAdapter() != null) {
//                    Log.d(TAG, "mLayout.findLastVisibleItemPosition()=" + mLayout.findLastVisibleItemPosition() + "getAdapter().getItemCount()=" + getAdapter().getItemCount());
                    if (getAdapter() instanceof SimpleAdapter) {
                        if (mLayout.findLastVisibleItemPosition() == getAdapter().getItemCount() - 2) {
                            Log.d(TAG, "HeadRecyclerView trigger onLoadMore");
                            mOnLoadMoreListener.onLoadMore(this);
                            isLaodMore = true;
                        }
                    } else {
                        if (mLayout.findLastVisibleItemPosition() == getAdapter().getItemCount() - 1) {
                            Log.d(TAG, "HeadRecyclerView trigger onLoadMore");
                            mOnLoadMoreListener.onLoadMore(this);
                            isLaodMore = true;
                        }
                    }
                }
            }
        }

        //设置头部View
        if (mTopView == null) {
            getTopView();
        }
        if (mTopView == null || mHeadView == null) {
            return;
        }
        if (mTopViewHeight == 0) {
            mTopViewHeight = mTopView.getMeasuredHeight();
        }
        getHeadInfo();
        int remainY = mHeadViewHeight - mScrollY;//剩余Y
        int headBottom = mHeadView.getBottom();//HeadView底部
//        Log.v(TAG, "mScrollY=" + mScrollY + "\tremainY=" + remainY + "\theadBottom=" + headBottom);
        if (remainY > mFixedViewHeight) {
            int offset = remainY - headBottom;
            ViewCompat.offsetTopAndBottom(mHeadView, offset);
            //滑动了HeadView需要通知
//            if (mOnHeadViewChangeListener != null) {
//                mOnHeadViewChangeListener.offsetTopAndBottom(this, offset);
//            }
//            Log.v(TAG, "mScrollY=" + mScrollY + "\tremainY=" + remainY + "\theadBottom=" + headBottom + "\toffset=" + offset);
        } else {
            if (remainY != mFixedViewHeight) {
                int offset = mFixedViewHeight - headBottom;
                ViewCompat.offsetTopAndBottom(mHeadView, offset);
                //滑动了HeadView需要通知
//                if (mOnHeadViewChangeListener != null) {
//                    mOnHeadViewChangeListener.offsetTopAndBottom(this, offset);
//                }
            }
        }

也就是这个方法,让我们的真正的HeadView能够跟着RecyclerView的滚动一起联动起来,上拉加载更多的代码倒是不用太在意,这是我顺带做的一件事

  • 事件分发
   //获取TopView
    private View getTopView() {
        if (mTopView == null) {
            getThisLayoutManager();
            if (mLayout != null && mLayout.getChildCount() > 0) {
                mTopView = getChildAt(0);
                //把TopView的事件分发给mHeadView
                mTopView.setOnTouchListener(new OnTouchListener() {
                    @Override
                    public boolean onTouch(View v, MotionEvent event) {
                        if (mHeadView != null) {
//                            MotionEvent ev = MotionEvent.obtain(event);
//                            ev.setLocation(event.getX(), event.getY() + getPaddingTop());
                            mHeadView.dispatchTouchEvent(event);
                            return true;
                        }
                        return false;
                    }
                });
            }
        }
        return mTopView;
    }

mTopView也就是 SimpleAdapter里面加的那个HeadView,当它被点击的时候,就把事件分发给mHeadView(真正的HeadView),

那么还有一个问题,就是当mTopView滑到头 不见了之后,就点不到了,所以这个时候就要把RecyclerView的事件分发出来了,不过有一点需要处理,由于HeadView是往上滑了一点距离的,所以这个时候在RecyclerView得到的Y的位置 应该加上mSlideViewHeight的位置才是真正的位置。

//当TopView滑不见之后的事件分发
    @Override
    public boolean dispatchTouchEvent(MotionEvent e) {
        //点击getPaddingTop内的区域
        if (e.getY() <= getPaddingTop()) {
            //如果滑动了的Y距离大于  mTopViewHeight - mFixedViewHeight,也就是mSlideViewHeight
//            if (mHeadView != null && mScrollY > mTopViewHeight - mFixedViewHeight) {
            if (mHeadView != null && mScrollY > mSlideViewHeight) {
                MotionEvent ev = MotionEvent.obtain(e);
                ev.setLocation(e.getX(), e.getY() + mSlideViewHeight);
//                Log.v(TAG, "ev.getY()=" + ev.getY());
                mHeadView.dispatchTouchEvent(ev);
            }
        }
        return super.dispatchTouchEvent(e);
    }

到此就基本上已经完成了一大半了,如果是不加ViewPager的话,这样是没有什么问题的,但是加上ViewPager之后的话,就会有一个 RecyclerView的数据 有没有满一屏的区别了,假如有的满一屏 有的 不满一屏,就会造成有的滑不动 或者 滑动出BUG等等问题

所以这里还有最后一步

  • 动态修改FooterView的高度

    /**
     * 动态设置满屏FooterView
     */
    public void setFullScreenFooter() {
        if (mFooterView == null) {
            mFooterView = new View(getContext());
        }
        if (mFooterView.getMeasuredHeight() != 0) {
            return;
        }
        getThisLayoutManager();
        if (mLayout != null && getAdapter() != null) {
            int spanCount = 1;
            if (mLayout instanceof GridLayoutManager) {
                spanCount = ((GridLayoutManager) mLayout).getSpanCount();
            }

            int itemCount = getAdapter().getItemCount();
            int centreHeight = 0;
            int count = mLayout.getChildCount();//这里是获取的当前显示的ChildCount
            //计算所有item的高度
            int childHeight = 0;
            for (int i = 0; i < count; i++) {
                if (i == 0) {
                    childHeight = mLayout.getChildAt(i).getMeasuredHeight();
                } else {
                    if ((i - 1) % spanCount == 0) {
                        int itemHeight = mLayout.getChildAt(i).getMeasuredHeight();
                        childHeight += itemHeight;
                    }
                }
                if (i == count / 2) {
                    centreHeight = mLayout.getChildAt(i).getMeasuredHeight();
                }
            }
            int height = getMeasuredHeight();
            // Log.v(TAG, getTag() + "\tchildHeight=" + childHeight + "\theight=" + height + "\tcentreHeight=" + centreHeight + "\tmHeadViewHeight=" + mHeadViewHeight);
            int difference = height - childHeight;
            if (difference > 0) {//不满屏幕
                int footerHeight = height + mHeadViewHeight - childHeight - mFixedViewHeight + 5;
//                Log.v(TAG, getTag() + "\tfooterHeight=" + footerHeight);
                setFooterViewHeight(footerHeight);
                //这句代码是为了防止 直接点击后面3个以上的tab的时候 scrollBy执行太快而没有绘制过来的问题
                postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        scrollBy(0, getHeadScrollY() - mScrollY);
                    }
                }, 10);
            } else {
                int invisibleItem = itemCount - count - 1;//没有显示出来的item,再减去一个footer
                if (invisibleItem > 0) {
                    int invisibleHeight = invisibleItem * centreHeight / spanCount;
                    childHeight += invisibleHeight;
                    difference = height + mHeadViewHeight - childHeight;
                    if (difference > 0) {
                        int footerHeight = difference - mFixedViewHeight + 5;
//                        Log.v(TAG, getTag() + "\tfooterHeight=" + footerHeight);
                        setFooterViewHeight(footerHeight);
                    }
                }
            }
        }
    }

这个计算方法 也是我经过多种尝试算出来的,为了避免重复设置,加了mFooterView的高度为0才设置的条件, mLayout.getChildCount()只能获取到 显示的View的个数,实际个数是getAdapter().getItemCount(),那么就有一部分没有显示出来,所以这里需要把没有显示出来的View高度也算一下,我取了一个中间的item高度centreHeight来作为未显示View高度的计算,得到的最终footerHeight值在后面+5,是我体验出来的,不知道为什么不外加一点距离,会滑动不到头

/**
     * 设置FooterView高度
     */
    public void setFooterViewHeight(int height) {
        if (height == 0) return;

        if (mFooterView == null) {
            mFooterView = new View(getContext());
            mFooterView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, height));
        } else {
            ViewGroup.LayoutParams lp = mFooterView.getLayoutParams();
            if (lp == null) {
                lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, height);
                mFooterView.setLayoutParams(lp);
            } else {
                if (lp.height != height) {
                    lp.height = height;
                    mFooterView.setLayoutParams(lp);
                }
            }
        }
    }

然后就是ViewPager里面装RecyclerView的联动了

其实主要的事,都在RecyclerView里面做了,所以这里只需要稍微处理一下ViewPager就可以了

  • 一,就是翻页的时候修改RecyclerView的滚动位置
  @Override
    protected void onPageScrolled(int position, float offset, int offsetPixels) {
        super.onPageScrolled(position, offset, offsetPixels);
        setHeadRecyclerView(getHeadRecyclerView(getChildAt(position)));
        if (position + 1 < getChildCount()) {
            setHeadRecyclerView(getHeadRecyclerView(getChildAt(position + 1)));
        }
    }

    private void setHeadRecyclerView(HeadRecyclerView headRecyclerView) {
        if (headRecyclerView == null) {
            return;
        }
        headRecyclerView.setFullScreenFooter();
        int headScrollY = headRecyclerView.getHeadScrollY();
        int scrolledY = headRecyclerView.getScrolledY();
        if (scrolledY < headScrollY) {
//            Log.v(TAG, headRecyclerView.getTag() + "\theadScrollY=" + headScrollY + "\tscrolledY=" + scrolledY);
            headRecyclerView.scrollBy(0, headScrollY - scrolledY);
        } else if (scrolledY > headScrollY) {
            int slideViewHeight = headRecyclerView.getSlideViewHeight();
            if (scrolledY > slideViewHeight) {
                if (!headRecyclerView.isTop()) {
                    headRecyclerView.scrollBy(0, slideViewHeight - scrolledY);
                }
//                Log.v(TAG, "headScrollY=" + headScrollY + "\tscrolledY=" + scrolledY + "\tslideViewHeight" + slideViewHeight);
            } else {
                headRecyclerView.scrollBy(0, headScrollY - scrolledY);
//                Log.v(TAG, "headScrollY=" + headScrollY + "\tscrolledY=" + scrolledY + "\tslideViewHeight" + slideViewHeight);
            }
        }
    }

    private HeadRecyclerView getHeadRecyclerView(View v) {
        if (v instanceof HeadRecyclerView) {
//            Log.v(TAG, "v instanceof HeadRecyclerView");
            return (HeadRecyclerView) v;
        } else if (v instanceof ViewGroup) {
            ViewGroup group = (ViewGroup) v;
            for (int i = 0; i < group.getChildCount(); i++) {
                HeadRecyclerView headRecyclerView = getHeadRecyclerView(group.getChildAt(i));
                if (headRecyclerView != null)
                    return headRecyclerView;
            }
        }
        return null;
    }

虽然我自己不是使用ViewPager装Fragment里面再装RecyclerView,但是我这里getHeadRecyclerView是递归查找的,所以应该是支持这种做法的。

  • 二,就是分发横向滑动事件给HeadView
 /**
     * 设置真正的HeadView
     */
    public void setHeadView(View v) {
        mHeadView = v;
        //把HeadView重置到最上层布局
        //mHeadView.bringToFront();
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                isDispatchToHeadView = false;
                isFixedViewRegion = false;
                isDispatched = false;
                if (mHeadView != null) {
                    scrollY = ev.getY();
                    if (mFixedViewHeight == 0) {
                        if (mHeadView instanceof HeadLayout) {
                            HeadLayout head = (HeadLayout) mHeadView;
                            if (head.getFixedView() != null) {
                                bringChildToFront(head.getFixedView());
                                mFixedViewHeight = head.getFixedView().getMeasuredHeight();
                            }
                        } else if (mHeadView instanceof ViewGroup) {
                            ViewGroup group = (ViewGroup) mHeadView;
                            if (group.getChildCount() > 1) {
                                mFixedViewHeight = group.getChildAt(1).getMeasuredHeight();
                            }
                        }
                    }
                    int bottom = mHeadView.getBottom();
                    if (scrollY <= bottom && scrollY > bottom - mFixedViewHeight) {
                        isFixedViewRegion = true;
                        scrollX = ev.getX();
                    }
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (isFixedViewRegion && !isDispatched && !isDispatchToHeadView) {
                    float y = ev.getY();
                    if (Math.abs(scrollY - y) > mTouchSlop) {
                        isDispatchToHeadView = false;
                        isDispatched = true;
                        break;
                    }
                    float x = ev.getX();
                    if (Math.abs(scrollX - x) > mTouchSlop) {
                        isDispatchToHeadView = true;
                        isDispatched = true;
                    }
                }
//                if (!isDispatchToHeadView) {
//                    int x = (int) ev.getX();
//                    Log.v(TAG, "x=" + x + "\tscrollX=" + scrollX);
//                    if (Math.abs(scrollX - x) < 0) {
//                        isDispatchToHeadView = true;
//                    }
                break;
        }
        if (isDispatchToHeadView) {
            return mHeadView.dispatchTouchEvent(ev);
        }
        return super.dispatchTouchEvent(ev);
    }

如果HeadView没有横滑事件的话,就不需要setHeadView,也就不会再有事件分发机制。mTouchSlop是系统的一个滑动触发最短距离
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();

使用方法

使用方法比较简单了,因为大部分逻辑都已经在控件中处理了,可以参考我传到GitHub上的使用方法

觉得还行的话就顺便给个star吧,第一次写文章,希望大神勿喷,欢迎大家提问和提BUG。

你可能感兴趣的:(Head联动RecyclerView)