CoordinatorLayout.Behavior
2017-11-01 14:59
399 查看
CoordinatorLayout我们可以将它理解为一个超级Fragment,它的布局方式是一层一层叠上去,而且它可以组织子View之间的协作。组织协作的方式需要使用最重要的对象Behavior
Behavior是CoordinatorLayout实现子View之间交互的插件,它可以实现用户的一个或多个交互行为,它们可能包括拖拽、滑动、或者其他一些手势。
我们在使用CoordinatorLayout的时候,在NestedScrollView的xml属性中总是能看到
常用的方法(暂不介绍嵌套滑动)
1 .
2 .
3 .
只是介绍这几个方法,可能我们还不太清楚,所以直接做几个练习,就能看到效果了。
我这里不准备做练习了,练习可以自己试,我这里要简单说一说源码里面这几个方法怎么调用的(我的水平只能简单说一说/(ㄒoㄒ)/~~)
我们一般会在XML中直接定义Behavior,我们在代码中来看看这个Behavior是怎么解析的。
在xml里面写的话,是在inflate的时候对Behavior赋值的
以注解方式写的话,是在onMeasure内赋值的。(后面会说为什么)
为View配置了Behavior,那么我们接着来看View之间是怎么依赖的。首先在
返回两种结果,满足一种就是依赖的关系。一种是设置anchor ,另一种就是View的Behavior对另一个View有依赖。
如何处理这种依赖关系呢?既然Behavior能够监测另一个View的变化状况,那么肯定会有重新测量等操作。所以我们来看下面的代码
点进去
可以看到
依赖的集合也有了,那么接下来,就要添加各种监听了,监听绘制过程,监听View的层级变化了,所以在
ViewTreeObserver 是用来注册一个观察者来监听视图树,当视图树的布局、焦点、绘制、滚动等发生改变时,ViewTreeObserver都会收到通知。ViewTreeObserver不能被实例化,可以调用View.getViewTreeObserver()来获得。
监听提供依赖的View的添加和移除,
然后回调给
这样Behavior中关于依赖的关系就是这个样子了。
下面我们写一个简单的demo来测试一下,上面的方法。简单暴力,直接贴代码,也没有什么好讲的。
XML
这里请注意ToolBar设置了锚定于上面的FrameLayout
主要是让两个ImageView有交互,所以我找了两张图片。然后先设置
代码比较好理解,就是一些移动与计算。另一个ImageView的Behavior也和这个差不多,就不贴了。而且这里面在计算上还有一些问题,没有时间去搞了。
那么看一眼效果图吧:
最近看到项目组的大佬们在重构代码,马上就要上线的项目,还要大改架构,真是不明白这是在干啥。之前用了1年半的时间,解决了将近4k个问题了,这样一搞,可能要重来一遍了。。。。。
幸亏和我没啥关系,大爷的。O(∩_∩)O
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 .
layoutDependsOnView的依赖关系在这里设置
/** * 表示是否给应用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 4000 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来测试一下,上面的方法。简单暴力,直接贴代码,也没有什么好讲的。
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"> e666 <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
相关文章推荐
- 关于CoordinatorLayout与Behavior的一点分析
- CoordinatorLayout布局和自定义Behavior
- CoordinatorLayout与Behavior
- 拦截一切的CoordinatorLayout Behavior
- CoordinatorLayout高级用法-自定义Behavior
- CoordinatorLayout中,有水平RecyclerView,导致appbar_scrolling_view_behavior失效的解决方法
- CoordinatorLayout与Behavior总结
- Android使用CoordinatorLayout和BottomSheetBehavior实现滑动效果(底部抽屉)
- CoordinatorLayout高级用法-自定义Behavior
- 一个神奇的控件——Android CoordinatorLayout与Behavior使用指南
- CoordinatorLayout.Behavior自定义效果的实现
- Android CoordinatorLayout和Behavior的源码分析(一)
- 关于CoordinatorLayout与Behavior的一点分析
- CoordinatorLayout自定义Behavior
- CoordinatorLayout高级用法-自定义Behavior
- CoordinatorLayout behavior
- Android CoordinatorLayout和Behavior的源码分析(二)
- 深入理解Android开发中的CoordinatorLayout Behavior
- android.support.design库组件(CoordinatorLayout和CoordinatorLayout.Behavior)
- CoordinatorLayout+Behavior讲解