一、前言
app 首页中经常要实现首页头卡共享,tab 吸顶,内容区通过 viewpager 切换的需求,以前往往是利用事件处理来完成,还有 google 官方也提供了相关的库如coordinatorlayout,但是这些也有一定的弊端和滑动方面不如意的地方,瑕疵比较明显,实际上很多大厂的吸顶效果都是自己写的,同样适配起来还是比较复杂。
这里我们利用 nestedscrolling 机制来实现。
当然也有很多开源项目,发现存在的问题很多面,主要问题如下:
- 头部和内容区域不联动
- 没有中断 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。
<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接口
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添加布局属性
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);
}
}
测量
我们这里纵向排列即可
@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);
}
}
}
核心方法,纵向滑动处理
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);
}
四、代码实现
4.1 要点
头部不联动问题:
我们需要处理在 dispatchtouchevent 或者利用 onintecepttouchevent + ontouchevent 处理,主要处理 velocitytracker + fling 事件。接着我们判断滑动开始位置是不是在头部,因为按照布局设计,头部和recyclerview不一样,头部是随着整体滑动,而recyclerview是可以内部滑动的,直到无法滑动时,我们才能让父布局整体滑动,通过这种方式就能解决联动问题。
recyclerview 中断 fling 效果问题:
recyclerview 没有在 stopnestedscroll () 方法中中断滑动,因此需要通过侵入方式,调用 stopscroll () 去完成,其实我们这里希望官方提供接口终止recyclerview停止滑动,但是事实上没有,这个问题一定概率上造成recyclerview减速滑动时,viewpager也无法切换,当然很多其他开源方案都有类似的问题。
if (mverticalscrollview instanceof recyclerview) {
/**
* recyclerview 虽然继承了nestedscrollingchild,但是没有在stopnestedscroll中停止
* 调用stopscroll,导致滑动状态事件自动捕获,造成viewpager切换问题,这里使用stopscroll()侵入式调用
*/
((recyclerview) mverticalscrollview).stopscroll();
}
查找事件点所在的view,这里我们使用了下面方法,理论上我们不会子head和body部分做matrix变换,因此android内部通过矩阵判断view的逆矩阵方式我们可以不用。
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
@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 主要代码
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类型
<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 使用
布局文件
<?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>
至此,我们的方案基本实现了,使用方式如下
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吸顶效果实现有一定的难度,其实也有很多实现,但是通用性和易用性都有些问题,因此,即便的是最完美的方案也需要经常调整,因此这类效果很难作为库的方式输出,通过本篇的文章,其实提供了一个现成的模板。
以上就是android使用scrolling机制实现tab吸顶效果的详细内容,更多关于android scrolling吸顶的资料请关注代码网其它相关文章!
发表评论