CoordinatorLayout自定義Behavior和原始碼分析

weixin_34236869發表於2017-12-02

前沿

Behavior是Android新出的Design庫裡新增的佈局概念。Behavior只有是CoordinatorLayout的直接子View才有意義。可以為任何View新增一個Behavior。Behavior是一系列回撥。讓你有機會以非侵入的為View新增動態的依賴佈局,和處理父佈局(CoordinatorLayout)滑動手勢的機會。

一、 某個View需要監聽另一個View的狀態(比如:位置、大小、顯示狀體)

一個View監聽另一個View,只需要在自定義Behavior重寫:layoutDependsOn/onDependentViewChanged方法

自定義Behavior
public class CustomBehavior extends CoordinatorLayout.Behavior {

    public CustomBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * 用來決定需要監聽哪些控制元件或者容器的狀態(1.知道監聽誰;2.什麼狀態改變)
     * CoordinatorLayout parent ,父容器
     * View child, 子控制元件---需要監聽dependency這個view的檢視們---觀察者
     * View dependency,你要監聽的那個View
     */
    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        //還可以根據ID或者TAG來判斷是哪一個TextView
        return dependency instanceof TextView || super.layoutDependsOn(parent, child, dependency);
    }

    /**
     * 當被監聽的view發生改變的時候回撥
     * 可以在此方法裡面做一些響應的聯動動畫等效果。
     */
    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        //獲取被監聽的view的狀態---垂直方向位置
        int offset = dependency.getTop() - child.getTop();
        //讓child進行平移
        ViewCompat.offsetTopAndBottom(child, offset);
        child.animate().rotation(child.getTop()*90);
        return true;
    }
}

xml檔案
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/tv1"
        android:tag="tv1"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:background="#ff0"
        android:layout_gravity="left|top"
        android:text="被觀察--dependent" />
    <ImageView
        app:layout_behavior="com.example.mycustombehavior.CustomBehavior"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:layout_gravity="right|top"
        android:background="#f00"
        android:src="@mipmap/ic_launcher"
        android:text="觀察者" />

</android.support.design.widget.CoordinatorLayout>

MainActivity中設定一個控制元件的點選事件
@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.tv1).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ViewCompat.offsetTopAndBottom(v, 10);
            }
        });
    }

效果圖:

2918620-6dde75d7a791b86e.gif

tips:

1、自定義Behavior一定要重寫構造方法不然就會報錯(因為在CoordinatorLayout裡利用反射去獲取這個Behavior的時候就是拿的這個構造。)

2、layoutDependsOn(parent,child,dependency) : 用來決定需要監聽哪些控制元件或者容器的狀態,parent父容器;child子控制元件也是觀察者;dependency監聽的View也是被觀察者

3、onDependentViewChanged(parent,child,dependecy) : 當被監聽的View發生改變的時候回撥,可以在此方法裡面做一些相應的聯動效果

二、某個View需要監聽CoordinatorLayout裡面所有控制元件的滑動狀態。

某個View需要監聽CoordinatorLayout裡面所有控制元件的滑動效果需要重寫:onStartNestedScroll/onNestedScroll,或則onNestedPreScroll方法,

==注意==:能被CoordinatorLayout捕獲到的滑動狀態的控制元件只有:recyclerView/NestScrollView/ViewPager

自定義Behavior
public class SyncScrollBehavior extends CoordinatorLayout.Behavior<View>{

    public SyncScrollBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes) {
        return (axes == ViewCompat.SCROLL_AXIS_VERTICAL) || super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, axes);
    }

    @Override
    public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dx, int dy, @NonNull int[] consumed) {
        int scrollY = target.getScrollY();
        child.setScrollY(scrollY);
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
    }


    /*@Override
    public boolean onNestedFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, float velocityX, float velocityY, boolean consumed) {
        // 快速滑動的慣性移動(鬆開手指後還會有滑動效果)
        ((NestedScrollView)child).fling((int) velocityY);
        return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
    }*/
}

xml檔案
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <android.support.v4.widget.NestedScrollView
        android:layout_width="80dp"
        android:layout_height="match_parent"
        android:layout_gravity="left" >

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:orientation="vertical" >

            <TextView
                android:layout_width="80dp"
                android:layout_height="180dp"
                android:layout_gravity="left|top"
                android:background="#ff0"
                android:text="被觀察--dependent" />

            <TextView
                android:layout_width="80dp"
                android:layout_height="180dp"
                android:layout_gravity="left|top"
                android:background="#ff0"
                android:text="被觀察--dependent" />

            ...
        </LinearLayout>
    </android.support.v4.widget.NestedScrollView>

    <android.support.v4.widget.NestedScrollView
        android:layout_width="80dp"
        android:layout_height="match_parent"
        app:layout_behavior="com.example.mycustombehavior2.SyncScrollBehavior"
        android:layout_gravity="right" >

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:orientation="vertical" >

            <TextView
                android:layout_width="80dp"
                android:layout_height="180dp"
                android:layout_gravity="left|top"
                android:background="#ff0"
                android:text="被觀察--dependent" />

            <TextView
                android:layout_width="80dp"
                android:layout_height="180dp"
                android:layout_gravity="left|top"
                android:background="#ff0"
                android:text="被觀察--dependent" />

            ...
        </LinearLayout>
    </android.support.v4.widget.NestedScrollView>

</android.support.design.widget.CoordinatorLayout>

