转载

精通 CoordinatorLayout Behavior

精通 CoordinatorLayout Behavior CoordinatorLayout 是 Android Design Support Library 的重要控件之一,Design 库中的很多控件都要依赖 CoordinatorLayout。CoordinatorLayout 本身只是继承自 FrameLayout,并没有什么特殊的,魔法来自于 CoordinatorLayout.Behavior 。通过把一个 Behavior 添加到 CoordinatorLayout 的直接子 View 上,可以通过该 Behavior 来拦截点击事件、window insets、View 计算本身大小、布局 View 的位置以及 嵌套滚动( touch events, window insets, measurement, layout, and nested scrolling )。Design 库依赖 Behavior 来实现各种特效。

创建 Behavior

继承自 Behavior 类就可以创建一个 Behavior 了:

public class FancyBehavior<V extends View>     extends CoordinatorLayout.Behavior<V> {   /**    * 默认无参数构造函数,可以用来在代码中初始化该类    */   public FancyBehavior() {   }   /**    * 在 XML 布局文件中使用该类的 构造函数    *    * @param context The {@link Context}.    * @param attrs The {@link AttributeSet}.    */   public FancyBehavior(Context context, AttributeSet attrs) {     super(context, attrs);     // Extract any custom attributes out     // preferably prefixed with behavior_ to denote they     // belong to a behavior   } }

注意上面定义的类的泛型为任意的 View 子类,所以 FancyBehavior 可以添加到任意 View 上。如果你只想对特定的类使用 Behavior,则可以这样定义:

public class FancyFrameLayoutBehavior     extends CoordinatorLayout.Behavior<FancyFrameLayout>

这样的好处的,很多地方不需要把 View 参数强制转换为你需要的 View 了。

使用 Behavior.setTag()/Behavior.getTag() 函数可以暂时保存数据,也可以通过 Behavior 的 onSaveInstanceState()/onRestoreInstanceState() 函数来保存实例相关的数据。这些函数可以创建带有状态的 Behavior。

使用 Behavior

Behavior 需要添加到 CoordinatorLayout 子类上才能发挥作用。可以在代码中添加、也可以在 xml 布局文件中指定还可以通过 Java 注解来定义默认的 Behavior。

通过代码使用 Behavior

Behavior 实际上保存在每个 CoordinatorLayout 子类的 LayoutParams 中,该 LayoutParams 为 CoordinatorLayout.LayoutParams,只有 CoordinatorLayout 的直接子类的 LayoutParams 为 CoordinatorLayout.LayoutParams,所以 Behavior 只能在 CoordinatorLayout 直接子类上使用。

FancyBehavior fancyBehavior = new FancyBehavior(); CoordinatorLayout.LayoutParams params =     (CoordinatorLayout.LayoutParams) yourView.getLayoutParams(); params.setBehavior(fancyBehavior);

上面的示例代码中,使用了默认无参数构造函数来初始化 Behavior,你还可以使用其他构造函数来初始化 Behavior。

在 Layout 布局文件中使用 Behavior

如果你嫌在 代码中 使用 Behavior 太麻烦,则可以是 XML 布局文件中使用,通过 layout_behavior 属性:

<FrameLayout   android:layout_height=”wrap_content”   android:layout_width=”match_parent”   app:layout_behavior=”.FancyBehavior” />

上面的定义将会使用 FancyBehavior(Context context, AttributeSet attrs) 构造函数来初始化 Behavior。这样你还可以在 布局文件 中定义其他 Behavior 需要的参数,然后通过 AttributeSet 来获取,这样在使用的时候就可以自定义 Behavior 的行为了。

注意: 为了区分 Behavior 属性和 Layout 属性,请用 behavior_ 作为属性的前缀。

使用注解定义默认 Behavior

如果你写了一个自定义 View 需要使用一个 Behavior,则可以通过注解来默认定义一个,这样在使用的过程中就不用每次再次定义了。通过 注解 定义的方式如下:

@CoordinatorLayout.DefaultBehavior(FancyFrameLayoutBehavior.class) public class FancyFrameLayout extends FrameLayout { }

如果 该默认的 Behavior 不满足要求,则可以通过在 Java 代码中设置或者 在 XML 布局文件中设置来覆盖默认的 Behavior。

拦截触屏事件(Intercepting Touch Events)

Behavior 设置好后,就可以完成各种各样的功能了,其中之一就是拦截触屏事件。

如果不使用 CoordinatorLayout 来拦截触屏事件的话,就需要继承自不同的 ViewGroup 来处理事件。如果使用 CoordinatorLayout ,则 CoordinatorLayout 会把传递到自身的 onInterceptTouchEvent() 来调用你 Behavior 的 [onInterceptTouchEvent()](http://developer.android.com/reference/android/support/design/widget/CoordinatorLayout.Behavior.html#onInterceptTouchEvent(android.support.design.widget.CoordinatorLayout, V, android.view.MotionEvent)),这样你的 Behavior 就有机会拦截触屏事件了。如果返回 true,则你的 Behavior 的 onTouchEvent() 就会收到后续的触屏事件(View 本身对此一无所知)。 SwipeDismissBehavior 就是这样实现的,可以应用到任意 View 上实现滑动删除功能。

通过 [blocksInteractionBelow()](http://developer.android.com/reference/android/support/design/widget/CoordinatorLayout.Behavior.html#blocksInteractionBelow(android.support.design.widget.CoordinatorLayout, V)) 函数可以禁用后续 View 拦截事件。如果你想使用该功能, 则最好给出一些视觉提示用户这些 View 现在无法响应触屏事件(不然用户可能认为你的应用出 Bug 了)。所以默认的 blocksInteractionBelow() 实现依赖于 getScrimOpacity() 的返回值,如果返回值大于 0 则 blocksInteractionBelow() 返回 true。

Intercepting(拦截) Window Insets

通过fitsSystemWindows 属性可以避免 View 内存绘制到 状态栏和导航栏下面,本身是通过 Window Insets 来设置 View 的 padding 来控制 View 内容绘制的位置。Behavior 也有机会处理该情况,如果你的 View 的 fitsSystemWindows 为 true,则会优先调用 View Behavior 的 onApplyWindowInsets() 函数。

注意:如果你的 Behavior 没有完全吃掉(消耗)整个 window insets,则应该用剩余的 insets 值调用 ViewCompat.dispatchApplyWindowInsets() ,让其他的 View 处理 WindowInsets。

Intercepting Measurement and Layout

Measurement 和 layout 过程是 View 的核心之一。Behavior 也可以通过回调函数 onMeasureChild() 和 onLayoutChild() 来优先拦截这两个事件。

例如,在 任意 ViewGroup 中添加一个 maxWidth 功能:MaxWidthBehavior.java

/*  * Copyright 2015 Google Inc.  */   package com.example.behaviors;  import android.content.Context; import android.content.res.TypedArray; import android.support.design.widget.CoordinatorLayout; import android.util.AttributeSet; import android.view.ViewGroup;  import static android.view.View.MeasureSpec;  /**  * Behavior that imposes a maximum width on any ViewGroup.  *  * <p />Requires an attrs.xml of something like  *  * <pre>  * <declare-styleable name="MaxWidthBehavior_Params">  *     <attr name="behavior_maxWidth" format="dimension"/>  * </declare-styleable>  * </pre>  */ public class MaxWidthBehavior<V extends ViewGroup> extends CoordinatorLayout.Behavior<V> {     private int mMaxWidth;      public MaxWidthBehavior(Context context, AttributeSet attrs) {         super(context, attrs);         TypedArray a = context.obtainStyledAttributes(attrs,                 R.styleable.MaxWidthBehavior_Params);         mMaxWidth = a.getDimensionPixelSize(                 R.styleable.MaxWidthBehavior_Params_behavior_maxWidth, 0);         a.recycle();     }      @Override     public boolean onMeasureChild(CoordinatorLayout parent, V child,             int parentWidthMeasureSpec, int widthUsed,             int parentHeightMeasureSpec, int heightUsed) {         if (mMaxWidth <= 0) {             // No max width means this Behavior is a no-op             return false;         }         int widthMode = MeasureSpec.getMode(parentWidthMeasureSpec);         int width = MeasureSpec.getSize(parentWidthMeasureSpec);          if (widthMode == MeasureSpec.UNSPECIFIED || width > mMaxWidth) {             // Sorry to impose here, but max width is kind of a big deal             width = mMaxWidth;             widthMode = MeasureSpec.AT_MOST;             parent.onMeasureChild(child,                     MeasureSpec.makeMeasureSpec(width, widthMode), widthUsed,                     parentHeightMeasureSpec, heightUsed);             // We've measured the View, so CoordinatorLayout doesn't have to             return true;         }          // Looks like the default measurement will work great         return false;     } }

通过一个泛型的 Behavior 来控制一个 View 的所有子类型是很有用的,但是根据你应用的具体情况使用具体的 Behavior 则可以简化 Behavior 的开发。

理解 View 之间的依赖关系

上面的示例只是牵涉到一个 View。 Behavior 的真正强大之处在于根据 View 之间的关系来创建各种效果,例如 根据 一个View 位置的改变来定位另外一个 View。

Behavior 可以有两种方式来定义一个依赖的 View: 一、Behavior 的类的位置依赖另外一个 View(隐形依赖);二、layoutDependsOn() 函数 返回 True。

如果在布局文件中使用 layout_anchor 属性,就会发生View 位置依赖另外一个 View。该属性和 layout_anchorGravity 一起使用,可以很方便的把两个 View 的位置绑定到一起。例如,可以使用 FloatingActionButton.Behavior 来把 FloatingActionButton 的位置设置为 AppBarLayout 上,当 AppBarLayout 滚动到屏幕外面后, FloatingActionButton 自动隐藏。

如果 Behavior 依赖的 View 删除了,会调用 Behavior 的 onDependentViewRemoved() 函数;如果依赖的 View 位置或者大小改变了,会调用 Behavior 的 onDependentViewChanged() 函数。

Support 库中的很多炫酷的特性都是依赖该功能实现的。例如 FloatingActionButton 和 Snackbar。如果在 CoordinatorLayout 中 Snackbar 出现了, 则 FloatingActionButton 依赖 Snackbar并把自己的位置往上移,避免 Snackbar 和 FloatingActionButton 重叠。

注意:只要添加了依赖的 View, 则该 View 在 依赖的 View 布局完成后才开始布局。和 View 的顺序无关。

Nested Scrolling (嵌套滚动)

Android 从 5.0 开始支持嵌套滚动,通过 View 的 onNestedXXX 函数来处理嵌套滚动。 Behavior 的嵌套滚动有如下几点不一样的地方:

  1. 嵌套滚动不需要定义依赖 View。 CoordinatorLayout 的所有子 View 都有机会处理嵌套滚动。
  2. 非 CoordinatorLayout 的直接子 View 也可以发出嵌套滚动事件。
  3. 虽然名字为 嵌套滚动, 但是同时包含scrolling 和 flinging 手势。

如果你对滚动事件感兴趣,可以重写 onStartNestedScroll() 函数。在该函数中可以知道滚动的方向(水平滚动或者垂直滚动), 如果你想继续收到该方向的滚动事件,则必须返回 true。

onStartNestedScroll() 返回 true 以后,嵌套滚动按照如下步骤执行:

  • 在滚动的 View 开始滚动之前调用 onNestedPreScroll() 函数,在该函数内你的 Behavior 可以吃掉(消耗)部分或者全部滚动的距离,最后的 int[] 参数为返回值参数,告诉系统你处理了多少滚动距离。(比如 用户在屏幕上滑动了100像素,滚动的 View 本来应该滚动 100 像素,但是你的 Behavior 完全吃掉了这100个像素的滚动距离,则 滚动的 View 就没有滚动了。)
  • 滚动的 View 滚动的时候将会调用 onNestedScroll() 。这个函数可以知道滚动的 View 滚动的多少距离,还有多少没有消耗的距离(滚动到头了)。

对于 fling 操作是同样的处理流程。

当嵌套滚动停止的时候,会调用 onStopNestedScroll()。

假设当往下滚动的时候隐藏 FloatingActionButton ,往上滚动的时候显示,则只需要重写 onStartNestedScroll() 和 onNestedScroll() 即可。

public class ScrollAwareFABBehavior extends FloatingActionButton.Behavior {     public ScrollAwareFABBehavior(Context context, AttributeSet attrs) {         super();     }      @Override     public boolean onStartNestedScroll(final CoordinatorLayout coordinatorLayout, final FloatingActionButton child,                                        final View directTargetChild, final View target, final int nestedScrollAxes) {         // Ensure we react to vertical scrolling         return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL                 || super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);     }      @Override     public void onNestedScroll(final CoordinatorLayout coordinatorLayout, final FloatingActionButton child,                                final View target, final int dxConsumed, final int dyConsumed,                                final int dxUnconsumed, final int dyUnconsumed) {         super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);         if (dyConsumed > 0 && child.getVisibility() == View.VISIBLE) {             // User scrolled down and the FAB is currently visible -> hide the FAB             child.hide();         } else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) {             // User scrolled up and the FAB is currently not visible -> show the FAB             child.show();         }     } }

这才刚刚开始

每个独立的 Behavior 都很好玩,如果很多的 Behavior 组合到一起,则屌爆了。建议查看下 Support 库中的各种 Behavior 的实现,来了解各种 Behavior 的高级用法。 /extras/android/m2repository 目录包含最新的代码,也可以通过 Android SDK Search chrome 浏览 器插件来查看源代码。

原文  http://blog.chengyunfeng.com/?p=906
正文到此结束
Loading...