Behavior是Android Design包中出现的一个概念,Android Design包中很多控件的动画效果都是使用Behavior实现的,所以想要更好的实现Material Design风格的应用就有必要弄清楚Behavior。这篇文章从简单开始,介绍如何自定义Behavior以实现快速返回的效果。
先看最终的实现效果
介绍
Interaction behavior plugin for child views of CoordinatorLayout.
A Behavior implements one or more interactions that a user can take on a child view. These interactions may include drags, swipes, flings, or any other gestures.
上面的介绍说Behavior是CoordinatorLayout子视图的一个交互插件,它可以为子视图实现一个或多个交互,这些交互包括拖拽,滑动或其他的手势操作。
通过上面的介绍我们知道Behavior是作用于CoordinatorLayout子视图的,而CoordinatorLayout我们可以把它看做一个FrameLayout。
根据我的理解来说Behavior其实就是一系列手势操作行为的回调,通过这些回调来处理CoordinatorLayout子视图的手势操作。
使用
用过Android Design库中AppBarLayout与NestedScrollView这两个类的同学应该知道,这两个类一起使用会产生很漂亮的滑动效果,这也是Android库中对Behavior的一个很典型的应用。而对于Behavior的使用,也可以参考这两个类的两种使用方式:
1. 为CoordinatorLayout的直接子View设置app:layout_behavior=“behavior完全类名”
2. 为CoordinatorLayout的某个子View设置默认Behavior,设置方式是在该View的类声明上添加注解@CoordinatorLayout.DefaultBehavior(Behavior类.class)
大家看上面代码NestedScrollView的布局部分,可以发现会有一个app:layout_behavior属性,该属性就是设置NestedScrollView这个子视图的Behavior插件,所以当我们自定义实现了Behavior后,只需要在CoordinatorLayout的子View中为其添加app:layout_behavior属性即可。
Behavior相关方法
public class BackTopBehavior extends CoordinatorLayout.Behavior {
public MyBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
//必须实现此构造方法,因为CoordinatorLayout中初始化Behavior时是通过反射调用此构造来进行初始化的
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return super.layoutDependsOn(parent, child, dependency);
//判断视图child进行layout布局时是否依赖于某个特定的View dependency
//child是指应用此Behavior的View,dependency是触发执行此Behavior的视图并与child进行相关交互,也就是上面所说的是child的依赖
//此方法在CoordinatorLayout进行request layout时至少会调用一次
//如果返回true,CoordinatorLayout会总是在依赖视图dependency layout完成之后对child视图进行layout布局
//同时如果依赖视图dependency的layout或position发生变化,CoordinatorLayout会调用onDependentViewChanged
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
return super.onDependentViewChanged(parent, child, dependency);
//此方法的调用时机参考上面的方法layoutDependsOn的说明
//当child的依赖视图dependency发生layout变化后,如果想对child布局(child's size or position)做出相应变化则返回true否则返回false,具体对child如何update则需要在onLayoutChild中进行实现
}
@Override
public void onDependentViewRemoved(CoordinatorLayout parent, View child, View dependency) {
super.onDependentViewRemoved(parent, child, dependency);
//当child的依赖视图dependency从其parent中remove掉后会调用此方法
}
@Override
public boolean onMeasureChild(CoordinatorLayout parent, View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
return super.onMeasureChild(parent, child, parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed);
//当测量CoordinatorLayout的子视图child时调用此方法
}
@Override
public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
return super.onLayoutChild(parent, child, layoutDirection);
//当对CoordinatorLayout的子视图child进行layout布局时会调用此方法
//当child的依赖视图layout结束之后,会调用此方法对child进行layout布局
//如果onDependentViewChanged中返回了true,则需要在此方法中对child视图进行update
}
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
//滑动开始调用,返回true表示此Behavior接收此滑动,才会有后续的滑动处理
}
@Override
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target) {
super.onStopNestedScroll(coordinatorLayout, child, target);
//滑动结束调用
}
@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
//滑动过程中调用
}
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
//滑动过程中,在child自身消费掉此次滑动的distance之前调用此方法
//onNestedPreScroll is called each time the nested scroll is updated by the nested scrolling child
// before the nested scrolling child has consumed the scroll distance itself
}
@Override
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float velocityY, boolean consumed) {
return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
//快速滑动时调用此方法
}
}
上面就是Behavior中比较常用比较重要的一些方法。
自定义Behavior
我要实现的是在在列表上滑时,显示快速返回按钮,列表下滑时隐藏快速返回按钮,当快速返回按钮显示时,点击该按钮,列表会自动滑动到顶部。
快速返回按钮我用的是Android Design
中的FloatingActionButton
,其实在FloatingActionButton
中设置了默认的Behavior
,但是这个默认的Behavior
是与SnackBar
结合使用的,因此我可以直接继承FloatingActionButton.Behavior
复写其中的相关方法实现我们所要的效果,这样可以减少很多工作
public class BackTopBehavior extends FloatingActionButton.Behavior {
private static final String TAG = "BackTopBehavior";
public BackTopBehavior(Context context, AttributeSet attrs) {
super();
}
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, View directTargetChild, View target, int nestedScrollAxes) {
return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL;//垂直方向滑动
}
@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
if (dyConsumed > 0 && dyUnconsumed == 0) {
Log.d(TAG, "上滑中。。。");
}
if (dyConsumed == 0 && dyUnconsumed > 0) {
Log.d(TAG, "到边界了还在上滑。。。");
}
if (dyConsumed < 0 && dyUnconsumed == 0) {
Log.d(TAG, "下滑中。。。");
}
if (dyConsumed == 0 && dyUnconsumed < 0) {
Log.d(TAG, "到边界了,还在下滑。。。");
}
if((dyConsumed > 0 && dyUnconsumed == 0) || (dyConsumed == 0 && dyUnconsumed > 0) && child.getVisibility() != View.VISIBLE) {
child.show();//上滑的时候显示按钮
} else if((dyConsumed < 0 && dyUnconsumed == 0) || (dyConsumed == 0 && dyUnconsumed < 0) && child.getVisibility() != View.GONE) {
child.hide();//下滑的时候因此按钮
}
}
}
这样实现起来是不是感觉很简单,如果不用Behavior
的话,需要自己自定义View,并对滑动事件进行处理,实现起来肯定比Behavior
方式要费劲。
剩下的布局和Activity代码如下:
public class BackTopBehaviorActivity extends AppCompatActivity {
CommonRecyclerView mRecyclerView;
LinearLayoutManager mLayoutManager;
FloatingActionButton mFloatingActionButton;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_backtop_behavior);
mFloatingActionButton = (FloatingActionButton) findViewById(R.id.floatingActionButton);
mFloatingActionButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mLayoutManager.smoothScrollToPosition(mRecyclerView, null , 0);
}
});
initRecyclerView();
}
private void initRecyclerView() {
mRecyclerView = (CommonRecyclerView) findViewById(R.id.recyclerView);
mLayoutManager = new LinearLayoutManager(this);
mRecyclerView.setLayoutManager(mLayoutManager);
mRecyclerView.addItemDecoration(new SpacesItemDecoration(5));
List<String> data = new ArrayList<>();
for (int i = 0; i < 30; i ++) {
data.add("数据" + (i + 1));
}
StringListAdapter mAdapter = new StringListAdapter(this, data);
mRecyclerView.setAdapter(mAdapter);
}
}
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
app:title="@string/app_name"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:titleTextColor="@android:color/white"/>
</android.support.design.widget.AppBarLayout>
<cn.ittiger.demo.ui.CommonRecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_collapseMode="pin"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/floatingActionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/d_15"
android:src="@android:drawable/stat_sys_upload_done"
app:layout_behavior="cn.ittiger.demo.behavior.BackTopBehavior"
app:layout_scrollFlags="scroll|enterAlways|snap"
android:visibility="gone"/>
</android.support.design.widget.CoordinatorLayout>