前言
目前主流APP常见首页布局有顶部banner+列表、顶部banner+ViewPager等形式,如果有刷新需求,再通过外层嵌套SwipeRefreshLayout以实现刷新需求。若能掌握这些布局方式,就能应对大部分APP需求。
XML布局
布局用到系统控件,需要添加依赖:
implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha03' implementation "androidx.viewpager2:viewpager2:1.0.0" implementation 'com.google.android.material:material:1.1.0-beta02'
一. SwipeRefreshLayout+顶部banner+RecyclerView
效果预览:
xml实现:
<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/swipe_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".NestedScrollViewActivity">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".NestedScrollViewActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/banner_view"
android:layout_width="match_parent"
android:layout_height="300dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
使用androidx.swiperefreshlayout.widget.SwipeRefreshLayout、androidx.core.widget.NestedScrollView、androidx.recyclerview.widget.RecyclerView按照这种排列布局即可实现,不需要自定义view处理滑动冲突。so easy~
ps:RecyclerView初始化、adapter初始化、模拟数据填充等代码,可以参考NestedRecyclerViewActivity.java。
二. SwipeRefreshLayout+顶部banner+ViewPager
效果预览:
xml实现:
<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/swipe_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".NestedScrollViewActivity">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".NestedScrollViewActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/banner_view"
android:layout_width="match_parent"
android:layout_height="300dp" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tablayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<androidx.viewpager2.widget.ViewPager2
android:id="@id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
布局方式和上面类似,只是将RecyclerView替换成TabLayout和ViewPager2。 接下来运行,会发现......有点不对劲,滑动卡壳了。怎么回事,为什么同样是系统控件,TabLayout和ViewPager2就不支持嵌套滑动呢?
点开SwipeRefreshLayout和NestedScrollView源码,发现他们均实现了NestedScrollingParent2、NestedScrollingChild2接口,RecyclerView实现了NestedScrollingChild2接口。而TabLayout和ViewPager2没有实现NestedScrollingParent2、NestedScrollingChild2任一接口。
关于NestedScrollingParent2、NestedScrollingChild2接口
- NestedScrollingParent2官方注释:
This interface should be implemented by {@link android.view.ViewGroup ViewGroup} subclasses that wish to support scrolling operations delegated by a nested child view. Classes implementing this interface should create a final instance of a {@link NestedScrollingParentHelper} as a field and delegate any View or ViewGroup methods to the
NestedScrollingParentHelper
methods of the same signature.
简单说就是希望支持嵌套滑动的父容器需要实现此接口,处理来自子视图传递的滑动事件。可以借助NestedScrollingParentHelper辅助类来执行相应方法。
- NestedScrollingChild2官方注释:
This interface should be implemented by {@link View View} subclasses that wish to support dispatching nested scrolling operations to a cooperating parent {@link android.view.ViewGroup ViewGroup}. Classes implementing this interface should create a final instance of a {@link NestedScrollingChildHelper} as a field and delegate any View methods to the
NestedScrollingChildHelper
methods of the same signature.
简单说就是希望支持嵌套滑动的子视图需要实现此接口,优先将滚动事件向上传递给父容器处理。可以借助NestedScrollingChildHelper辅助类来执行相应方法。
- NestedScrollingParent2接口方法说明:
/**
* 子视图触发滑动时会回调该方法,父容器在该方法中根据子view、滑动方向、触摸类型等判断自己是否支持接收,
* 若接收返回true,否则返回false。(可由NestedScrollingChild2的startNestedScroll方法触发)
*/
boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes, @NestedScrollType int type);
/**
* onStartNestedScroll返回true后会回调该方法,可在此方法中做一些初始配置操作。
*/
void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes, @NestedScrollType int type);
/**
* 开始滑动时,子视图会优先回调该方法。父容器可以处理自己的滚动操作,之后将剩余的滚动偏移量
* 传回给子视图。(可由NestedScrollingChild2的dispatchNestedPreScroll方法触发)
*/
void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, @NestedScrollType int type);
/**
* 子视图处理完剩余的滚动偏移量后,若还有剩余,则将剩余的滚动偏移量再通过该回调传给
* 父容器处理。(可由NestedScrollingChild2的dispatchNestedScroll方法触发)
*/
void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @NestedScrollType int type);
/**
* 当滑动结束时,回调该方法。(可由NestedScrollingChild2的stopNestedScroll方法触发)
*/
void onStopNestedScroll(@NonNull View target, @NestedScrollType int type);
(ps:接口方法详细的说明可以查看源码注释或者百度谷歌。)
- NestedScrollingChild2接口方法说明:
/**
* 通知开始滑动,会回调父容器的onStartNestedScroll方法。
*/
boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type);
/**
* 通知停止滑动,会回调父容器的onStopNestedScroll方法。
*/
void stopNestedScroll(@NestedScrollType int type);
/**
* 查询是否有父容器支持指定类型的嵌套滑动。
*/
boolean hasNestedScrollingParent(@NestedScrollType int type);
/**
* 在子视图处理滑动前,先将滚动偏移量传递给父容器。
*/
boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, @NestedScrollType int type);
/**
* 子视图处理滑动后,再将剩余的滚动偏移量传递给父容器。
*/
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, @NestedScrollType int type);
(ps:接口方法详细的说明可以查看源码注释或者百度谷歌。)
方法执行流程规范: child.startNestedScroll -> parent.onStartNestedScroll -> parent.onNestedScrollAccepted -> child.dispatchNestedPreScroll -> parent.onNestedPreScroll -> child.dispatchNestedScroll -> parent.onNestedScroll -> child.stopNestedScroll -> parent.onStopNestedScroll
简而言之,滑动产生时,由child主动通知,parent被动接收判断处理。这里的child和parent不必是直接父子关系,child会向上遍历parent。
部分参数含义说明:
- child:表示包含target的当前容器的直接子view。
- target:表示调用startNestedScroll触发onStartNestedScroll回调的那个子view。
- axes:表示即将滑动的坐标轴方向,通过位运算求出方向。
- type:表示触摸类型,有TYPE_TOUCH(用户触摸)、TYPE_NON_TOUCH(惯性滑动)两种类型。
- dx:水平滑动偏移量。<0表示手指向右划,>0则相反。
- dy:垂直滑动偏移量。<0表示手指向下划,>0则相反。
- consumed:保存父容器滑动消耗的偏移量(索引0存x轴偏移,1存y轴偏移)。在父容器滑动后,子view会将原偏移量减去consumed中的值得到剩余偏移量,再进行自身的滚动处理。
- dxConsumed:子view消耗的水平偏移量。
- dyConsumed:子view消耗的垂直偏移量。
- dxUnconsumed:子view滑动后还剩下的水平偏移量。
- dyUnconsumed:子view滑动后还剩下的垂直偏移量。
注意:若有用户触摸滑动到惯性滑动,会走两遍方法执行流程,即不同type各触发一次流程。
因为TabLayout和ViewPager2不支持这种布局下的嵌套滑动,所以只能通过自定义view来处理滑动和事件分发。
滑动逻辑分析
首先将布局拆分成上下两部分(即父容器包含top_view和content_view两个子视图):
- 当手指向上滑动时,若top_view仍然可见,则父容器需要进行滚动处理直至top_view不可见。
- 当手指向下滑动时,若top_view不完全可见(即之前向上滑动过),且content_view不可向下滑动(即content_view自身内容已经滑动至自身顶部),则父容器需要进行滚动处理直至top_view完全可见。
代码实现
自定义父容器ComboScrollLayout
- ComboScrollLayout继承NestedScrollingParent2
public class ComboScrollLayout extends LinearLayout implements NestedScrollingParent2 {
private View topView;
private View contentView;
private int topHeight;
private NestedScrollingParentHelper parentHelper = new NestedScrollingParentHelper(this);
@Override
protected void onFinishInflate() {
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
}
@Override
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
}
@Override
public int getNestedScrollAxes() {
}
@Override
public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) {
}
@Override
public void onStopNestedScroll(@NonNull View target, int type) {
}
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
}
@Override
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
}
}
- 初始成员变量
public class ComboScrollLayout extends LinearLayout implements NestedScrollingParent2 {
@Override
protected void onFinishInflate() {
super.onFinishInflate();
// 获取top_view和content_view
if (getChildCount() > 0) {
topView = getChildAt(0);
}
if (getChildCount() > 1) {
contentView = getChildAt(1);
}
if (topView == null || contentView == null) {
throw new AndroidRuntimeException("容器中至少需要两个子view");
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// 获取top_view的高度
if (topView != null) {
topHeight = topView.getMeasuredHeight();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 调整contentView的高度为父容器高度,使之填充布局,避免父容器滚动后出现空白
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
ViewGroup.LayoutParams lp = contentView.getLayoutParams();
lp.height = getMeasuredHeight();
contentView.setLayoutParams(lp);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
- 判断处理滑动的方向
public class ComboScrollLayout extends LinearLayout implements NestedScrollingParent2 {
@Override
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
if (contentView != null) {
// 开始滚动前先停止滚动
if (contentView instanceof RecyclerView) {
((RecyclerView) contentView).stopScroll();
} else if (contentView instanceof NestedScrollView) {
((NestedScrollView) contentView).stopNestedScroll();
} else if (contentView instanceof ViewPager2) {
((ViewPager2) contentView).stopNestedScroll();
}
}
topView.stopNestedScroll();
// 处理垂直方向的滑动
boolean handled = (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
return handled;
}
@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);
}
}
ComboScrollLayout需要在onStartNestedScroll方法中判断即将开始滑动的方向是否是自己想要处理的。本例中判断若为垂直方向滚动就返回true,表示将接收处理垂直方向滑动事件。
在onNestedScrollAccepted方法中,调用了NestedScrollingParentHelper辅助类的同样方法签名的方法,用以缓存前一步onStartNestedScroll方法中判断条件拦截的结果。
getNestedScrollAxes方法中,调用NestedScrollingParentHelper辅助类的同名方法,返回前一步onNestedScrollAccepted方法中缓存的进行拦截处理的坐标轴。
- 拦截滑动处理
public class ComboScrollLayout extends LinearLayout implements NestedScrollingParent2 {
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
// 向上滑动。若当前topview可见,需要将topview滑动至不可见
boolean hideTop = dy > 0 && getScrollY() < topHeight;
// 向下滑动。若contentView滑动至顶,已不可再滑动,且当前topview未完全可见,则将topview滑动至完全可见
boolean showTop = dy < 0 &&
getScrollY() > 0 &&
!ViewCompat.canScrollVertically(target, -1) &&
!ViewCompat.canScrollVertically(contentView, -1);
if (hideTop || showTop) {
// 若需要滑动topview,则滑动dy偏移量
scrollBy(0, dy);
// 将ComboScrollLayout消耗的偏移量赋值给consumed数组
consumed[1] = dy;
}
}
@Override
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
if (dyUnconsumed > 0) {
if (target == topView) {
// 由topView发起的向上滑动,继续让contentView滑动剩余的未消耗完的偏移量
scrollBy(0, dyUnconsumed);
}
}
}
@Override
public void scrollTo(int x, int y) {
// 将ComboScrollLayout自身的滚动范围限制在0~topHeight(即在topview完全可见至完全不可见的范围内滑动)
if (y < 0) {
y = 0;
}
if (y > topHeight) {
y = topHeight;
}
super.scrollTo(x, y);
}
}
ComboScrollLayout拦截滑动处理是在onNestedPreScroll和onNestedScroll回调方法中,其中onNestedPreScroll触发时机是在子view进行滑动之前,onNestedScroll是在子view滑动之后。
本例在onNestedPreScroll方法中,会判断当前是否需要由ComboScrollLayout进行滚动,若判断成立,则会消耗掉所有偏移量,子view将不再处理滚动,从而达到拦截的目的。
- 滑动停止
public class ComboScrollLayout extends LinearLayout implements NestedScrollingParent2 {
@Override
public void onStopNestedScroll(@NonNull View target, int type) {
parentHelper.onStopNestedScroll(target, type);
}
}
当滑动结束时,将会触发onStopNestedScroll方法,可以做一些收尾工作。本例中委托给NestedScrollingParentHelper的同名方法。
- SwipeRefreshLayout冲突处理
若要支持下拉刷新,需要在ComboScrollLayout外套一层SwipeRefreshLayout,需要处理和SwipeRefreshLayout下拉的滑动冲突。
首先获取SwipeRefreshLayout引用:
public class ComboScrollLayout extends LinearLayout implements NestedScrollingParent2 {
private SwipeRefreshLayout refreshLayout;
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (topView != null) {
topHeight = topView.getMeasuredHeight();
}
// 获取外层SwipeRefreshLayout
if (refreshLayout == null && getParent() != null && getParent() instanceof SwipeRefreshLayout) {
refreshLayout = (SwipeRefreshLayout) getParent();
}
}
}
在滑动开始/结束时启用/禁用SwipeRefreshLayout:
public class ComboScrollLayout extends LinearLayout implements NestedScrollingParent2 {
@Override
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
// 省略部分代码
...
boolean handled = (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
// 若为垂直滚动方向,且topView未完全可见,应由ComboScrollLayout处理滑动,禁用SwipeRefreshLayout。
if (handled && refreshLayout != null && getScrollY() != 0) {
refreshLayout.setEnabled(false);
}
return handled;
}
@Override
public void onStopNestedScroll(@NonNull View target, int type) {
// 滑动结束,启用SwipeRefreshLayout
if (refreshLayout != null) {
refreshLayout.setEnabled(true);
}
parentHelper.onStopNestedScroll(target, type);
}
}
至此完成了自定义容器编写,ComboScrollLayout实现了NestedScrollingParent2接口,从而能够响应处理子view滑动事件,优先消耗掉滑动事件。
(ps:完整源码见ComboScrollLayout.java)
修改XML布局
<?xml version="1.0" encoding="utf-8"?>
<com.cdh.nestedscrolling.widget.ComboSwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/swipe_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".NestedScrollViewActivity">
<com.cdh.nestedscrolling.widget.ComboScrollLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@id/combo_top_view"
android:layout_width="match_parent"
android:layout_height="300dp"
app:layout_constraintTop_toTopOf="parent"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tablayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<androidx.viewpager2.widget.ViewPager2
android:id="@id/combo_content_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
</com.cdh.nestedscrolling.widget.ComboScrollLayout>
</com.cdh.nestedscrolling.widget.ComboSwipeRefreshLayout>
(ps:ComboSwipeRefreshLayout继承自SwipeRefreshLayout,重写了onInterceptTouchEvent方法,用于处理嵌套横向banner时的滑动冲突。完整源码可见ComboSwipeRefreshLayout.java)
自定义子视图ComboChildLayout
父容器实现了NestedScrollingParent2接口,子view也需要实现NestedScrollingChild2接口,才能形成完整的交互。
- 定义关键成员变量
public class ComboChildLayout extends LinearLayout implements NestedScrollingChild2 {
private int orientation;
// touch滑动相关参数
private int lastX = -1, lastY = -1;
private final int[] offset = new int[2];
private final int[] consumed = new int[2];
// fling滑动相关参数
private boolean isFling;
private final int minFlingVelocity, maxFlingVelocity;
private Scroller scroller;
private VelocityTracker velocityTracker;
private int lastFlingX, lastFlingY;
private final int[] flingConsumed = new int[2];
private NestedScrollingChildHelper childHelper = new NestedScrollingChildHelper(this);
public ComboChildLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
// 获取布局排布方向
orientation = getOrientation();
setNestedScrollingEnabled(true);
// 获取当前页面配置信息
ViewConfiguration config = ViewConfiguration.get(context);
// 设置系统默认最小和最大加速度
minFlingVelocity = config.getScaledMinimumFlingVelocity();
maxFlingVelocity = config.getScaledMaximumFlingVelocity();
scroller = new Scroller(context);
}
}
- 继承NestedScrollingChild2
public class ComboChildLayout extends LinearLayout implements NestedScrollingChild2 {
@Override
public boolean startNestedScroll(int axes, int type) {
return childHelper.startNestedScroll(axes, type);
}
@Override
public void stopNestedScroll(int type) {
childHelper.stopNestedScroll(type);
}
@Override
public boolean hasNestedScrollingParent(int type) {
return childHelper.hasNestedScrollingParent(type);
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, int type) {
if (orientation == VERTICAL) {
dxUnconsumed = 0;
} else {
dyUnconsumed = 0;
}
return childHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, int type) {
if (orientation == VERTICAL) {
dx = 0;
} else {
dy = 0;
}
return childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
}
@Override
public void setNestedScrollingEnabled(boolean enabled) {
childHelper.setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {
return childHelper.isNestedScrollingEnabled();
}
}
这些方法中全部委托给NestedScrollingChildHelper辅助类相应的同名方法执行。
- 重写onTouchEvent分发事件
public class ComboChildLayout extends LinearLayout implements NestedScrollingChild2 {
@Override
public boolean onTouchEvent(MotionEvent event) {
// 重置fling相关参数
cancelFling();
// 获取VelocityTracker,用于加速度计算
if (velocityTracker == null) {
velocityTracker = VelocityTracker.obtain();
}
// 追踪触摸点移动加速度
velocityTracker.addMovement(event);
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
// 初始化值
consumed[0] = 0;
consumed[1] = 0;
offset[0] = 0;
offset[1] = 0;
lastX = (int) event.getX();
lastY = (int) event.getY();
// 调用startNestedScroll通知parent根据滑动方向和滑动类型进行启用嵌套滑动,
// 当前属于用户触摸滑动,type传TYPE_TOUCH。
if (orientation == VERTICAL) {
// 垂直方向滑动
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
} else {
// 水平方向滑动
startNestedScroll(ViewCompat.SCROLL_AXIS_HORIZONTAL, ViewCompat.TYPE_TOUCH);
}
break;
case MotionEvent.ACTION_MOVE:
int curX = (int) event.getX();
int curY = (int) event.getY();
// 计算滑动偏移量,起始坐标-当前坐标
int dx = lastX - curX;
int dy = lastY - curY;
// 优先将滑动偏移量交由parent处理,
if (dispatchNestedPreScroll(dx, dy, consumed, offset, ViewCompat.TYPE_TOUCH)) {
// parent滑动完后
// 滑动偏移量减去parent消耗的量
dx -= consumed[0];
dy -= consumed[1];
}
// 用于记录子view自身滑动消耗的偏移量
int consumedX = 0;
int consumedY = 0;
// 自身或child进行滑动
if (orientation == VERTICAL) {
consumedY = childConsumedY(dy);
} else {
consumedX = childConsumedX(dx);
}
// 滑动偏移量减去自身或child消耗的量,然后再交由parent处理
dispatchNestedScroll(consumedX, consumedY, dx-consumedX, dy-consumedY, null, ViewCompat.TYPE_TOUCH);
lastX = curX;
lastY = curY;
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
// 通知parent滑动结束
stopNestedScroll(ViewCompat.TYPE_TOUCH);
if (velocityTracker != null) {
// 计算触摸点加速度
velocityTracker.computeCurrentVelocity(1000, maxFlingVelocity);
// 获取xy轴加速度
int vx = (int) velocityTracker.getXVelocity();
int vy = (int) velocityTracker.getYVelocity();
// 进行fling
fling(vx, vy);
velocityTracker.clear();
}
lastX = -1;
lastY = -1;
break;
default:
break;
}
return true;
}
}
在onTouchEvent方法中,首先在DOWN时通过通知parent对滑动进行判断响应。之后在ACTION_MOVE过程中,计算滑动偏移量,优先交由parent进行消耗处理,若有parent接收处理,则在parent滑动后,减去parent消耗的偏移量,在交给自身或子view进行剩余偏移量的滑动。若自身或子view滑动后还有剩余的偏移量,则再交由parent处理。最后在UP/CANCEL通知parent滑动结束。
(ps:在本例中,UP/CANCEL后有进行fling操作,在fling中会再触发startNestedScroll到stopNestedScroll的过程,不同的是传递的type变为TYPE_NON_TOUCH。)
自定义子视图完整源码可见ComboChildLayout.java
- ComboChildLayout使用场景 ComboChildLayout不是用来包装ViewPager2,而是当ViewPager2有某一页为普通view时,用来包裹该页的根布局。 本例在ViewPager2中添加了三个Fragment,分别演示ViewPager2下为普通线性布局、滚动布局、列表布局的情况。其中普通线性布局需要使用ComboChildLayout进行包裹,而滚动布局和列表布局因为使用NestedScrollView和RecyclerView,其自身实现了NestedScrollingChild2接口,所以不需要额外操作。