CoordinatorLayout.Behavior

CoordinatorLayout我们可以将它理解为一个超级Fragment,它的布局方式是一层一层叠上去,而且它可以组织子View之间的协作。组织协作的方式需要使用最重要的对象Behavior
Behavior是CoordinatorLayout实现子View之间交互的插件,它可以实现用户的一个或多个交互行为,它们可能包括拖拽、滑动、或者其他一些手势。
我们在使用CoordinatorLayout的时候,在NestedScrollView的xml属性中总是能看到app:layout_behavior="@string/appbar_scrolling_view_behavior"。NestedScrollView要想与AppBarLayout有联动,那么NestedScrollView作为直接的子View,就必须设置这个behavior,当然这个behavior谷歌已经给默认设置好了。

常用的方法(暂不介绍嵌套滑动)

1 . onLayoutChild可以用于子View视图布局的更改,修改behavior默认设置子View的行为。需要调用parent.onLayoutChild

  /**
     *
     * @param parent CoordinatorLayout
     * @param child   子View
     * @param layoutDirection  ViewCompat.LAYOUT_DIRECTION_LTR(水平布局从左到右)
     *                         ViewCompat.LAYOUT_DIRECTION_RTL(水平布局从右到左)
     * @return  false表示不改变,true改变View的视图
     */
    @Override
    public boolean onLayoutChild(CoordinatorLayout parent, ImageView child, int layoutDirection) {
        return super.onLayoutChild(parent, child, layoutDirection);
    }



2 . layoutDependsOn View的依赖关系在这里设置

/**
     * 表示是否给应用Behavior的View指定一个依赖的布局,一般当依赖的View布局发生变化时
     * 不管被被依赖View的顺序怎样,被依赖的View也会重新布局
     * @param parent CoordinatorLayout 
     * @param child 绑定behavior 的View
     * @param dependency   依赖的view
     * @return 如果child是依赖的指定的View 返回true,否则返回false
     */
    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        return super.layoutDependsOn(parent, child, dependency);
    }


3 . onDependentViewChanged

/**
     * 当依赖的视图状态位置、大小发生变化时,就会调用这个方法
     * @param parent
     * @param child
     * @param dependency
     * @return
     */
    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        return super.onDependentViewChanged(parent, child, dependency);
    }

只是介绍这几个方法,可能我们还不太清楚,所以直接做几个练习,就能看到效果了。
我这里不准备做练习了,练习可以自己试,我这里要简单说一说源码里面这几个方法怎么调用的(我的水平只能简单说一说/(ㄒoㄒ)/~~)
我们一般会在XML中直接定义Behavior,我们在代码中来看看这个Behavior是怎么解析的。

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

    ...省略代码

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

    ...省略代码

    //通过检测app:behavior="xxx",获取路径。所以在定义behavior的时候,一定要写有
    //两个参数的构造方法
    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 (TextUtils.isEmpty(name)) {
        return null;
    }

    final String fullName;
    if (name.startsWith(".")) {
        //相对路径
        fullName = context.getPackageName() + name;
    } else if (name.indexOf('.') >= 0) {
        //全限定名
        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<String, Constructor<Behavior>> constructors = sConstructors.get();
        if (constructors == null) {
            constructors = new HashMap<>();
            sConstructors.set(constructors);
        }
        Constructor<Behavior> c = constructors.get(fullName);
        //通过反射来新建实例
        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);
    } catch (Exception e) {
        throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
    }
}


在xml里面写的话,是在inflate的时候对Behavior赋值的
以注解方式写的话,是在onMeasure内赋值的。(后面会说为什么)


为View配置了Behavior,那么我们接着来看View之间是怎么依赖的。首先在LayoutParams中有关依赖的代码如下:

boolean dependsOn(CoordinatorLayout parent, View child, View dependency) {
    return dependency == mAnchorDirectChild|| (mBehavior != null && mBehavior.layoutDependsOn(parent, child, dependency));
}


返回两种结果,满足一种就是依赖的关系。一种是设置anchor ,另一种就是View的Behavior对另一个View有依赖。


