Behavior 是 CoordinatorLayout为其 子视图 提供的一种交互行为插件。
它实现了用户可以操作子视图的一种或多种交互行为,包括包括拖拽,Fling(滑翔)或任何其他手势。
直接子类
CoordinatorLayout、AppBarLayout、SwipeDismissBehavior等使用方法
自定义
自定义Behavior,这里分为两类:
- Dependent机制:
layoutDependsOn和onDependentViewChanged作为一组。Dependent机制最常见的案例就是FloatingActionButton和SnackBar的交互行为。如图 - Nested机制
onStartNestedScroll和onNestedScroll作为一组。Nested机制要求CoordinatorLayout包含了一个实现了NestedScrollingChild接口的滚动视图控件,比如v7包中的RecyclerView,设置Behavior属性的Child View会随着这个控件的滚动而发生变化。如图
如下只标注了Behavior部分方法。开发者可以根据自身业务需求有选择的复写。
部分方法
layoutDependsOn
此方法用于判断给定的View和同级View是否作为布局依赖关系。/** * @param parent * @param child 给定的View,即应用了layout_behavior的View * @param dependency 任何与child同级的View * @return 如果返回true,那么parent将做两件事: * 1.将忽略View的顺序,总是先去布局dependency,之后布局child。 * 2.当dependency视图的布局或位置发生改变时,调用onDependentViewChanged方法。 */ public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) { //判断dependency是否是需要的依赖项,如果是,则返回true return false; }onDependentViewChanged
此方法用于对依赖视图的改变做出响应。开发者可以复写此方法从而改变child的大小和位置,并返回true。public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) { //处理child的位置或大小,并返回true return true; }onStartNestedScroll
此方法用于判断是否进行嵌套滚动。与CoordinatorLayout的任何直接子项相关联的任何Behavior都可以响应此事件。如果返回true,表明CoordinatorLayout应该充当此滚动的嵌套滚动父项。只有返回true,才会执行后续的嵌套滚动方法。/** * @param coordinatorLayout * @param child 关联Behavior的CoordinatorLayout的子View * @param directTargetChild CoordinatorLayout的子View或包含嵌套滚动操作的View。比如RecycleView外层的RelativeLayout * @param target 嵌套滚动的View * @param nestedScrollAxes 嵌套滚动的坐标轴。SCROLL_AXIS_HORIZONTAL, SCROLL_AXIS_VERTICAL * @return */ @Override public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) { return false; }onNestedScroll
此方法用于处理嵌套滚动。
每次嵌套滚动由嵌套滚动子元素更新时,onNestedScroll被调用,滚动的消费组件和未消费组件以像素提供。/** * * @param coordinatorLayout * @param child * @param target * @param dxConsumed 水平方向滚动增量, * @param dyConsumed 垂直方向滚动增量,如果大于0,手指上滑中;如果小于0,手指下滑中。 * @param dxUnconsumed 同dyUnconsumed描述 * @param dyUnconsumed 正常情况下,始终为0,当View处于最顶部或最底部,用户仍然强制下滑或上滑时,dy则不为0 */ @Override public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { if (dyConsumed > 0 && dyUnconsumed == 0) { System.out.println("上滑中。。。"); } if (dyConsumed == 0 && dyUnconsumed > 0) { System.out.println("到边界了还在上滑。。。"); } if (dyConsumed < 0 && dyUnconsumed == 0) { System.out.println("下滑中。。。"); } if (dyConsumed == 0 && dyUnconsumed < 0) { System.out.println("到边界了,还在下滑。。。"); } }
原理分析
Behavior所属
通过查看
CoordinatorLayout源码可以知道,它有一个内部类LayoutParams,用于存储CoordinatorLayout的所有子View布局参数,其中Behavior也是LayoutParams的一个属性值。因此和文章开始的描述保持了一致,Behavior只能设置到CoordinatorLayout的子View上。/** * Parameters describing the desired layout for a child of a {@link CoordinatorLayout}. */ public static class LayoutParams extends ViewGroup.MarginLayoutParams { /** * A {@link Behavior} that the child view should obey. */ Behavior mBehavior; //省略n多代码 }设置Behavior两种方式
app:layout_behavior布局属性
在布局中设置,值为自定义 Behavior类的名字字符串(包含路径),有两种写法,包含包名的全路径和以”.”开头的省略项目包名的路径。app:layout_behavior="com.yolo.myapplication.MyBehavior"
app:layout_behavior="@string/appbar_scrolling_view_behavior"@CoordinatorLayout.DefaultBehavior类注解
在需要使用 Behavior的控件源码定义中添加该注解,然后通过反射机制获取。系统的 AppBarLayout、 FloatingActionButton都采用了这种方式,所以无需在布局中重复设置。
Behavior实例化
在LayoutParams构造方法中,调用了
parseBehavior(context, attrs, a.getString( R.styleable.CoordinatorLayout_Layout_layout_behavior))方法。判断名称,然后通过反射机制实例化Behavior。从而回调Behaivor的其他方法。
注意:在自定义Behavior时,一定要重写第二个带参数的构造函数,否则这个Behavior是不会起作用的。static Behavior parseBehavior(Context context, AttributeSet attrs, String name) { if (TextUtils.isEmpty(name)) { return null; } final String fullName; if (name.startsWith(".")) { //如果behavior的值以 . 开头,则自动补全包名信息 // 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<String, Constructor<Behavior>> constructors = sConstructors.get(); if (constructors == null) { constructors = new HashMap<>(); sConstructors.set(constructors); } Constructor<Behavior> c = constructors.get(fullName); if (c == null) { //通过反射实例化Behavior 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); } }