CoordinatorLayout作为顶层布局与NestedScrollView配合使用,可以用来协调子View的嵌套滑动。但是,如果要在CoordinatorLayout的外层嵌套XRefreshView下拉刷新控件,并且NestedScrollView嵌套多种可滑动的控件,这时候就出现了滑动冲突,具体嵌套结构如下所示:
1534665037068.jpg
XML布局结构大致如下:
<XRefreshView>
<MyCoordinatorLayout>
<AppBarLayout>
<CollapsingToolbarLayout>
......
</CollapsingToolbarLayout>
</AppBarLayout>
</MyCoordinatorLayout>
<NestedScrollView>
<VerticalLinearLayout>
<LinearLayout/>
<SmartTabLayout/>
<ViewPager/>
</VerticalLinearLayout>
</NestedScrollView>
</XRefreshView>
<!--XRefreshView开源下拉刷新控件-->
<!--SmartTabLayout开源分类轴控件-->
<!--MyCoordinatorLayout:自定义控件,继承CoordinatorLayout-->
<!--VerticalLinearLayout:自定义控件,继承LinearLayout-->
<!--ViewPager下每个Fragment的布局是RecyclerView-->
<RecyclerView/>
对于这些滑动冲突,我们该如何解决呢?下面我们就来逐一分析,解决这些滑动冲突。
1、XRefreshView嵌套CoordinatorLayout
我们知道事件分发是从上往下传递的(从Activity->Window->DecorView->......View),所以我们是不是可以在需要下拉刷新的时候,将事件拦截,交给下拉刷新控件处理,其他时候都不拦截事件。那么我们要选择用内部拦截法还是外部拦截法来处理滑动冲突呢?
外部拦截法,那我们就需要在XRefreshView的onInterceptTouchEvent进行处理。XRefreshView是一个开源控件,从源码中,我们可以看到XRefreshView并未覆写onInterceptTouchEvent,而是覆写了dispatchTouchEvent方法,并在方法中进行了一系列复杂的事件分发。若使用外部拦截法来处理,就需要理清XRefreshView原有的事件分发规则,根据我们的实际需求对源码进行修改,对于父容器需要的事件进行拦截。
内部拦截法,自定义CoordinatorLayout,覆写dispatchTouchEvent方法,通过调用requestDisallowInterceptTouchEvent方法来干扰父容器对事件的拦截。从XRefreshView的源码中我们发现该控件提供了一个disallowInterceptTouchEvent方法,从方法注释可知,子View需要事件的时候,可设置为true,不允许父容器拦截触摸事件。
XRefreshView.java
/**
//XRefreshView.java
* if child need the touch event,pass true
*/
public void disallowInterceptTouchEvent(boolean isIntercept) {
mIsIntercept = isIntercept;
}
相比两种方法,这里内部拦截法更简单,所以最终选择了内部拦截法。
触发下拉刷新的时机:垂直向下滑动,appBarLayout完全展开状态,允许XRefreshView拦截事件。
//MyCoordinatorLayout.java
private int mLastX,mLastY;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//内部拦截法
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
if(xRefreshView!=null){
xRefreshView.disallowInterceptTouchEvent(true);
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if(Math.abs(deltaY)>Math.abs(deltaX)){
// if(observed!=null && observed.getAppBarLayoutStatus() == 1 && deltaY>0 && xRefreshView!=null && isTop()){
// //垂直向下滑动,appBarLayout展开状态,列表第一个item可见,将事件交还给父容器XRefreshView
// xRefreshView.disallowInterceptTouchEvent(false);
// }
// 注意:若此时item位置不是第一个可见时,不能下拉刷新,若不需要item位置第一个可见时就可以下拉刷新,可以把isTop判断去掉,若下所示:
if(observed!=null && observed.getAppBarLayoutStatus() == 1 && deltaY>0 && xRefreshView!=null){
//垂直向下滑动,appBarLayout展开状态,将事件交还给父容器XRefreshView
xRefreshView.disallowInterceptTouchEvent(false);
}else{
//判断触摸点是否落在banner上
bannerView = getBannerView();
if(bannerView!=null){
isTouchPointInBannerView = Util.calcViewScreenLocation(bannerView).contains(ev.getRawX(),ev.getRawY());
}else{
isTouchPointInBannerView = false;
}
}
}
break;
case MotionEvent.ACTION_UP:
isTouchPointInBannerView = false;
break;
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(ev);
}
2、NestedScrollView嵌套多种布局控件(LinearLayout、SmartTabLayout、ViewPager,ViewPager中又嵌套了RecyclerView)
这里主要解决的不是滑动冲突,而是NestedScrollView嵌套ViewPager无法显示的问题。网上的解决方案有2种,一是重写ViewPager的onMeasure方法,遍历每个页面,获取最高的页面高度来设置ViewPager的高度,二是给NestedScrollView设置android:fillViewport="true"允许 NestedScrollView中的组件去充满它。对于第一种方案,存在一个问题,每个页面的内容高度都一样,并且滑动其中一个页面的列表时,其他页面的列表也会滑动,所以这里采用了方案二。
在SmartTablayout上方我们设置了一个LinearLayout,这个LinearLayout可以用来作为广告布局的一个父容器。当滑动NestedScrollView时,这个LinearLayout需要可以往屏幕外滑出,直到smartTabLayout保持在置顶位置。那要怎么让LinerLayout可以滑出屏幕直至不可见呢?答案是增大可滑动的空间,在原来内容高度的基础上增加广告布局父容器的高度。我们知道,NestedScrollView不能直接嵌套多个布局,只能有一个直接子类(允许直接嵌套的子类有RecyclerVIew、ViewPager、LinearLayout),这里选择在线性布局里嵌套多个布局,并且自定义这个直接子类,重新去计算它的高度(在系统计算的高度上,加上允许滑出屏幕的高度),具体如下所示:
xml布局:
......
<android.support.v4.widget.NestedScrollView
android:id="@+id/nestedScrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behaviorr"
android:fillViewport="true">
<com.lmz.viewdemo.view.VerticalLinearLayout
android:id="@+id/NestedVerLinearLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:descendantFocusability="blocksDescendants">
<LinearLayout
android:id="@+id/linBanner"
android:layout_width="match_parent"
android:layout_height="150dp"
android:orientation="vertical"
android:background="@drawable/banner"/>
<com.ogaclejapan.smarttablayout.SmartTabLayout
android:id="@+id/smartTabLayout"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_toLeftOf="@+id/ivCategoryBtn"
android:background="#d8d8d8"
android:overScrollMode="never"
app:stl_defaultTabTextHorizontalPadding="24dp"
app:stl_dividerColor="@android:color/transparent"
app:stl_dividerThickness="0dp"
app:stl_indicatorColor="#ff3444"
app:stl_indicatorInterpolation="linear"
app:stl_indicatorThickness="4dp"
app:stl_titleOffset="auto_center"
app:stl_underlineColor="@android:color/transparent"
app:stl_underlineThickness="0dp"/>
<android.support.v4.view.ViewPager
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</com.lmz.viewdemo.view.VerticalLinearLayout>
</android.support.v4.widget.NestedScrollView>
......
VerticalLinearLayout.java
public class VerticalLinearLayout extends LinearLayout{
private int maxOffsetY;
public VerticalLinearLayout(Context context) {
super(context);
init();
}
public VerticalLinearLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public VerticalLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init(){
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int specSize = MeasureSpec.getSize(heightMeasureSpec);
View child;
int childCount = getChildCount();
int offset = 0;
for(int i=0;i<childCount;i++){
child = getChildAt(i);
if(child!=null && child.getVisibility()!=View.GONE
&& !(child instanceof ViewPager) && !(child instanceof SmartTabLayout)){
measureChildWithMargins(child,widthMeasureSpec,0,MeasureSpec.UNSPECIFIED,0);
offset = offset + child.getMeasuredHeight();
}
}
this.maxOffsetY = offset;//可滑出屏幕的最大距离
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(specSize + offset,MeasureSpec.EXACTLY));
}
public int getMaxOffsetY(){
return maxOffsetY;
}
}
3、CoordnatorLayout嵌套NestedScrollView(NestedScrollView内嵌套多种控件)
在进入正题之前,我们先来简单回顾下NestedScrolling滑动机制。关键接口NestedScrollingParent和NestedScrollingChild,以及他们所对应的Helper(NestedScrollingParentHelper、NestedScrollingChildHelper)。
嵌套滑动首先由子view触发调用startNestedScroll方法,寻找能够配合子View嵌套滑动parent。在子View处理滑动事件之前调用dispatchNestedPreScroll,询问parent是否需要在子View之前处理滑动,通过回调onNestedPreScroll方法告知parent当前滑动的距离,若父类有消耗滑动距离,可通过onNestedPreScroll方法的consumed这个输出参数来告知子View。子View处理完滑动事件后调用dispatchNestedScroll方法通过回调onNestedScroll告知parent,关于子view消费的部分和子view没有消费的部分,parent可对未消费部分进行处理。
CoordnatorLayout实现了NestedScrollingParent
NestedScrollView实现了NestedScrollingParent和NestedScrollingChild
RecyclerView实现了NestedScrollingChild
以下是嵌套滑动child和parent对应关系
1534679979957.jpg简单的回顾了NestedScrolling滑动机制,现在开始进入正题。
当我们的触摸点在RecyclerView或在分类轴上方广告父容器时,配合他们滑动的parent是谁呢?我们可以自定义CoordinatorLayout和NestedScrollView,覆写 public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int type) 方法,打印target参数,发现只有CoordinatorLayout的onNestedPreScroll有打印日志,并且target的输出是RecyclerView或NestedScrollView,所以配合RecyclerView和NestedScrollView嵌套滑动的parent是CoordinatorLayout,除此之外,还发现调用super.onNestedPreScroll(target, dx, dy, consumed, type)方法,在AppbarLayout折叠或下滑时consumed[1]=0。也就是AppBarLayout折叠或下滑的时候,CoordinatorLayout告知子VIew,父View在Y轴距离上没有消耗,这就是滑动冲突的原因。
比如,appbarLayout折叠时,滑动触摸点在RecycleView上,进行上滑操作,只能滑动列表,分类轴上方广告容器滑动不了,这是由于parent告知RecyclerView它在y轴上消耗0,将所有y轴距离都交给了RecycleView来消耗。
既然知道了配合滑动的parent是CoordinatorLayout以及滑动冲突原因,那么就可以在onNestedPreScroll方法中按业务制定滑动规则,来分配dy的消耗。这里的处理规则可以看以下代码中的注释。
注:分类轴上方广告容器(代码中都称banner)。
//MyCoordinatorLayout.java
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int type) {
Log.e(TAG,"target:"+target);
// Log.e(TAG,"super before dy:"+dy);
super.onNestedPreScroll(target, dx, dy, consumed, type);//必须放在前面调用,后面对父容器消耗的dy进行处理,来解决与子元素的滑动冲突
// Log.e(TAG,"super after dy:"+dy+",dx:"+dx+",consumed[1]:"+consumed[1]);
if(consumed[1] == 0 && !isTouchPointInBannerView){
//AppbarLayout折叠或下滑时,consumed[1]=0,并且触摸点不在Banner上
nsvMaxOffsetY = getNestedScrollViewMaxOffset();
if(nsvMaxOffsetY>0 && nestedScrollView !=null){
//NestedScrollView存在最大滑出屏幕的偏移量时,需要对dy消耗进行处理
if(dy>0){
//上滑
if(nsvMaxOffsetY == nestedScrollView.getScrollY()){
//banner隐藏时,不消耗dy,交给列表,列表滑动dy
consumed[1] = 0;
}else{
//banner可见
//触摸点在RecyclerView上时,设置父容器消耗dy,不让列表滑动,同时滚动NestedScrollView
consumed[1] = dy;
nestedScrollViewScrollBy(0,dy);
}
}else{
//下滑
if(nestedScrollView.getScrollY() == nsvMaxOffsetY){
//banner隐藏
if(isTop()){
//列表第一个item可见,设置父容器消耗dy,不让列表滑动,同时滚动NestedScrollView
consumed[1] = dy;
nestedScrollViewScrollBy(0,dy);
}else{
//列表第一个item不可见,父容器不消耗dy,交给RecyclerView消耗dy
consumed[1] = 0;
}
}else if(nestedScrollView.getScrollY()>0){
//banner可见未完全展开
//触摸点在RecyclerView上时,设置父容器消耗dy,不让列表滑动,同时滚动NestedScrollView
consumed[1] = dy;
nestedScrollViewScrollBy(0,dy);
}
}
}
}
}
那么多层嵌套滑动冲突的解决到这里就结束了。源码地址:https://github.com/lmz14/NestedScrollDemo
网友评论