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上的使用方法
【Head联动RecyclerView】觉得还行的话就顺便给个star吧,第一次写文章,希望大神勿喷,欢迎大家提问和提BUG。

    推荐阅读