如何处理这种依赖关系呢?既然Behavior能够监测另一个View的变化状况,那么肯定会有重新测量等操作。所以我们来看下面的代码

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        prepareChildren();
        ensurePreDrawListener();
        ...省略代码
    }

点进去prepareChildren();方法,看看里面写了什么…


private final List<View> mDependencySortedChildren = new ArrayList<View>();

//根据依赖关系对child进行排序
 private void prepareChildren() {
        mDependencySortedChildren.clear();
        for (int i = 0, count = getChildCount(); i < count; i++) {
            final View child = getChildAt(i);
            final LayoutParams lp = getResolvedLayoutParams(child);
            lp.findAnchorView(this, child);

            mDependencySortedChildren.add(child);
        }
      //排序,按依赖关系排序,被依赖的View排在前面,保证被依赖的View先被测量绘制
        selectionSort(mDependencySortedChildren, mLayoutDependencyComparator);
    }


可以看到mDependencySortedChildren这个集合按照依赖关系将View存储了起来,至于为什么要排序,这个有性能上的考虑。getResolvedLayoutParams(child)这里有判断和解析注解的:

LayoutParams getResolvedLayoutParams(View child) {
        final LayoutParams result = (LayoutParams) child.getLayoutParams();
        //XML中如果定义了Behavior,那么result.mBehaviorResolved = true;
        if (!result.mBehaviorResolved) {
            Class<?> childClass = child.getClass();
            DefaultBehavior defaultBehavior = null;
            while (childClass != null &&
                    (defaultBehavior = childClass.getAnnotation(DefaultBehavior.class)) == null) {
                childClass = childClass.getSuperclass();
            }
            if (defaultBehavior != null) {
                try {
                    result.setBehavior(defaultBehavior.value().newInstance());
                } catch (Exception e) {
                    Log.e(TAG, "Default behavior class " + defaultBehavior.value().getName() +
                            " could not be instantiated. Did you forget a default constructor?", e);
                }
            }
            result.mBehaviorResolved = true;
        }
        return result;
    }

依赖的集合也有了,那么接下来,就要添加各种监听了,监听绘制过程,监听View的层级变化了,所以在ensurePreDrawListener方法中先判断是否存在依赖关系,如果存在,直接注册相关的监听。

//添加或者删除绘制前的监听器 
    void ensurePreDrawListener() {
        boolean hasDependencies = false;
        final int childCount = getChildCount();
        //判断下CoordinatorLayout的子view是否存在依赖关系
        //如果存在的话就hasDependencies为true
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            if (hasDependencies(child)) {
                hasDependencies = true;
                break;
            }
        }

        if (hasDependencies != mNeedsPreDrawListener) {
            if (hasDependencies) {
                addPreDrawListener();
            } else {
                removePreDrawListener();
            }
        }
    }

    ...省略代码

void addPreDrawListener() {
        if (mIsAttachedToWindow) {
            // Add the listener
            if (mOnPreDrawListener == null) {
                mOnPreDrawListener = new OnPreDrawListener();
            }
            final ViewTreeObserver vto = getViewTreeObserver();
            //在重绘之前,我们在onPreDraw里调用了dispatchOnDependentViewChanged方法
            vto.addOnPreDrawListener(mOnPreDrawListener);
        }

        // Record that we need the listener regardless of whether or not we're attached.
        // We'll add the real listener when we become attached.
        mNeedsPreDrawListener = true;
    }
    ...省略代码

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


ViewTreeObserver 是用来注册一个观察者来监听视图树,当视图树的布局、焦点、绘制、滚动等发生改变时,ViewTreeObserver都会收到通知。ViewTreeObserver不能被实例化,可以调用View.getViewTreeObserver()来获得。


