??CoordinatorLayout作為頂層布局與NestedScrollView配合使用够挂,可以用來協(xié)調(diào)子View的嵌套滑動哲身。但是好港,如果要在CoordinatorLayout的外層嵌套XRefreshView下拉刷新控件影所,并且NestedScrollView嵌套多種可滑動的控件,這時候就出現(xiàn)了滑動沖突赠制,具體嵌套結(jié)構(gòu)如下所示:
XML布局結(jié)構(gòu)大致如下:
<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
??我們知道事件分發(fā)是從上往下傳遞的(從Activity->Window->DecorView->......View),所以我們是不是可以在需要下拉刷新的時候牛哺,將事件攔截,交給下拉刷新控件處理春哨,其他時候都不攔截事件荆隘。那么我們要選擇用內(nèi)部攔截法還是外部攔截法來處理滑動沖突呢恩伺?
??外部攔截法赴背,那我們就需要在XRefreshView的onInterceptTouchEvent進行處理。XRefreshView是一個開源控件晶渠,從源碼中凰荚,我們可以看到XRefreshView并未覆寫onInterceptTouchEvent,而是覆寫了dispatchTouchEvent方法褒脯,并在方法中進行了一系列復(fù)雜的事件分發(fā)便瑟。若使用外部攔截法來處理,就需要理清XRefreshView原有的事件分發(fā)規(guī)則番川,根據(jù)我們的實際需求對源碼進行修改到涂,對于父容器需要的事件進行攔截。
??內(nèi)部攔截法颁督,自定義CoordinatorLayout践啄,覆寫dispatchTouchEvent方法,通過調(diào)用requestDisallowInterceptTouchEvent方法來干擾父容器對事件的攔截沉御。從XRefreshView的源碼中我們發(fā)現(xiàn)該控件提供了一個disallowInterceptTouchEvent方法屿讽,從方法注釋可知,子View需要事件的時候吠裆,可設(shè)置為true伐谈,不允許父容器攔截觸摸事件。
XRefreshView.java
/**
//XRefreshView.java
* if child need the touch event,pass true
*/
public void disallowInterceptTouchEvent(boolean isIntercept) {
mIsIntercept = isIntercept;
}
??相比兩種方法试疙,這里內(nèi)部攔截法更簡單诵棵,所以最終選擇了內(nèi)部攔截法。
??觸發(fā)下拉刷新的時機:垂直向下滑動祝旷,appBarLayout完全展開狀態(tài)履澳,允許XRefreshView攔截事件柱徙。
//MyCoordinatorLayout.java
private int mLastX,mLastY;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//內(nèi)部攔截法
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展開狀態(tài)奇昙,列表第一個item可見护侮,將事件交還給父容器XRefreshView
// xRefreshView.disallowInterceptTouchEvent(false);
// }
// 注意:若此時item位置不是第一個可見時,不能下拉刷新储耐,若不需要item位置第一個可見時就可以下拉刷新羊初,可以把isTop判斷去掉,若下所示:
if(observed!=null && observed.getAppBarLayoutStatus() == 1 && deltaY>0 && xRefreshView!=null){
//垂直向下滑動什湘,appBarLayout展開狀態(tài)是偷,將事件交還給父容器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無法顯示的問題箱吕。網(wǎng)上的解決方案有2種荣病,一是重寫ViewPager的onMeasure方法,遍歷每個頁面闸餐,獲取最高的頁面高度來設(shè)置ViewPager的高度饱亮,二是給NestedScrollView設(shè)置android:fillViewport="true"允許 NestedScrollView中的組件去充滿它。對于第一種方案舍沙,存在一個問題近上,每個頁面的內(nèi)容高度都一樣,并且滑動其中一個頁面的列表時拂铡,其他頁面的列表也會滑動壹无,所以這里采用了方案二。
??在SmartTablayout上方我們設(shè)置了一個LinearLayout感帅,這個LinearLayout可以用來作為廣告布局的一個父容器斗锭。當(dāng)滑動NestedScrollView時,這個LinearLayout需要可以往屏幕外滑出留瞳,直到smartTabLayout保持在置頂位置拒迅。那要怎么讓LinerLayout可以滑出屏幕直至不可見呢?答案是增大可滑動的空間她倘,在原來內(nèi)容高度的基礎(chǔ)上增加廣告布局父容器的高度璧微。我們知道,NestedScrollView不能直接嵌套多個布局硬梁,只能有一個直接子類(允許直接嵌套的子類有RecyclerVIew前硫、ViewPager、LinearLayout)荧止,這里選擇在線性布局里嵌套多個布局屹电,并且自定義這個直接子類阶剑,重新去計算它的高度(在系統(tǒng)計算的高度上,加上允許滑出屏幕的高度)危号,具體如下所示:
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內(nèi)嵌套多種控件)
??在進入正題之前,我們先來簡單回顧下NestedScrolling滑動機制外莲。關(guān)鍵接口NestedScrollingParent和NestedScrollingChild猪半,以及他們所對應(yīng)的Helper(NestedScrollingParentHelper、NestedScrollingChildHelper)偷线。
??嵌套滑動首先由子view觸發(fā)調(diào)用startNestedScroll方法磨确,尋找能夠配合子View嵌套滑動parent。在子View處理滑動事件之前調(diào)用dispatchNestedPreScroll声邦,詢問parent是否需要在子View之前處理滑動乏奥,通過回調(diào)onNestedPreScroll方法告知parent當(dāng)前滑動的距離,若父類有消耗滑動距離亥曹,可通過onNestedPreScroll方法的consumed這個輸出參數(shù)來告知子View邓了。子View處理完滑動事件后調(diào)用dispatchNestedScroll方法通過回調(diào)onNestedScroll告知parent,關(guān)于子view消費的部分和子view沒有消費的部分歇式,parent可對未消費部分進行處理驶悟。
CoordnatorLayout實現(xiàn)了NestedScrollingParent
NestedScrollView實現(xiàn)了NestedScrollingParent和NestedScrollingChild
RecyclerView實現(xiàn)了NestedScrollingChild
以下是嵌套滑動child和parent對應(yīng)關(guān)系
??簡單的回顧了NestedScrolling滑動機制胡野,現(xiàn)在開始進入正題材失。
??當(dāng)我們的觸摸點在RecyclerView或在分類軸上方廣告父容器時,配合他們滑動的parent是誰呢硫豆?我們可以自定義CoordinatorLayout和NestedScrollView龙巨,覆寫 public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int type) 方法,打印target參數(shù)熊响,發(fā)現(xiàn)只有CoordinatorLayout的onNestedPreScroll有打印日志旨别,并且target的輸出是RecyclerView或NestedScrollView,所以配合RecyclerView和NestedScrollView嵌套滑動的parent是CoordinatorLayout汗茄,除此之外秸弛,還發(fā)現(xiàn)調(diào)用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方法中按業(yè)務(wù)制定滑動規(guī)則瞭空,來分配dy的消耗。這里的處理規(guī)則可以看以下代碼中的注釋疗我。
??注:分類軸上方廣告容器(代碼中都稱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);//必須放在前面調(diào)用,后面對父容器消耗的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上時剖膳,設(shè)置父容器消耗dy,不讓列表滑動魏颓,同時滾動NestedScrollView
consumed[1] = dy;
nestedScrollViewScrollBy(0,dy);
}
}else{
//下滑
if(nestedScrollView.getScrollY() == nsvMaxOffsetY){
//banner隱藏
if(isTop()){
//列表第一個item可見,設(shè)置父容器消耗dy,不讓列表滑動吱晒,同時滾動NestedScrollView
consumed[1] = dy;
nestedScrollViewScrollBy(0,dy);
}else{
//列表第一個item不可見甸饱,父容器不消耗dy,交給RecyclerView消耗dy
consumed[1] = 0;
}
}else if(nestedScrollView.getScrollY()>0){
//banner可見未完全展開
//觸摸點在RecyclerView上時仑濒,設(shè)置父容器消耗dy,不讓列表滑動叹话,同時滾動NestedScrollView
consumed[1] = dy;
nestedScrollViewScrollBy(0,dy);
}
}
}
}
}
那么多層嵌套滑動沖突的解決到這里就結(jié)束了。源碼地址:https://github.com/lmz14/NestedScrollDemo
版權(quán)聲明: 轉(zhuǎn)載請注明出處