Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 深入理解CoordinatorLayout.Behavior

深入理解CoordinatorLayout.Behavior

編輯:關於Android編程

Behavior

要研究的幾個問題

一、Behavior是什麼?為什麼要用Behavior?
二、怎麼使用Behavior?
三、從源碼角度看為什麼要這麼使用Behavior?

一、Behavior是什麼?為什麼要用Behavior?

CoordinatorLayout是android support design推出的新布局,主要用於作為視圖根布局以及協調子控件的行為,而Behavior就是用於直接子控件來協調自身CoordinatorLayout以及和其他子控件的關系,使用Behavior的控件必須是直接從屬於CoordinatorLayout。

在傳統的事件分發流程中,在子控件處理事件過程中,父控件是可以進行攔截的,但一旦父控件進行攔截,那麼這次事件只能由父控件處理,而不能再由子控件處理了。

在android5.0之後新的嵌套滑動機制中,引入了:NestScrollChildNestedScrollingParent兩個接口,用於協調子父控件滑動狀態,而CoordinatorLayout實現了NestedScrollingParent接口,在實現了NestScrollChild這個接口的子控件在滑動時會調用NestedScrollingParent接口的相關方法,將事件發給父控件,由父控件決定是否消費當前事件,在CoordinatorLayout實現的NestedScrollingParent相關方法中會調用Behavior內部的方法。

我們實現Behavior的方法,就可以嵌入整個CoordinatorLayout所構造的嵌套滑動機制中,可以獲取到兩個方面的內容:

1、某個view監聽另一個view的狀態變化,例如大小、位置、顯示狀態等
需要重寫layoutDependsOnonDependentViewChanged方法

2、某個view監聽CoordinatorLayout內NestedScrollingChild的接口實現類的滑動狀態
重寫onStartNestedScrollonNestedPreScroll方法。注意:是監聽實現了NestedScrollingChild的接口實現類的滑動狀態,這就可以解釋為什麼不能用ScrollView而用NestScrollView來滑動了。

二、怎麼使用Behavior?

我們先看下Behavior最常見的幾個方法,Behavior還有其他比如onMeasureChild、onLayoutChild等一些方法,列舉的這幾個方法平時還是比較常見的,知道常見方法的使用後,在研究下其他方法,思路還是相通的。

public static abstract class Behavior {
//指定Behavior關注的滑動方向
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
                V child, View directTargetChild, View target, int nestedScrollAxes) {
            return false;
        }
//用來監聽滑動狀態,對象消費滾動距離前回調
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target,
                int dx, int dy, int[] consumed) {
            // TODO
        }
//確定子視圖與同級視圖的依賴
    @Override 
     public boolean layoutDependsOn(CoordinatorLayout parent, View 
child, View dependency) {
        return Build.VERSION.SDK_INT >= 11 && dependency instanceof Snackbar.SnackbarLayout;
}
 //依賴布局變化時調用
//If the Behavior changes the child view's size or position, 
//it should return true. The default implementation returns false
    public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
            return false;
        }
    @Override
      public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float
      velocityY, boolean consumed) {
    //快速滑動
       return super.onNestedFling(coordinatorLayout, child,target,velocityX, velocityY, consumed);
}
//所有Behavior能在子View之前收到CoordinatorLayout的所有觸摸事件
    @Override
    public boolean onInterceptTouchEvent(CoordinatorLayout parent,View child, MotionEvent ev) { 
      return super.onInterceptTouchEvent(parent, child, ev);
    }
  @Override
  public boolean onTouchEvent(CoordinatorLayout parent, View child, MotionEvent ev) { 
      return super.onTouchEvent(parent, child, ev);
    }
}

1、某個view監聽另一個view的狀態變化

這樣的效果最常見的如之後導航欄那樣:

vcC4z9TKvtL+stg=" src="/uploadfile/Collfiles/20160822/20160822094152464.gif" title="\" />

