翻译与: Intercepting everything with CoordinatorLayout Behaviors
使用过 Android Design Support Library
的小伙伴应该对 CoordinatorLayout
比较熟悉,它可以让它的子View产生一系列联动效应,如下效果图:
但这些究竟是怎么做到的?其实CoordinatorLayout本身并没有做太多的事情,它的布局方式和FrameLayout基本相同,那么上图中我们看到的神奇的动态效果是怎么实现的呢?答案就是 CoordinatorLayout.Behaviors
:
通过把一个Behavior附加到CoordinatorLayout的一个直接子类上面,那么这个子类就拥有了拦截CoordinatorLayout的 touch events
, window insets
, measurement
, layout
和 nested scrolling
这一系列事件的能力。
创建一个Behavior很简单,只要继承Behavior类即可
public class FancyBehavior<V extends View> extends CoordinatorLayout.Behavior<V> { /** *当FancyBehavior在代码中被添加到子类时调用的构造函数 */ public FancyBehavior() { } /** * 当FancyBehavior是从布局文件中被添加到子类时调用的构造函数 * * @param context The {@link Context}. * @param attrs The {@link AttributeSet}. */ public FancyBehavior(Context context, AttributeSet attrs) { super(context, attrs); } }
注意,在上面这段代码中使用了范型,此时FancyBehavior可以被添加到任何View上,如果希望FancyBehavior只被添加到特定的View子类上,可以采用如下写法:
public class FancyFrameLayoutBehavior extends CoordinatorLayout.Behavior<FancyFrameLayout>
另外在Behavior的使用中,可以使用 Behavior.setTag()/Behavior.getTag()
来保存临时性的变量,另外也可以使用 onSaveInstanceState()/onRestoreInstanceState()
进行数据保存。善用这些方法可以让Behavior更具状态性。
Behavior本身并不做任何事情,它们需要被添加到CoordinatorLayout的直接子类才能被使用。主要有三种方法来将Behavior添加进子类,分别是 程序中动态添加
, Xml布局文件添加
和 使用注解添加
。
FancyBehavior fancyBehavior = new FancyBehavior(); CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) yourView.getLayoutParams(); params.setBehavior(fancyBehavior);
在上面的这个例子中,我们在代码中向yourView添加了fancyBehavior,同时也是使用了FancyBehavior()这个构造函数,如果对FancyBehavior的构造过程需要额外的参数,可以自行重载构造函数。
<FrameLayout android:layout_height=”wrap_content” android:layout_width=”match_parent” app:layout_behavior=”.FancyBehavior” />
如果觉得在代码中添加会让代码变得混乱的话可以使用上面这种方式,它需要使用到CoordinatorLayout的一个自定义属性 layout_behavior
,属性内容是你的类名。
需要注意的是使用这种方法,FancyBehavior(Context context, AttributeSet attrs)这个构造函数会被默认调用,这样我们就可以为其自定义一些xml属性,以保证其他开发者可以通过xml来修改FancyBehavior的行为。
Note:给布局文件添加自定义属性后,在代码中一般使用layout_的形式获取自定义属性,与此类似,使用behavior_的形式获取Behavior的自定义属性。
如果你构建了一个自己的视图,并且这个视图需要一个自定义的Behavior,那么你就可以使用下面这种方式:
@CoordinatorLayout.DefaultBehavior(FancyFrameLayoutBehavior.class) public class FancyFrameLayout extends FrameLayout { }
使用这种方式添加的Behavior被设置成了DefaultBehavior,如果这时在Xml中使用layout_behavior的话,这个DefaultBehavior会被覆盖。
一但你的Behavior设置到位,那么你就可以真正做一些有意义的事情了。第一个要说的就是 拦截触摸事件
@Override public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) { return super.onInterceptTouchEvent(parent, child, ev); } @Override public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) { return super.onTouchEvent(parent, child, ev); }
需要拦截触摸事件的话需要重写上述两个方法,当onInterceptTouchEvent返回true的时候,那么我们的Behavior就会活的所有后续的onTouchEvent。
另外还有一个简单粗暴的方法来拦截触摸事件:
@Override public boolean blocksInteractionBelow(CoordinatorLayout parent, V child) { return true; }
故名思意,当返回true的时候,我们这个视图下的其他视图将获取不到任何Touch事件。
如果你的视图的fitsSystemWindows属性是true,那么你的Behavior的onApplyWindowInsets()就会被调用,可以在这里优先处理WindowInsets相关问题。
Note:如果你的视图没有消费掉WindowsInsets,那么需要调用ViewCompat.dispatchApplyWindowInsets()将其传递给子视图。
在Behavior中,可以通过重写onMeasureChild()和onLayoutChild()来拦截父视图的相关Measurement和Layout操作,比如下面的代码就是通过重写onMeasureChild()来拦截父视图的onMeasureChild(),以达到设置视图最大宽度的目的。
/* * Copyright 2015 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ 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固然有用,但我们需要知道的是有时候如果你想让你的app简单一点的话完全可以把Behavior的相关功能写在自定义View的内部,没必要为了使用Behavior而是用它。
上面说到的这些功能之一来一个视图,但Behavior的强大之处不在于此,而是它可以支持视图间的相互依赖,就像一开始的那张动图一样。当一个视图发生变化后,依附在上面的Behavior就会收到一个回调,以此来实现更加丰富有用的功能。
有两种方法可以实现Behavior对视图的依赖,一种是Behavior对应的视图 固定
在另一个视图上,另一种是在 layoutDependsOn()
中返回true。
使用 固定
的方法也很简单,只要在Xml中添加CoordinatorLayout的自定义属性 layout_anchor
和 layout_anchorGravity
,前者用来确定目标视图,后者用来确定固定到目标视图的位置。
例如将 FloatingActionButton
固定到 AppBarLayout
后,FloatingActionButton的Behavior就会默认使用依赖关系来隐藏自己当AppBarLayout从屏幕滑出后。
通常建立了依赖关系后Behavior的以下两个方法会被激活, onDependentViewRemoved()
, onDependentViewChanged()
。
Note:建立依赖关系的两个视图中,若被依赖的视图被从布局中移除,那么相应的那个布局也会被移除。
关于Nested Scrolling,我们需要知道一下几点:
1.我们没有必要为了获得Nested Scrolling相关回调而去写依赖关系,CoordinatorLayout的每一个子View都能有机会捕获到Nested Scrolling事件。
2.Nested Scrolling事件不仅可以被CoordinatorLayout的直接子类捕获,也可以被它的间接子类们捕获。
3.虽然称之为 nested(折叠) Scrolling
,但它产生的滑动事件是1:1的。
所以,如果你需要捕获nested scrolling事件,可以在适当的时候在 onStartNestedScroll()
里返回true。接着你就能使用以下两个方法回调了:
1.onNestedPreScroll()会在scrolling View获得滚动事件前调用,它允许你消费部分或者全部的事件信息。
2.onNestedScroll()会在scrolling View做完滚动后调用,通过回调可以知道scrolling view滚动了多少?和它没有消耗的滚动事件。
当nested scrolling结束后,你会得到一个onStopNestedScroll()回调,说明这次的滚动事件已经结束。等待下一次的onStartNestedScroll()。
当把上面的所有都结合起来使用的时候,就是见证奇迹的时候了。如果想了解更多相关资料,鼓励你去查看其源码,相信你能收获更多知识。