dispatchOnDependentViewChanged方法是核心的方法,它会遍历根据依赖关系排序好的子View集合,找到位置改变了的View,或者有锚定目标的View,并回调依赖这个View的Behavior的onDependentViewChanged方法

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

            // 检查View设置了Anchor,然后处理
            for (int j = 0; j < i; j++) {
                final View checkChild = mDependencySortedChildren.get(j);

                if (lp.mAnchorDirectChild == checkChild) {
                    //调整child,让孩子到正确的锚视图位置
                    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;
            }
            //记录newRect到LayoutParams里
            recordLastChildRect(child, newRect);

            // 找到依赖当前View的Behavior来进行回调
            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 (!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;
                    }

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

监听提供依赖的View的添加和移除,HierarchyChangeListener在View的添加和移除都会回调

private class HierarchyChangeListener implements OnHierarchyChangeListener {
    ...
    @Override
    public void onChildViewRemoved(View parent, View child) {
        dispatchDependentViewRemoved(child);
        ...
    }
}


然后回调给Behavior#onDependentViewRemoved

void dispatchDependentViewRemoved(View view) {
    final int childCount = mDependencySortedChildren.size();
    boolean viewSeen = false;
    for (int i = 0; i < childCount; i++) {
        final View child = mDependencySortedChildren.get(i);
        if (child == view) {
            // 判断后续位置的View是否依赖当前View并回调
            viewSeen = true;
            continue;
        }
        if (viewSeen) {
            CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams)child.getLayoutParams();
            CoordinatorLayout.Behavior b = lp.getBehavior();
            if (b != null && lp.dependsOn(this, child, view)) {
                b.onDependentViewRemoved(this, child, view);
            }
        }
    }
}

这样Behavior中关于依赖的关系就是这个样子了。


下面我们写一个简单的demo来测试一下,上面的方法。简单暴力,直接贴代码,也没有什么好讲的。

  1. XML
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:ignore="RtlHardcoded">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/main.appbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/main.collapsing"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">

            <ImageView
                android:id="@+id/main.imageview.placeholder"
                android:layout_width="match_parent"
                android:layout_height="300dp"
                android:scaleType="fitXY"
                android:src="@drawable/huo"
                android:tint="#11000000"
                app:layout_collapseMode="parallax"
                app:layout_collapseParallaxMultiplier="0.9"
                android:contentDescription=""
                tools:ignore="ContentDescription" />

            <FrameLayout
                android:id="@+id/main.framelayout.title"
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:layout_gravity="bottom|center_horizontal"
                android:background="@color/primary"
                android:orientation="vertical"
                app:layout_collapseMode="parallax"
                app:layout_collapseParallaxMultiplier="0.3">

                <LinearLayout
                    android:id="@+id/main.linearlayout.title"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:orientation="vertical"
                    tools:ignore="UselessParent">

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_gravity="center_horizontal"
                        android:gravity="bottom|center"
                        android:text="@string/quila_name"
                        android:textColor="@android:color/white"
                        android:textSize="30sp" />

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_gravity="center_horizontal"
                        android:layout_marginTop="4dp"
                        android:text="@string/quila_tagline"
                        android:textColor="@android:color/white" />

                </LinearLayout>
            </FrameLayout>
        </android.support.design.widget.CollapsingToolbarLayout>
    </android.support.design.widget.AppBarLayout>


    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="none"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <android.support.v7.widget.CardView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="8dp"
            app:cardElevation="8dp"
            app:contentPadding="16dp">

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:lineSpacingExtra="8dp"
                android:text="@string/lorem"
                android:textSize="18sp" />
        </android.support.v7.widget.CardView>


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

    <android.support.v7.widget.Toolbar
        android:id="@+id/main.toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="@color/primary"
        app:layout_anchor="@id/main.framelayout.title"
        app:theme="@style/ThemeOverlay.AppCompat.Dark"
        app:title="">

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <TextView
                android:id="@+id/main.textview.title"
                android:layout_width="wrap_content"
                android:layout_centerInParent="true"
                android:layout_height="wrap_content"
                android:text="@string/quila_name2"
                android:textColor="@android:color/white"
                android:textSize="20sp" />

        </RelativeLayout>
    </android.support.v7.widget.Toolbar>

    <ImageView
        android:layout_width="@dimen/image_width"
        android:layout_height="@dimen/image_width"
        android:layout_gravity="center"
        app:layout_behavior="myapplication.ImageCameraBehavior"
        android:src="@drawable/ic_perm_camera_mic_black_48dp"/>
    <ImageView
        android:layout_width="@dimen/image_width"
        android:layout_height="@dimen/image_width"
        android:layout_gravity="center"
        app:layout_behavior="myapplication.ImageHomeBehavior"
        android:src="@drawable/ic_home_black_48dp"/>

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