前面已經說了,如果要監聽另一個view的狀態變化,需要重寫layoutDependsOnonDependentViewChanged方法,看下具體實現:
layout:




    

        
    

        
            

                
                
                
                
                
                
                
                
                

            
        

    
        
    

attrs:

   
        
    

MyCustomBehavior.java:

public class MyCustomBehavior extends CoordinatorLayout.Behavior{

    private int id;
    public MyCustomBehavior(Context context, AttributeSet attrs) {
        super(context,attrs);
        TypedArray typedArray = context.getResources().obtainAttributes(attrs, R.styleable.MyCustomStyle);
        id = typedArray.getResourceId(R.styleable.MyCustomStyle_anchor_id, -1);
        typedArray.recycle();
    }

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {

//        return dependency instanceof AppBarLayout;
        return dependency.getId() == id;
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {

        child.setTranslationY(-dependency.getTop());
        return true;
    }
}

重點關注幾點:
首先,我們必須重寫兩個參數的構造方法,因為通過反射實例化的時候就是用的這個構造方法,在這個構造方法中我們也可以獲取一些東西,比如我們的依賴控件ID。
之後layoutDependsOn方法我們來決定要依賴哪個view,如果我們知道要依賴的控件,可以直接寫:

return dependency instanceof AppBarLayout

而如果我們不知道,也可以由外部傳入,在構造方法中獲取資源ID來進行判斷,這樣具有更高的靈活性:

return dependency.getId() == id

我們看下在CoordinatorLayout中兩個方法的調用過程:

class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
        @Override
        public boolean onPreDraw() {
            dispatchOnDependentViewChanged(false);
            return true;
        }
    }

void dispatchOnDependentViewChanged(final boolean fromNestedScroll) {
        final int layoutDirection = ViewCompat.getLayoutDirection(this);
        ...
            // Update any behavior-dependent views for the change
            for (int j = i + 1; j < childCount; j++) {
                final View checkChild = mDependencySortedChildren.get(j);
                final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
                final Behavior b = checkLp.getBehavior();
              //如果Behavior不為null,layoutDependsOn方法返回true
                if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                    if (!fromNestedScroll && checkLp.getChangedAfterNestedScroll()) {
                        // If this is not from a nested scroll and we have already been changed
                        // from a nested scroll, skip the dispatch and reset the flag
                        checkLp.resetChangedAfterNestedScroll();
                        continue;
                    }
                  //調用onDependentViewChanged方法
                    final boolean handled = b.onDependentViewChanged(this, checkChild, child);
                 ...
            }
        }
    }

從調用上來看,在CoordinatorLayout內部的任何子view均可產生依賴關系。

2、某個view監聽CoordinatorLayout內NestedScrollingChild的接口實現類的滑動狀態

如前所說,重寫onStartNestedScrollonNestedPreScroll方法。它可以監聽實現了NestedScrollingChild的接口實現類的滑動狀態

如果用WebView來滾動的,結果預期要隱藏和顯示的appbar沒有反應,在外層加上NestScrollView就解決了問題,這是因為WebView沒有實現NestedScrollingChild接口造成的,因為滑動控件的滑動狀態是通過NestedScrollingChild接口方法處理中來調用NestedScrollingParent接口方法來實現。
實現上面的效果我們還可以用重寫onStartNestedScrollonNestedPreScroll來實現。來看看吧:

public class MyCustomBehavior extends CoordinatorLayout.Behavior<view>{

