1、解决RecyclerView与NestedScrollView的滑动冲突
问题一:当我们滑动RecyclerView组件时,上方的轮播图并没有进行滑动(NestedScrollView没有滑动,即滑动事件被RecyclerView消费了),当RecyclerView滑到底时,轮播图部分才进行滑动。 如下图,RecyclerView已经进行了滑动,但轮播图部分没有。
整体布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:AutoLoopStyle="http://schemas.android.com/apk/res-auto"
android:id="@+id/home_pager_parent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/color_page_Bg"
android:gravity="center"
android:orientation="vertical">
<!--滚动页面-->
<androidx.core.widget.NestedScrollView
android:id="@+id/home_pager_nested_scroller"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="never">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<LinearLayout
android:id="@+id/home_pager_header_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="125dp"
android:layout_marginBottom="14dp">
<com.example.taobaounion.ui.custom.AutoLoopViewPager
android:id="@+id/looper_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
AutoLoopStyle:duration="4000"
android:overScrollMode="never" />
<LinearLayout
android:id="@+id/looper_point_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginBottom="10dp"
android:gravity="center"
android:orientation="horizontal" />
</RelativeLayout>
<!--标题-->
<include layout="@layout/include_home_pager_title_part"
/>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/home_pager_content_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:overScrollMode="never" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>
这并不符合我们的设计要求,我们希望让轮播图先滑到顶部,然后才进行RecycleView的滑动。在recycleView中有个方法使用recyclerView.setNestedScrollingEnabled(false); 即可解决滑动冲突。进行测试
@BindView(R.id.home_pager_content_list)
public RecyclerView mContentList;
@Override
protected void initView(View rootView) {
mContentList.setNestedScrollingEnabled(false); //没错,就是我了
}
测试后发现可行,但是出现了另一个问题,当轮播图部分滑动出屏幕时,就不能继续向上滑动了。 如下图,此时无论是NestedScrollView还是RecyclerView都不能再继续向上滑动了。
基于这个现象,我猜测recyclerView.setNestedScrollingEnabled(false)方法实际上让NestedScrollView不再将滑动事件继续向下分发,而是独自消费了这个事件。那么由于NestScrollView的高度限制(轮播图的高度和recyclerView的item高度),以及RecyclerView没有收到滑动事件(不能继续更新item数据),因此此时不能再继续向上滑动。 如果我的猜测正确,那么要解决这个问题只需要在合适的时机让NestedScrollView将事件分发给RecyclerView,即在合适的时机调用mContentList.setNestedScrollingEnabled(true)即可。 于是我在HomePagerFragment类的initListener()方法中,为NestedScrollView设置了监听器,根据其滑动的距离为其设置是否独自消费事件。
//尝试解决滑动冲突
mNestedScrollView.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {
@Override
public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
LogUtils.e(HomePagerFragment.class,"NestedScrollView --> scrollY -->" + scrollY);
//滑动的距离大于或等于mContentList的顶部位置时
if (scrollY >= mContentList.getTop()) mContentList.setNestedScrollingEnabled(true);
else mContentList.setNestedScrollingEnabled(false);
}
});
测试结果:可行,滑动冲突解决,但是仍存在一些瑕疵。
2、解决刷新控件冲突
解决了滑动冲突之后,把刷新控件增加进来,这里1为了修改刷新组件的源代码,将它的模块依赖添加到项目中
项目地址:github地址
发现再次出现了问题。 如下图,还没拉到底呢,你怎么就给我加载更多了呢
道理是一样的,因为刷新组件TwinklingRefreshLayout消耗了事件,RecyclerView并没有收到事件,所以出现了这种情况。 解决的方法就是在刷新组件消耗事件的方法中进行判断,如果RecyclerView还能进行滑动,那就不消耗这个事件,将事件分发给RecyclerView,否则就消耗这个事件,进行数据的加载。
通过观察源码发现RefreshProcessor类重写了dispatchTouchEvent方法,如下代码所示
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
downEventSent = false;
intercepted = false;
mTouchX = ev.getX();
mTouchY = ev.getY();
if (cp.isEnableKeepIView()) {
if (!cp.isRefreshing()) {
cp.setPrepareFinishRefresh(false);
}
if (!cp.isLoadingMore()) {
cp.setPrepareFinishLoadMore(false);
}
}
cp.dispatchTouchEventSuper(ev);
return true;
case MotionEvent.ACTION_MOVE:
mLastMoveEvent = ev;
float dx = ev.getX() - mTouchX;
float dy = ev.getY() - mTouchY;
if (!intercepted && Math.abs(dx) <= Math.abs(dy) && Math.abs(dy) > cp.getTouchSlop()) {//滑动允许最大角度为45度
if (dy > 0 && ScrollingUtil.isViewToTop(cp.getTargetView(), cp.getTouchSlop()) && cp.allowPullDown()) {
cp.setStatePTD();
mTouchX = ev.getX();
mTouchY = ev.getY();
sendCancelEvent();
intercepted = true;
return true;
} else if (dy < 0 && ScrollingUtil.isViewToBottom(cp.getTargetView(), cp.getTouchSlop()) && cp.allowPullUp()) {
cp.setStatePBU();
mTouchX = ev.getX();
mTouchY = ev.getY();
intercepted = true;
sendCancelEvent();
return true;
}
}
if (intercepted) {
if (cp.isRefreshVisible() || cp.isLoadingVisible()) {
return cp.dispatchTouchEventSuper(ev);
}
if (!cp.isPrepareFinishRefresh() && cp.isStatePTD()) {
if (dy < -cp.getTouchSlop() || !ScrollingUtil.isViewToTop(cp.getTargetView(), cp.getTouchSlop())) {
cp.dispatchTouchEventSuper(ev);
}
dy = Math.min(cp.getMaxHeadHeight() * 2, dy);
dy = Math.max(0, dy);
cp.getAnimProcessor().scrollHeadByMove(dy);
} else if (!cp.isPrepareFinishLoadMore() && cp.isStatePBU()) {
//加载更多的动作
if (dy > cp.getTouchSlop() || !ScrollingUtil.isViewToBottom(cp.getTargetView(), cp.getTouchSlop())) {
cp.dispatchTouchEventSuper(ev);
}
dy = Math.max(-cp.getMaxBottomHeight() * 2, dy);
dy = Math.min(0, dy);
cp.getAnimProcessor().scrollBottomByMove(Math.abs(dy));
}
if (dy == 0 && !downEventSent) {
downEventSent = true;
sendDownEvent();
}
return true;
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
if (intercepted) {
if (cp.isStatePTD()) {
willAnimHead = true;
} else if (cp.isStatePBU()) {
willAnimBottom = true;
}
intercepted = false;
return true;
}
break;
}
return cp.dispatchTouchEventSuper(ev);
}
这里的isViewToBottom方法中的getTargetView就是拿到我们包裹在TwinklingRefreshLayout刷新控件里的内容
再看看isViewToBottom方法
public static boolean isViewToBottom(View view, int mTouchSlop) {
if (view instanceof AbsListView) return isAbsListViewToBottom((AbsListView) view);
if (view instanceof RecyclerView) return isRecyclerViewToBottom((RecyclerView) view);
if (view instanceof WebView) return isWebViewToBottom((WebView) view, mTouchSlop);
if (view instanceof ViewGroup) return isViewGroupToBottom((ViewGroup) view);
return false;
}
这个方法判断刷新组件中的子容器是什么类型,并根据类型调用不同的方法,我们添加一段判断子容器是NestedScrollView的语句,注意要写在判断ViewGroup类型的上面。
if (view instanceof NestedScrollView) return isNestedScrollViewToBottom((NestedScrollView)view);//这段是我添加的
根据之前的分析完成方法
private static boolean isNestedScrollViewToBottom(NestedScrollView view) {
ViewGroup viewGroup = (ViewGroup) view.getChildAt(0); //根据布局文件知道这其实是一个LinearLayout
RecyclerView recyclerView = null;
//找到LinearLayout中的RecyclerView
for (int i = 0; i < viewGroup.getChildCount(); i++) {
if (viewGroup.getChildAt(i) instanceof RecyclerView)
recyclerView = (RecyclerView) viewGroup.getChildAt(i);
}
//如果RecyclerView能继续向上滑动,则不消费这个事件
//recyclerView.canScrollVertically(1))表示是否可以向上滑动
if (recyclerView != null && recyclerView.canScrollVertically(1)) return false;
//否则消费该事件
return true;
}
至此,刷新控件冲突就解决了。不过这个方法并没有经过细细的打磨,因此还存在着一些问题。比如最后实现的方法实际上并不完善,它只能在刷新组件中仅有一个RecyclerView的情况下才能正常使用。不过也是一种解决问题的思路