二话不说,先上个效果图
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。