    private boolean isAnimate;
    public MyCustomBehavior(Context context, AttributeSet attrs) {
        super(context,attrs);
    }

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL)!=-1;//判斷是否為垂直滾動
    }

    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
        //super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);

        if (dy>0 &&!isAnimate && child.getTranslationY()<child.getheight()){ 0="" else="" if="" isanimate="">0){
            child.setVisibility(View.VISIBLE);
            if (child.getTranslationY()+dy<0){
                child.setTranslationY(0);
            }else {
                child.setTranslationY(child.getTranslationY()+dy);
            }
        }
    }


    @Override
    public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target) {
        //super.onStopNestedScroll(coordinatorLayout, child, target);
            if (child.getTranslationY()<child.getheight() private="" void="" final="" view="" int="" viewpropertyanimator="" animator="" scrolly="" new="" override="" public="" isanimate="true;" if="" pre="">

用這個來實現的話,需要注意的是滾動控件必須實現NestedScrollingChild接口,而沒有實現該接口且不調用 dispatchNestedScroll相關接口的滾動控件如ScrollView、WebView、ListView是沒有作用的。

三、從源碼角度看為什麼要這麼使用Behavior

我們從Behavior獲取實例化開始看,看CoordinatorLayout.LayoutParams源碼:

 LayoutParams(Context context, AttributeSet attrs) {
            super(context, attrs);

            final TypedArray a = context.obtainStyledAttributes(attrs,
                    R.styleable.CoordinatorLayout_LayoutParams);

            this.gravity = a.getInteger(
                    R.styleable.CoordinatorLayout_LayoutParams_android_layout_gravity,
                    Gravity.NO_GRAVITY);
            mAnchorId = a.getResourceId(R.styleable.CoordinatorLayout_LayoutParams_layout_anchor,
                    View.NO_ID);
            this.anchorGravity = a.getInteger(
                    R.styleable.CoordinatorLayout_LayoutParams_layout_anchorGravity,
                    Gravity.NO_GRAVITY);

            this.keyline = a.getInteger(R.styleable.CoordinatorLayout_LayoutParams_layout_keyline,
                    -1);

            mBehaviorResolved = a.hasValue(
                    R.styleable.CoordinatorLayout_LayoutParams_layout_behavior);
            if (mBehaviorResolved) {
              //在這裡解析獲取Behavior
                mBehavior = parseBehavior(context, attrs, a.getString(
                        R.styleable.CoordinatorLayout_LayoutParams_layout_behavior));
            }

            a.recycle();
        }

接著來看看具體是怎麼獲取到Behavior的:

 static final Class[] CONSTRUCTOR_PARAMS = new Class[] {
            Context.class,
            AttributeSet.class
    };

static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
        if (TextUtils.isEmpty(name)) {
            return null;
        }

        final String fullName;
        if (name.startsWith(".")) {
            // Relative to the app package. Prepend the app package name.
            fullName = context.getPackageName() + name;
        } else if (name.indexOf('.') >= 0) {
            // Fully qualified package name.
            fullName = name;
        } else {
            // Assume stock behavior in this package (if we have one)
            fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME)
                    ? (WIDGET_PACKAGE_NAME + '.' + name)
                    : name;
        }
        try {
            Map> constructors = sConstructors.get();
            if (constructors == null) {
                constructors = new HashMap<>();
                sConstructors.set(constructors);
            }
            Constructor c = constructors.get(fullName);
            if (c == null) {
                //這裡通過反射獲取到Behavior
                final Class clazz = (Class) Class.forName(fullName, true,
                        context.getClassLoader());
                //獲取兩個參數的構造方法
                c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
                c.setAccessible(true);
                constructors.put(fullName, c);
            }
          //在這裡實例化
            return c.newInstance(context, attrs);
        } catch (Exception e) {
            throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
        }
    }

這裡就解釋了為什麼我們每次繼承都要寫兩個參數的構造方法了,如果沒有,則會報Caused by: java.lang.NoSuchMethodException: [class android.content.Context, interface android.util.AttributeSet]錯誤。 然後我們看看主要關注的onStartNestedScroll和onNestedPreScroll的調用時機,當實現了NestScrollChild接口的子控件滑動時,會回調CoordinatorLayout中的onStartNestedScroll方法:

 public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        boolean handled = false;

        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            //獲取Behavior 
            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
            //true if the Behavior wishes to accept this nested scroll
            //調用viewBehavior.onStartNestedScroll方法,如果返回true表示希望接受滾動事件
                final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,
                        nestedScrollAxes);
                handled |= accepted;
                lp.acceptNestedScroll(accepted);
            } else {
                lp.acceptNestedScroll(false);
            }
        }
        return handled;
    }

