欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Head联动RecyclerView

程序员文章站 2022-04-28 09:34:43
...

二话不说,先上个效果图

Head联动RecyclerView

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 : http://www.jianshu.com/p/6e92406334e7,转载请注明出处