以上程式碼有點問題,就是當快速滑動的時候會出現錯位,原因是慣性引起。

如下圖:

2918620-47971fbee39b6ba0.gif
有問題

所以解決問題有兩種:

① 、需要在onNestedFling方法呼叫RecyclerView的fling方法(上面自定義Behavior裡的註釋的程式碼)
②、上面程式碼自定義Behavio的兩個方法都是過時的,因此使用不是過時的就可以了

public class SyncScrollBehavior extends CoordinatorLayout.Behavior{

    public SyncScrollBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                                       @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {

        return (axes == ViewCompat.SCROLL_AXIS_VERTICAL) || super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, axes, type);
    }

    @Override
    public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
        int scrollY = target.getScrollY();
        child.setScrollY(scrollY);
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
    }

}

最終效果:

2918620-df385baa01429bc8.gif
完美圖

三、Behavior原始碼分析

在CoordinatorLayout原始碼parseBehavior函式中,此函式是在初始化CoordinatorLayout.LayoutParams的時候呼叫

 public static class LayoutParams extends ViewGroup.MarginLayoutParams {
    Behavior mBehavior;
    ...
    LayoutParams(Context context, AttributeSet attrs) {
        super(context, attrs);

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

       ...

        mBehaviorResolved = a.hasValue(
                R.styleable.CoordinatorLayout_LayoutParams_layout_behavior);
        if (mBehaviorResolved) {
            mBehavior = parseBehavior(context, attrs, a.getString(
                    R.styleable.CoordinatorLayout_LayoutParams_layout_behavior));
        }

        a.recycle();
    }
}
static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
    ...
        if (c == null) {
            final Class<Behavior> clazz = (Class<Behavior>) Class.forName(fullName, true,
                    context.getClassLoader());
            c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
            c.setAccessible(true);
            constructors.put(fullName, c);
        }
        return c.newInstance(context, attrs);
   ...
}

其中CONSTRUCTOR_PARAMS

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

由此可見必須要在子類中重寫構造方法

根據上面文中的一和二來檢視原始碼.

首先一中的layoutDependsOn和onDependentViewChanged是在CoordinatorLayout類onChildViewsChanged方法中進行呼叫

final void onChildViewsChanged(@DispatchChangeEvent final int type) {
    ......
    for (int i = 0; i < childCount; i++) {
        final View child = mDependencySortedChildren.get(i);
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        if (type == EVENT_PRE_DRAW && child.getVisibility() == View.GONE) {
            // Do not try to update GONE child views in pre draw updates.
            continue;
        }

        ......

        // 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();

            if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
                    // If this is from a pre-draw and we have already been changed
                    // from a nested scroll, skip the dispatch and reset the flag
                    checkLp.resetChangedAfterNestedScroll();
                    continue;
                }

                final boolean handled;
                switch (type) {
                    case EVENT_VIEW_REMOVED:
                        // EVENT_VIEW_REMOVED means that we need to dispatch
                        // onDependentViewRemoved() instead
                        b.onDependentViewRemoved(this, checkChild, child);
                        handled = true;
                        break;
                    default:
                        // Otherwise we dispatch onDependentViewChanged()
                        handled = b.onDependentViewChanged(this, checkChild, child);
                        break;
                }

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

從原始碼中看出當Child為GONE時將不會執行後面的onDependenViewChanged等方法

通過檢視onChildViewsChanged方法的呼叫的源頭可以看出最終也是由onNestedPreScroll和onNestedScroll呼叫

@Override  
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,  
        int dxUnconsumed, int dyUnconsumed) {  
    final int childCount = getChildCount();  
    boolean accepted = false;  
    for (int i = 0; i < childCount; i++) {  
        final View view = getChildAt(i);  
        if (view.getVisibility() == GONE) {//如果為GONE則跳過  
            // If the child is GONE, skip...  
            continue;  
        }  
  
        final LayoutParams lp = (LayoutParams) view.getLayoutParams();  
        if (!lp.isNestedScrollAccepted()) {  
            continue;  
        }  
        final Behavior viewBehavior = lp.getBehavior();  
        if (viewBehavior != null) {  
            viewBehavior.onNestedScroll(this, view, target, dxConsumed, dyConsumed,  
                    dxUnconsumed, dyUnconsumed);  
            accepted = true;//當呼叫了Behavior的onNestedScroll方法也將會呼叫onChildViewChanged方法  
        }  
    }  
  
    if (accepted) {  
        onChildViewsChanged(EVENT_NESTED_SCROLL);//呼叫了上面提到的onChildViewChanged方法進而呼叫layoutDependsOn和onDependentViewChanged  
    }  
}  

當時onStartNestedScroll方法並沒有呼叫,當然這也和在onStartNestedScroll方法進行判斷滑動View是豎直還是水平有關

@Override  
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);  
        if (view.getVisibility() == View.GONE) {  
            // If it's GONE, don't dispatch  
            continue;  
        }  
        final LayoutParams lp = (LayoutParams) view.getLayoutParams();  
        final Behavior viewBehavior = lp.getBehavior();  
        if (viewBehavior != null) {  
            final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,  
                    nestedScrollAxes);  
            handled |= accepted;  
  
            lp.acceptNestedScroll(accepted);  
        } else {  
            lp.acceptNestedScroll(false);  
        }  
    }  
    return handled;  
} 

至此簡易的原始碼介紹結束

相關文章