當實現了NestScrollChild接口的子控件滾動時,在消費滾動距離之前把總的滑動距離傳給父布局,即CoordinatorLayout。然後回調onNestedPreScroll方法:

public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        int xConsumed = 0;
        int yConsumed = 0;
        boolean accepted = false;

        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            //遍歷所有子控件 如果不希望接受處理事件  跳出本次循環
            if (!lp.isNestedScrollAccepted()) {
                continue;
            }
            //獲得child view的Behavior
            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                mTempIntPair[0] = mTempIntPair[1] = 0;
                //調用viewBehavior.onNestedPreScroll方法
                viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair);
                //dy大於0是向上滾動 小於0是向下滾動
                xConsumed = dx > 0 ? Math.max(xConsumed, mTempIntPair[0])
                        : Math.min(xConsumed, mTempIntPair[0]);
                yConsumed = dy > 0 ? Math.max(yConsumed, mTempIntPair[1])
                        : Math.min(yConsumed, mTempIntPair[1]);

                accepted = true;
            }
        }
        //consumed:表示父布局要消費的滾動距離,consumed[0]和consumed[1]分別表示父布局在x和y方向上消費的距離
        consumed[0] = xConsumed;
        consumed[1] = yConsumed;

        if (accepted) {
            dispatchOnDependentViewChanged(true);
        }
    }

然後我們來研究layoutDependsOn和onDependentViewChanged的調用時機,看CoordinatorLayout的dispatchOnDependentViewChanged方法:

void dispatchOnDependentViewChanged(final boolean fromNestedScroll) {
        final int layoutDirection = ViewCompat.getLayoutDirection(this);
        final int childCount = mDependencySortedChildren.size();
        for (int i = 0; i < childCount; i++) {
            final View child = mDependencySortedChildren.get(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            // Check child views before for anchor
            for (int j = 0; j < i; j++) {
                final View checkChild = mDependencySortedChildren.get(j);

                if (lp.mAnchorDirectChild == checkChild) {
                    offsetChildToAnchor(child, layoutDirection);
                }
            }

            // Did it change? if not continue
            final Rect oldRect = mTempRect1;
            final Rect newRect = mTempRect2;
            getLastChildRect(child, oldRect);
            getChildRect(child, true, newRect);
            if (oldRect.equals(newRect)) {
                continue;
            }
            recordLastChildRect(child, newRect);

            // Update any behavior-dependent views for the change
            for (int j = i + 1; j < childCount; j++) {
                final View checkChild = mDependencySortedChildren.get(j);
                final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
                final Behavior b = checkLp.getBehavior();
                //behavior不為null同時layoutDependsOn返回了true
                if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                    if (!fromNestedScroll && checkLp.getChangedAfterNestedScroll()) {
                        // If this is not from a nested scroll and we have already been changed
                        // from a nested scroll, skip the dispatch and reset the flag
                        checkLp.resetChangedAfterNestedScroll();
                        continue;
                    }
                    //this:CoordinatorLayout
                    //checkChild:behavior所屬的view
                    //child:依賴的view
                    //true if the Behavior changed the child view's size or position, false otherwise
                    final boolean handled = b.onDependentViewChanged(this, checkChild, child);

                    if (fromNestedScroll) {
                        // If this is from a nested scroll, set the flag so that we may skip
                        // any resulting onPreDraw dispatch (if needed)
                        checkLp.setChangedAfterNestedScroll(handled);
                    }
                }
            }
        }
    }

這段代碼在onNestedScroll、onNestedPreScroll、onNestedFling和OnPreDrawListener.onPreDraw方法中都有調用,判斷依賴控件大小或者位置變化時及時通知behavior,子控件作出相應調整。 這裡把我們主要關心的控件的調用時機大體走讀了一遍,對於為什麼在behavior中調用相關方法可以依賴和監聽其他控件的滑動事件應該有了一定認識,如果關注CoordinatorLayout的實現細節,務必要搞明白NestScrollChild和NestedScrollingParent機制的調用關系,建議查看NestScrollView源碼,這裡給出NestScrollChild和NestedScrollingParent的一些主要方法說明

