美文网首页
CoordinatorLayout自定义Behavior和源码分

CoordinatorLayout自定义Behavior和源码分

作者: 咸鱼Jay | 来源:发表于2017-12-02 14:16 被阅读51次

前沿

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);
            }
        });
    }

效果图:

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>

以上代码有点问题,就是当快速滑动的时候会出现错位,原因是惯性引起。

如下图:

有问题

所以解决问题有两种:

① 、需要在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);
    }

}

最终效果:

完美图

三、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;  
} 

至此简易的源码介绍结束

相关文章

网友评论

      本文标题:CoordinatorLayout自定义Behavior和源码分

      本文链接:https://www.haomeiwen.com/subject/oosnbxtx.html