这里请注意ToolBar设置了锚定于上面的FrameLayout


主要是让两个ImageView有交互,所以我找了两张图片。然后先设置ImageCameraBehavior

@SuppressWarnings("unused")
public class ImageCameraBehavior extends CoordinatorLayout.Behavior<ImageView> {


    private final static String TAG = "kim";
    private Context mContext;
    private int toolBarYPosition;
    private int currentImageX;
    private int finalYPosition = 150;
    private float changeBehaviorPoint;
    private float childX;
    private int imageHeight;


    public ImageCameraBehavior(Context context, AttributeSet attrs) {
        mContext = context;
    }


    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, ImageView child, View dependency) {
        return dependency instanceof Toolbar;
    }

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

        final int maxScrollDistance = toolBarYPosition;
        //展开的百分比,初始是1.
        float expandedPercentageFactor = dependency.getY() / maxScrollDistance;
        if (expandedPercentageFactor < changeBehaviorPoint) {
            //折叠的百分比
            float heightFactor = (changeBehaviorPoint - expandedPercentageFactor) / changeBehaviorPoint;
            //这里直接设置150硬编码,为了方便,实际开发请从dimens中获取
            float distanceXToSubtract = ((currentImageX - 150) * heightFactor) + (child.getWidth() / 2);

            float distanceYToSubtract = (toolBarYPosition)
                    * (1f - expandedPercentageFactor);

            float iX = currentImageX - distanceXToSubtract;
            float iY = toolBarYPosition - distanceYToSubtract;
            Log.e(TAG, "ix=" + iX + "iy=" + iY);
            child.setX(iX);
            child.setY(iY);

            float heightToSubtract = ((imageHeight - 200) * heightFactor);
            CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
            lp.width = (int) (imageHeight - heightToSubtract);
            lp.height = (int) (imageHeight - heightToSubtract);
            child.setLayoutParams(lp);


        } else {

            float distanceYToSubtract = ((toolBarYPosition)
                    * (1f - expandedPercentageFactor));

            child.setX(currentImageX);
            child.setY(toolBarYPosition - distanceYToSubtract);
            CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
            lp.width = imageHeight;
            lp.height = imageHeight;
            child.setLayoutParams(lp);
        }
        return true;
    }
    //初始化需要用到的参数
    private void InitProperties(ImageView child, View dependency) {

        if (toolBarYPosition == 0)
            toolBarYPosition = (int) dependency.getY();
//        Log.e(TAG, "toolBarYPosition=" + toolBarYPosition);
        if (currentImageX == 0)
            currentImageX = (int) (child.getX() + child.getWidth());
//        Log.e(TAG, "currentImageX=" + currentImageX);
        if (finalYPosition == 0)
            finalYPosition = dependency.getHeight();//ToolBar高度
//        Log.e(TAG, "mFinalYPosition=" + finalYPosition);

        if (imageHeight == 0) {
            imageHeight = child.getHeight();
        }
        //设定一个阈值,滑动到设定的阈值范围之后,开始移动变化
        if (changeBehaviorPoint == 0)
            changeBehaviorPoint = child.getHeight() / (2f * (toolBarYPosition - finalYPosition));

    }

}

代码比较好理解,就是一些移动与计算。另一个ImageView的Behavior也和这个差不多,就不贴了。而且这里面在计算上还有一些问题,没有时间去搞了。

那么看一眼效果图吧:
这里写图片描述


最近看到项目组的大佬们在重构代码,马上就要上线的项目,还要大改架构,真是不明白这是在干啥。之前用了1年半的时间,解决了将近4k个问题了,这样一搞,可能要重来一遍了。。。。。

幸亏和我没啥关系,大爷的。O(∩_∩)O

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值