NestScrollChild

public void setNestedScrollingEnabled(boolean enabled) enabled:true表示view使用嵌套滾動,false表示禁用

public boolean startNestedScroll(int axes) axes:表示滾動的方向如:ViewCompat.SCROLL_AXIS_VERTICAL(垂直方向滾動)和 ViewCompat.SCROLL_AXIS_HORIZONTAL(水平方向滾動) return:true表示本次滾動支持嵌套滾動,false不支持 startNestedScroll表示view開始滾動了,一般是在ACTION_DOWN中調用,如果返回true則表示父布局支持嵌套滾動

public void stopNestedScroll() 在事件結束比如ACTION_UP或者ACTION_CANCLE中調用stopNestedScroll,告訴父布局滾動結束

public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) dxConsumed: 表示view消費了x方向的距離長度 dyConsumed: 表示view消費了y方向的距離長度 dxUnconsumed: 表示滾動產生的x滾動距離還剩下多少沒有消費>dyUnconsumed: 表示滾動產生的y滾動距離還剩下多少沒有消費 offsetInWindow: 表示剩下的距離dxUnconsumed和dyUnconsumed使得view在父布局中的位置偏移了多少 在view消費滾動距離之後,把剩下的滑動距離再次傳給父布局

public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) dx: 表示view本次x方向的滾動的總距離長度 dy: 表示view本次y方向的滾動的總距離長度 consumed: 表示父布局消費的距離,consumed[0]表示x方向,consumed[1]表示y方向 參數offsetInWindow: 表示剩下的距離dxUnconsumed和dyUnconsumed使得view在父布局中的位置偏移了多少 view消費滾動距離之前把總的滑動距離傳給父布局

* public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed)* velocityX:X方向滾動的距離 velocityY:Y方向滾動的距離 consumed:父布局是否消費

public boolean dispatchNestedPreFling(float velocityX, float velocityY) velocityX:X方向滾動的距離 velocityY:Y方向滾動的距離

NestedScrollingParent

public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) child:ViewParent包含觸發嵌套滾動的view的對象 target:觸發嵌套滾動的view (在這裡如果不涉及多層嵌套的話,child和target)是相同的 nestedScrollAxes:就是嵌套滾動的滾動方向了. 當子view的調用NestedScrollingChild的方法startNestedScroll時,會調用該方法 該方法決定了當前控件是否能接收到其內部View(並非是直接子View)滑動時的參數

public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes); 如果onStartNestedScroll方法返回true,之後就會調用該方法.它是讓嵌套滾動在開始滾動之前,讓布局容器(viewGroup)或者它的父類執行一些配置的初始化(React to the successful claiming of a nested scroll operation)

public void onStopNestedScroll(View target) 當子view調用stopNestedScroll時會調用該方法,停止滾動

public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) target:同上 dxConsumed:表示target已經消費的x方向的距離 dyConsumed:表示target已經消費的x方向的距離 dxUnconsumed:表示x方向剩下的滑動距離 dyUnconsumed:表示y方向剩下的滑動距離 當子view調用dispatchNestedScroll方法時,會調用該方法

public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) target:同上 dx:表示target本次滾動產生的x方向的滾動總距離 dy:表示target本次滾動產生的y方向的滾動總距離 consumed:表示父布局要消費的滾動距離,consumed[0]和consumed[1]分別表示父布局在x和y方向上消費的距離. 當子view調用dispatchNestedPreScroll方法是,會調用該方法

調用時機:

子view 父view startNestedScroll onStartNestedScroll、onNestedScrollAccepted dispatchNestedPreScroll onNestedPreScroll dispatchNestedScroll onNestedScroll stopNestedScroll onStopNestedScroll
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved