一、前言
app 首页中经常要实现首页头卡共享,tab 吸顶,内容区通过 ViewPager 切换的需求,以前往往是利用事件处理来完成,还有 Google 官方也提供了相关的库如CoordinatorLayout,但是这些也有一定的弊端和滑动方面不如意的地方,瑕疵比较明显,实际上很多大厂的吸顶效果都是自己写的,同样适配起来还是比较复杂。
这里我们利用 NestedScrolling 机制来实现,之前的博客中也有一篇类似的博客《Android NestedScrolling 机制为 RecyclerView 添加 Header》,这个版本实际上在之前的版本上进行了一些扩展,使其能支持 ViewPager。
当然也有很多开源项目,发现存在的问题很多面,主要问题如下:
- 头部和内容区域不联动
- 没有中断 RecyclerView 的 fling 效果,导致 RecyclerView 抢占 ViewPager 事件
- 仅仅只支持RecyclerView,不支持扩展
- 侵入式设计太多,反射太多。(当然,本篇方案解决 RecyclerView 中断 fling 时用了侵入式设计)
- 严重依赖Adapter、ViewHolder等。
二、效果展示
其实这个页面中存在以下布局元素:
Head 部分是大卡片和TabLayout
Body部分使用ViewPager,然后通过ViewPager"装载"两个RecyclerView。
三、实现逻辑
3.1 布局设计的注意事项
对于实现布局,评价一个布局的好坏应该从以下几方面出发
布局规划:提前规划好最终的效果和布局的组成,以及要处理最大一些问题,如果处理不好,则可能出现做到一半无法做下去的问题。
耦合程度:应该尽可能避免太多的耦合,比如View与View之间的直接调用,如果有,那么应该着手从设计原则着手或者父子关系方面改良设计。
减少XML组合布局:很多自定义布局中Inflate xml布局,虽然这种也属于自定义View,但是封装在xml中的View很难让你去修改属性和样式,设置要做大量的自定义属性去适配。
通用性和可扩展性:通用性是此View要做到随处可用,即便不能也要在这个方向进行扩展,可扩展性的提高可以促进通用性。为了实现布局效果,一些开发者不仅仅自定义了父布局,而且还定义了各种子布局,这显然降低了扩展性和适用性。原则上,两者同时定义的问题应该在父布局中去处理,而不是从子View中去处理。
完成好于完美:对于性能和瑕疵问题,避免提前处理,除非阻碍开发。遵循"完成好于完美"的原则,先实现再完善,不断循环优化才是正确的方式。很多人自定义的时候担心性能和瑕疵问题,导致无法设计出最终效果,实际上很多自定义布局的瑕疵和性能都是在完成之后优化效果的,因此过多的提前布置,可能会让你做大量返工处理。
下面是本篇设计过程,希望对你有帮助
3.2 主要逻辑
3.2.1 规划布局
规划布局是非常重要的,这里我们规划布局为
HEAD部分和BODY两部分,至于吸顶的TabLayout,我们放到Head部分,让吸顶时让Head部分top 最大移动为HEAD高度减去TabLayout的高度。BODY部分可以使用ViewPager,也可以是其他布局,因为ViewPager使用较广,本文使用ViewPager。
xml
<Head>
<Card></Card>
<TabLayout></TabLayout>
</Head>
<Body>
<RecyclerView1/>
....
<RecyclerViewN/>
</Body>
3.2.2 Scrolling 机制
其实在本篇之前,我们也通过Scrolling机制定义过,但要明白为什么要使用Scrolling机制?
Scrolling机制可以协同父子View、祖宗View的滑动,当然这个范围有点小。本篇我们要协同滑动,中间隔着ViewPager,人家可是爷孙关系。
Scrolling提供了祖宗树上可以互相通知的View
通用性强:Scrolling是通过support或者androidx库接入的,虽然当前发展到第三个版本了,但是毫不影响我们升级使用。
3.2.3 主要代码
继承Scrolling接口
java
public class NestedPagerRecyclerViewLayout extends FrameLayout implements NestedScrollingParent2 {
private final int mFlingVelocity; //fling 纵向速度计算
private int mHeadExpandedOffset; // tab偏移,也就是为了方便tab吸顶
private float startEventX = 0;
private float startEventY = 0;
private float mSlopTouchScale = 0; //互动判断阈值
private boolean isTouchMoving = false;
private View mHeaderView = null; //抽象调用head
private View mBodyView = null; // 抽象调用body
private View mVerticalScrollView = null;
private VelocityTracker mVelocityTracker; //顺时力度跟踪
//辅助当前布局滑动类型判断,如水平滑动还是垂直滑动以及是不是手指触动的滑动,实现主要是为了兼容外部调用
///参考NestedScrollView实现的
private NestedScrollingParentHelper parentHelper = new NestedScrollingParentHelper(this);
.....
}
自定义布局参数,主要是为子View添加布局属性
java
public static class LayoutParams extends FrameLayout.LayoutParams {
public final static int TYPE_HEAD = 0;
public final static int TYPE_BODY = 1;
private int childLayoutType = TYPE_HEAD;
public LayoutParams(@NonNull Context c, @Nullable AttributeSet attrs) {
super(c, attrs);
if (attrs == null) return;
final TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.NestedPagerRecyclerViewLayout);
childLayoutType = a.getInt(R.styleable.NestedPagerRecyclerViewLayout_layoutScrollNestedType, 0);
a.recycle();
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(@NonNull ViewGroup.LayoutParams source) {
super(source);
}
public LayoutParams(@NonNull MarginLayoutParams source) {
super(source);
}
}
测量
我们这里纵向排列即可
java
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int childCount = getChildCount();
int height = MeasureSpec.getSize(heightMeasureSpec);
int overScrollExtent = overScrollExtent();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp.childLayoutType == LayoutParams.TYPE_BODY) {
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin
+ 0, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin
+ 0, height - overScrollExtent);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
核心方法,纵向滑动处理
java
private void handleVerticalNestedScroll(int dx, int dy, @Nullable int[] consumed) {
if (dy == 0) {
return;
}
if (!canNestedScrollView(mVerticalScrollView)) {
//这里要判断向上滑动问题,
// 如果当前布局可以向上滑动,优先滑动,不然头部可能出现露一半但无法向上滑动的问题
if (dy < 0) {
return;
}
if (!allowScroll(dy)) {
return;
}
}
int maxOffset = computeVerticalScrollRange() - computeVerticalScrollExtent();
int scrollOffset = computeVerticalScrollOffset();
int dyOffset = dy;
int targetOffset = scrollOffset + dy;
if (targetOffset >= maxOffset) {
dyOffset = maxOffset - scrollOffset;
}
if (targetOffset <= 0) {
dyOffset = 0 - scrollOffset;
}
if (!canScrollVertically(dyOffset)) {
return;
}
consumed[1] = dyOffset;
Log.d("onNestedScroll", "::::" + dyOffset + "+" + scrollOffset + "=" + (scrollOffset + dyOffset));
scrollBy(0, dyOffset);
}
核心事件处理,主要处理滑动,瞬时速度问题
java
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int scrollRange = computeVerticalScrollRange();
if (scrollRange <= getHeight()) {
return super.dispatchTouchEvent(event);
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mVelocityTracker.addMovement(event);
startEventX = event.getX();
startEventY = event.getY();
isTouchMoving = false;
if (mVerticalScrollView instanceof RecyclerView) {
/**
*RecyclerView 虽然继承了NestedScrollingChild,但是没有在stopNestedScroll中停止
*调用stopScroll,导致滑动状态事件自动捕获,造成ViewPager切换问题,这里使用stopScroll()侵入式调用
*/
((RecyclerView) mVerticalScrollView).stopScroll();
} else if (mVerticalScrollView instanceof NestedScrollingChild) {
mVerticalScrollView.stopNestedScroll();
}
break;
case MotionEvent.ACTION_MOVE:
float currentX = event.getX();
float currentY = event.getY();
float dx = currentX - startEventX;
float dy = currentY - startEventY;
if (!isTouchMoving && Math.abs(dy) < Math.abs(dx)) {
startEventX = currentX;
startEventY = currentY;
break;
}
View touchView = null;
int offset = (int) -dy;
if (!isTouchMoving && Math.abs(dy) >= mSlopTouchScale) {
touchView = findTouchView(currentX, currentY);
//这里只关注头卡触摸事件即可
isTouchMoving = touchView != null && touchView == getHeaderView();
}
if (isTouchMoving && !allowScroll(offset)) {
isTouchMoving = false;
}
startEventX = currentX;
startEventY = currentY;
if (!isTouchMoving) {
break;
}
mVelocityTracker.addMovement(event);
int maxOffset = computeVerticalScrollRange() - computeVerticalScrollExtent();
int scrollOffset = computeVerticalScrollOffset();
int targetOffset = scrollOffset + offset;
if (targetOffset >= maxOffset) {
offset = maxOffset - scrollOffset;
}
if (targetOffset <= 0) {
offset = 0 - scrollOffset;
}
if (offset != 0) {
scrollBy(0, offset);
}
Log.d("onNestedScroll", ">:>:>" + offset + "+" + scrollOffset + "=" + (scrollOffset + offset));
super.dispatchTouchEvent(event);
return true;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_OUTSIDE:
mVelocityTracker.addMovement(event);
if (isTouchMoving) {
isTouchMoving = false;
mVelocityTracker.computeCurrentVelocity(1000, mFlingVelocity);
startFling(mVelocityTracker, (int) event.getX(), (int) event.getY());
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
}
return super.dispatchTouchEvent(event);
}
四、代码实现
4.1 要点
头部不联动问题:
我们需要处理在 dispatchTouchEvent 或者利用 onInteceptTouchEvent + onTouchEvent 处理,主要处理 VelocityTracker + fling 事件。接着我们判断滑动开始位置是不是在头部,因为按照布局设计,头部和RecyclerView不一样,头部是随着整体滑动,而RecyclerView是可以内部滑动的,直到无法滑动时,我们才能让父布局整体滑动,通过这种方式就能解决联动问题。
RecyclerView 中断 fling 效果问题:RecyclerView 没有在 stopNestedScroll () 方法中中断滑动,因此需要通过侵入方式,调用 stopScroll () 去完成,其实我们这里希望官方提供接口终止RecyclerView停止滑动,但是事实上没有,这个问题一定概率上造成RecyclerView减速滑动时,ViewPager也无法切换,当然很多其他开源方案都有类似的问题。
java
if (mVerticalScrollView instanceof RecyclerView) {
/**
* RecyclerView 虽然继承了NestedScrollingChild,但是没有在stopNestedScroll中停止
* 调用stopScroll,导致滑动状态事件自动捕获,造成ViewPager切换问题,这里使用stopScroll()侵入式调用
*/
((RecyclerView) mVerticalScrollView).stopScroll();
}
查找事件点所在的View,这里我们使用了下面方法,理论上我们不会子Head和Body部分做Matrix变换,因此Android内部通过矩阵判断View的逆矩阵方式我们可以不用。
java
private View findTouchView(float currentX, float currentY) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
float childX = (child.getX() - getScrollX());
float childY = (child.getY() - getScrollY());
if (currentX < childX || currentX > (childX + child.getWidth())) {
continue;
}
if (currentY < childY || currentY > (childY + child.getHeight())) {
continue;
}
return child;
}
return null;
}
捕获Scrolling Child,下面方法是捕获来自Child的滑动请求,如果没有达到吸顶状态,应该优先滑动父View
java
@Override
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
if (axes == SCROLL_AXIS_VERTICAL) {
//只关注垂直方向的移动
int maxOffset = computeVerticalScrollRange() - computeVerticalScrollExtent();
int offset = computeVerticalScrollOffset();
if (offset <= maxOffset) {
mVerticalScrollView = target;
return true;
}
} else {
mVerticalScrollView = null;
}
return false;
}
4.2 主要代码
java
public class NestedPagerRecyclerViewLayout extends FrameLayout implements NestedScrollingParent2 {
private final int mFlingVelocity;
private int mHeadExpandedOffset;
private float startEventX = 0;
private float startEventY = 0;
private float mSlopTouchScale = 0;
private boolean isTouchMoving = false;
private View mHeaderView = null;
private View mBodyView = null;
private View mVerticalScrollView = null;
private VelocityTracker mVelocityTracker;
private NestedScrollingParentHelper parentHelper = new NestedScrollingParentHelper(this);
public NestedPagerRecyclerViewLayout(@NonNull Context context) {
this(context, null);
}
public NestedPagerRecyclerViewLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public NestedPagerRecyclerViewLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
if (attrs != null) {
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NestedPagerRecyclerViewLayout);
mHeadExpandedOffset = a.getDimensionPixelSize(R.styleable.NestedPagerRecyclerViewLayout_headExpandedOffset, 0);
a.recycle();
}
mSlopTouchScale = ViewConfiguration.get(context).getScaledTouchSlop();
mFlingVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();
setClickable(true);
}
/**
* 头部余留偏移
*
* @param headExpandedOffset
*/
public void setHeadExpandOffset(int headExpandedOffset) {
this.mHeadExpandedOffset = headExpandedOffset;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int childCount = getChildCount();
int height = MeasureSpec.getSize(heightMeasureSpec);
int overScrollExtent = overScrollExtent();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp.childLayoutType == LayoutParams.TYPE_BODY) {
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin
+ 0, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin
+ 0, height - overScrollExtent);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
public boolean canScrollVertically(int direction) {
final int offset = computeVerticalScrollOffset();
final int range = computeVerticalScrollRange() - computeVerticalScrollExtent();
if (range == 0) return false;
if (direction < 0) {
return offset > 0;
} else {
return offset < range;
}
}
@Override
protected int computeVerticalScrollRange() {
int childCount = getChildCount();
if (childCount == 0) return super.computeVerticalScrollRange();
int range = getPaddingBottom() + getPaddingTop();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
range += child.getHeight() + lp.bottomMargin + lp.topMargin;
}
if (range < getHeight()) {
return super.computeVerticalScrollRange();
}
return range;
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
mHeaderView = getChildView(LayoutParams.TYPE_HEAD);
mBodyView = getChildView(LayoutParams.TYPE_BODY);
int childLeft = getPaddingLeft();
int childTop = getPaddingTop();
if (mHeaderView != null) {
LayoutParams lp = (LayoutParams) mHeaderView.getLayoutParams();
mHeaderView.layout(childLeft + lp.leftMargin, childTop + lp.topMargin, childLeft + lp.leftMargin + mHeaderView.getMeasuredWidth(), childTop + lp.topMargin + mHeaderView.getMeasuredHeight());
childTop += mHeaderView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
}
if (mBodyView != null) {
LayoutParams lp = (LayoutParams) mBodyView.getLayoutParams();
mBodyView.layout(childLeft + lp.leftMargin, childTop + lp.topMargin, childLeft + lp.leftMargin + mBodyView.getMeasuredWidth(), childTop + lp.topMargin + mBodyView.getMeasuredHeight());
}
}
protected int overScrollExtent() {
return Math.max(mHeadExpandedOffset, 0);
}
private View getHeaderView() {
return mHeaderView;
}
private View getBodyView() {
return mBodyView;
}
private View findTouchView(float currentX, float currentY) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
float childX = (child.getX() - getScrollX());
float childY = (child.getY() - getScrollY());
if (currentX < childX || currentX > (childX + child.getWidth())) {
continue;
}
if (currentY < childY || currentY > (childY + child.getHeight())) {
continue;
}
return child;
}
return null;
}
private boolean hasHeader() {
int count = getChildCount();
for (int i = 0; i < count; i++) {
LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
if (lp.childLayoutType == LayoutParams.TYPE_HEAD) {
return true;
}
}
return false;
}
public View getChildView(int layoutType) {
int count = getChildCount();
for (int i = 0; i < count; i++) {
LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
if (lp.childLayoutType == layoutType) {
return getChildAt(i);
}
}
return null;
}
private boolean hasBody() {
int count = getChildCount();
for (int i = 0; i < count; i++) {
LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
if (lp.childLayoutType == LayoutParams.TYPE_BODY) {
return true;
}
}
return false;
}
@Override
public void addView(View child) {
assertLayoutType(child);
super.addView(child);
}
private void assertLayoutType(View child) {
ViewGroup.LayoutParams lp = child.getLayoutParams();
assertLayoutParams(lp);
}
private void assertLayoutParams(ViewGroup.LayoutParams lp) {
if (hasHeader() && hasBody()) {
throw new IllegalStateException("header and body has already existed");
}
if (hasHeader()) {
if (!(lp instanceof LayoutParams)) {
throw new IllegalStateException("header should keep only one");
}
if (((LayoutParams) lp).childLayoutType == LayoutParams.TYPE_HEAD) {
throw new IllegalStateException("header should keep only one");
}
}
if (hasBody()) {
if ((lp instanceof LayoutParams) && ((LayoutParams) lp).childLayoutType == LayoutParams.TYPE_BODY) {
throw new IllegalStateException("header should keep only one");
}
}
}
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
assertLayoutParams(params);
super.addView(child, index, params);
}
@Override
public void addView(View child, int index) {
assertLayoutType(child);
super.addView(child, index);
}
@Override
public void addView(View child, int width, int height) {
assertLayoutParams(new LinearLayout.LayoutParams(width, height));
super.addView(child, width, height);
}
@Override
public void onViewAdded(View child) {
super.onViewAdded(child);
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
@Override
protected FrameLayout.LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}
@Override
public FrameLayout.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
return new LayoutParams(lp);
}
@Override
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
if (axes == SCROLL_AXIS_VERTICAL) {
//只关注垂直方向的移动
int maxOffset = computeVerticalScrollRange() - computeVerticalScrollExtent();
int offset = computeVerticalScrollOffset();
if (offset <= maxOffset) {
mVerticalScrollView = target;
return true;
}
} else {
mVerticalScrollView = null;
}
return false;
}
@Override
protected int computeVerticalScrollExtent() {
int computeVerticalScrollExtent = super.computeVerticalScrollExtent();
return computeVerticalScrollExtent;
}
@Override
public int getNestedScrollAxes() {
return parentHelper.getNestedScrollAxes();
}
@Override
public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) {
parentHelper.onNestedScrollAccepted(child, target, axes, type);
}
@Override
public void onStopNestedScroll(@NonNull View target, int type) {
if (mVerticalScrollView == target) {
Log.d("onNestedScroll", "::::onStopNestedScroll vertical");
parentHelper.onStopNestedScroll(target, type);
}
}
@Override
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
Log.e("onNestedScroll", "::::onNestedScroll 11111");
}
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @Nullable int[] consumed, int type) {
int scrollRange = computeVerticalScrollRange();
if (scrollRange <= getHeight()) {
return;
}
if (target == null) return;
if (mVerticalScrollView != target) {
return;
}
Log.e("onNestedScroll", "::::onNestedPreScroll 00000");
handleVerticalNestedScroll(dx, dy, consumed);
}
private void handleVerticalNestedScroll(int dx, int dy, @Nullable int[] consumed) {
if (dy == 0) {
return;
}
if (!canNestedScrollView(mVerticalScrollView)) {
//这里要判断向上滑动问题,
// 如果当前布局可以向上滑动,优先滑动,不然头部可能出现露一半但无法向上滑动的问题
if (dy < 0) {
return;
}
if (!allowScroll(dy)) {
return;
}
}
int maxOffset = computeVerticalScrollRange() - computeVerticalScrollExtent();
int scrollOffset = computeVerticalScrollOffset();
int dyOffset = dy;
int targetOffset = scrollOffset + dy;
if (targetOffset >= maxOffset) {
dyOffset = maxOffset - scrollOffset;
}
if (targetOffset <= 0) {
dyOffset = 0 - scrollOffset;
}
if (!canScrollVertically(dyOffset)) {
return;
}
consumed[1] = dyOffset;
Log.d("onNestedScroll", "::::" + dyOffset + "+" + scrollOffset + "=" + (scrollOffset + dyOffset));
scrollBy(0, dyOffset);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int scrollRange = computeVerticalScrollRange();
if (scrollRange <= getHeight()) {
return super.dispatchTouchEvent(event);
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mVelocityTracker.addMovement(event);
startEventX = event.getX();
startEventY = event.getY();
isTouchMoving = false;
if (mVerticalScrollView instanceof RecyclerView) {
/**
*RecyclerView 虽然继承了NestedScrollingChild,但是没有在stopNestedScroll中停止
*调用stopScroll,导致滑动状态事件自动捕获,造成ViewPager切换问题,这里使用stopScroll()侵入式调用
*/
((RecyclerView) mVerticalScrollView).stopScroll();
} else if (mVerticalScrollView instanceof NestedScrollingChild) {
mVerticalScrollView.stopNestedScroll();
}
break;
case MotionEvent.ACTION_MOVE:
float currentX = event.getX();
float currentY = event.getY();
float dx = currentX - startEventX;
float dy = currentY - startEventY;
if (!isTouchMoving && Math.abs(dy) < Math.abs(dx)) {
startEventX = currentX;
startEventY = currentY;
break;
}
View touchView = null;
int offset = (int) -dy;
if (!isTouchMoving && Math.abs(dy) >= mSlopTouchScale) {
touchView = findTouchView(currentX, currentY);
//这里只关注头卡触摸事件即可
isTouchMoving = touchView != null && touchView == getHeaderView();
}
if (isTouchMoving && !allowScroll(offset)) {
isTouchMoving = false;
}
startEventX = currentX;
startEventY = currentY;
if (!isTouchMoving) {
break;
}
mVelocityTracker.addMovement(event);
int maxOffset = computeVerticalScrollRange() - computeVerticalScrollExtent();
int scrollOffset = computeVerticalScrollOffset();
int targetOffset = scrollOffset + offset;
if (targetOffset >= maxOffset) {
offset = maxOffset - scrollOffset;
}
if (targetOffset <= 0) {
offset = 0 - scrollOffset;
}
if (offset != 0) {
scrollBy(0, offset);
}
Log.d("onNestedScroll", ">:>:>" + offset + "+" + scrollOffset + "=" + (scrollOffset + offset));
super.dispatchTouchEvent(event);
return true;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_OUTSIDE:
mVelocityTracker.addMovement(event);
if (isTouchMoving) {
isTouchMoving = false;
mVelocityTracker.computeCurrentVelocity(1000, mFlingVelocity);
startFling(mVelocityTracker, (int) event.getX(), (int) event.getY());
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
}
return super.dispatchTouchEvent(event);
}
public boolean allowScroll(int dy) {
int maxOffset = computeVerticalScrollRange() - computeVerticalScrollExtent();
int scrollOffset = computeVerticalScrollOffset();
int dyOffset = dy;
int targetOffset = scrollOffset + dy;
if (targetOffset >= maxOffset) {
dyOffset = maxOffset - scrollOffset;
}
if (targetOffset <= 0) {
dyOffset = 0 - scrollOffset;
}
if (!canScrollVertically(dyOffset)) {
return false;
}
return true;
}
private void startFling(VelocityTracker velocityTracker, int x, int y) {
int xVolecity = (int) velocityTracker.getXVelocity();
int yVolecity = (int) velocityTracker.getYVelocity();
if (mVerticalScrollView instanceof NestedScrollingChild) {
Log.d("onNestedScroll", "onNestedScrollfling xVolecity=" + xVolecity + ", yVolecity=" + yVolecity);
((RecyclerView) mVerticalScrollView).fling(xVolecity, -yVolecity);
}
}
private boolean canNestedScrollView(View view) {
if (view == null) {
return false;
}
if (view instanceof RecyclerView) {
//显示区域最上面一条信息的position
RecyclerView.LayoutManager manager = ((RecyclerView) view).getLayoutManager();
if (manager == null) {
return true;
}
if (manager.getChildCount() == 0) {
return true;
}
int scrollOffset = ((RecyclerView) view).computeVerticalScrollOffset();
return scrollOffset <= 0;
}
if (view instanceof NestedScrollingChild) {
return view.canScrollVertically(-1);
}
if (!(view instanceof ViewGroup) && (view instanceof View)) {
return true;
}
throw new IllegalArgumentException("不支持非NestedScrollingChild子类ViewGroup");
}
public static class LayoutParams extends FrameLayout.LayoutParams {
public final static int TYPE_HEAD = 0;
public final static int TYPE_BODY = 1;
private int childLayoutType = TYPE_HEAD;
public LayoutParams(@NonNull Context c, @Nullable AttributeSet attrs) {
super(c, attrs);
if (attrs == null) return;
final TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.NestedPagerRecyclerViewLayout);
childLayoutType = a.getInt(R.styleable.NestedPagerRecyclerViewLayout_layoutScrollNestedType, 0);
a.recycle();
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(@NonNull ViewGroup.LayoutParams source) {
super(source);
}
public LayoutParams(@NonNull MarginLayoutParams source) {
super(source);
}
}
}
4.3 布局属性定义
作为布局文件,增加属性,标记View类型
java
<declare-styleable name="NestedPagerRecyclerViewLayout">
<attr name="layoutScrollNestedType" format="flags">
<flag name="Head" value="0"/>
<flag name="Body" value="1"/>
</attr>
<attr name="headExpandedOffset" format="dimension|reference" />
</declare-styleable>
下面是使用时的布局demo,需要设置layoutScrollNestedType
4.4 使用
布局文件
java
<?xml version="1.0" encoding="utf-8"?>
<com.smartian.widget.NestedPagerRecyclerViewLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/NestedScrollChildLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusable="true"
android:focusableInTouchMode="true"
app:headExpandedOffset="45dp">
<LinearLayout
android:id="@+id/head"
android:layout_width="match_parent"
android:layout_height="200dp"
android:orientation="vertical"
app:layoutScrollNestedType="Head">
<TextView
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:background="@color/colorAccent"
android:gravity="center"
android:text="top Head" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="45dp">
<TextView
android:id="@+id/tab1"
android:layout_width="0dip"
android:layout_height="45dp"
android:layout_weight="1"
android:background="@android:color/white"
android:gravity="center"
android:text="我是tab1" />
<View
android:layout_width="1dip"
android:layout_height="match_parent"
android:background="@color/colorAccent" />
<TextView
android:id="@+id/tab2"
android:layout_width="0dip"
android:layout_height="45dp"
android:layout_weight="1"
android:background="@android:color/white"
android:gravity="center"
android:text="我是tab2" />
</LinearLayout>
</LinearLayout>
<android.support.v4.view.ViewPager
android:id="@+id/body"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary"
app:layoutScrollNestedType="Body" />
</com.smartian.widget.NestedPagerRecyclerViewLayout>
至此,我们的方案基本实现了,使用方式如下
java
public class MyNestedScrollViewActivity extends Activity implements View.OnClickListener {
private ViewPager viewPager;
private NestedPagerRecyclerViewLayout scrollChildLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_nested_scrolling_child_layout);
scrollChildLayout = findViewById(R.id.NestedScrollChildLayout);
scrollChildLayout.setHeadExpandOffset((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,45,getResources().getDisplayMetrics()));
viewPager = findViewById(R.id.body);
findViewById(R.id.tab1).setOnClickListener(this);
findViewById(R.id.tab2).setOnClickListener(this);
viewPager.setAdapter(new PagerAdapter() {
@Override
public int getCount() {
return 2;
}
@Override
public boolean isViewFromObject(@NonNull View view, Object object) {
return view==object;
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
container.addView((View) object);
}
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
View layoutView = LayoutInflater.from(container.getContext()).inflate(R.layout.fragment_recycler_view, container, false);
RecyclerView recyclerView = layoutView.findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(container.getContext()));
SimpleRecyclerAdapter adapter = new SimpleRecyclerAdapter(container.getContext(), position%2==0?getData():getData2());
recyclerView.setAdapter(adapter);
container.addView(layoutView);
return layoutView;
}
});
}
private List<String> getData() {
List<String> data = new ArrayList<>();
data.add("#ff9999");
data.add("#ffaa77");
data.add("#ff9966");
data.add("#ffcc55");
data.add("#ff99bb");
data.add("#ff77dd");
data.add("#ff33bb");
data.add("#ff9999");
data.add("#ffaa77");
data.add("#ff9966");
data.add("#ffcc55");
return data;
}
private List<String> getData2() {
List<String> data = new ArrayList<>();
data.add("#9999ff");
data.add("#aa77ff");
data.add("#9966ff");
data.add("#cc55ff");
data.add("#99bbff");
data.add("#77ddff");
data.add("#33bbff");
data.add("#9999ff");
data.add("#aa77ff");
data.add("#9966ff");
data.add("#cc55ff");
return data;
}
@Override
public void onClick(View v) {
int id = v.getId();
if(id==R.id.tab1){
viewPager.setCurrentItem(0,true);
}else if(id==R.id.tab2){
viewPager.setCurrentItem(1,true);
}
}
}
五、总结
ViewPager、RecyclerView 和Tab吸顶效果实现有一定的难度,其实也有很多实现,但是通用性和易用性都有些问题,因此,即便的是最完美的方案也需要经常调整,因此这类效果很难作为库的方式输出,通过本篇的文章,其实提供了一个现成的模板。