0
点赞
收藏
分享

微信扫一扫

Android Study Material Design 十五 深入Behavior以及自定义Behavior


LZ-Says:最大的无奈,便是无奈之中的无奈。


前言

本章将针对上一章进行继续深入,本章将一起来深入Behavior源码以及如何自定义Behavior。

Behavior源码分析

我们首先来看一下之前写过的关于AppBarlayout的一个布局:

<?xml version="1.0" encoding="utf-8"?>
<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:context="com.materialdesignstudy.coordinatorlayout.behavior.BehaviorActivity">

<android.support.v7.widget.RecyclerView
android:id="@+id/id_rv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />

<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize">

<android.support.v7.widget.Toolbar
android:id="@+id/id_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways" />
</android.support.design.widget.AppBarLayout>

<android.support.design.widget.FloatingActionButton
android:id="@+id/id_fab"
android:layout_width="58dp"
android:layout_height="58dp"
android:layout_gravity="right|bottom"
android:layout_margin="16dp"
android:background="@drawable/fag_bg"
android:onClick="displaySnackbar"
android:src="@drawable/ic_love"
app:layout_behavior="com.materialdesignstudy.coordinatorlayout.behavior.FabBehavior" />

</android.support.design.widget.CoordinatorLayout>

而如上的layout实现的效果如下:


Android Study Material Design 十五 深入Behavior以及自定义Behavior_偏移量_02

RecycleView滑动时,FloatingActionButton会随着RecycleView滑动方向而对其本身进行缩放效果,那么关于这里,我们在回过头在看下其中细节。

首先需要明确的是,Behavior,其作为协调桥梁或者说是纽带,其作用,便是用于管理CoordingatorLayout其包含所有子控件 产生的联动效果。

也就是说,我们要监听谁,就给谁设置Behavior即可。

那么针对上面的布局,我们先来简述下实现的效果:

RecycleView向上滑动时,FloatingActionButton逐渐缩小直至消失,而向下滑动时,则逐渐放大,直至正常。

如上的效果我们确实实现了,但是在实现的过程中,大家有没有注意到一个细节。还记得我们之前说的,如果你想监听谁,就给谁设置Behavior。但是针对我们上面的布局,为什么AppBarLayout并没有设置Behavior也能实现呢?

如下图:


Android Study Material Design 十五 深入Behavior以及自定义Behavior_控件_03

针对需求,我们分别为RecycleView以及FloatingActionButton设置了app:layout_behavior,唯独AppBarLayout没有设置,但是依旧实现了。

那么,是不是我们也可以不用设置Behavior或者说是AppBarLayout其内部自己实现了一个默认Behavior?

我们不知道,只能从源码中寻找答案,一起来看~

@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout

第一行,通过注解的方式定义了一个DefaultBehavior,这个也能很有力的证实了我们的猜测确实是内部定义了Behavior,并且参数指定了AppBarLayout类型的Behavior。

而第二行,告诉我们AppBarLayout继承自LinearLayout

再次深入,我们发现AppBarLayout中的Behavior又继承了一个叫做HeaderBehavior并指定了View类型,再次深入时发现HeaderBehavior继承了ViewOffsetBehavior,然而我们查到最后发现,最终还是回到了CoordinatorLayout.Behavior。

有没有一种心好累的感觉,幕后黑手终于浮出水面。。。


Android Study Material Design 十五 深入Behavior以及自定义Behavior_android_04

接着往下看~

我们回过头先来看一下我们之前的自定义Behavior,我们之前实现分为如下几步:

  • 添加Behavior构造,避免引发找不到的bug;
  • 重写onStartNestedScroll()监听观察的View(RecycleView)发生滑动的开始时进行回调,并且我们只关心垂直状态滑动;
  • 重写onNestedScroll()被监听观察的View滑动是开始回调。

一、AppBarLayout Behavior源码分析

接着我们深入源码去查看,首先进入AppBarlayout中的Behaior是如何操作的?

public Behavior(Context context, AttributeSet attrs) {
super(context, attrs);
}

一开始也是创建Behavior构造,方便使用layout方式进行实例化。

@Override
public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child,
View directTargetChild, View target, int nestedScrollAxes) {
// Return true if we're nested scrolling vertically, and we have scrollable children
// and the scrolling view is big enough to scroll
final boolean started = (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0
&& child.hasScrollableChildren()
&& parent.getHeight() - directTargetChild.getHeight() <= child.getHeight();
if (started && mOffsetAnimator != null) {
// Cancel any offset animation
mOffsetAnimator.cancel();
}
// A new nested scroll has started so clear out the previous ref
mLastNestedScrollingChildRef = null;
return started;
}

而关于上面对于重写onStartNestedScroll,其内部和我们编写的几乎相似,他们判断的内容如下:

  • 当前滑动方向是否是垂直;
  • 是否有可被滑动的子控件;
  • 是否具有足够的空间可被滑动。

如果满足以上条件,则进行关闭一些动画效果的偏移,之后再开始新的嵌套滑动开始之前,先清空之前的引用,之后将状态返回,方便后续进行处理。

@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
View target, int dx, int dy, int[] consumed) {
if (dy != 0 && !mSkipNestedPreScroll) {
int min, max;
if (dy < 0) {
// We're scrolling down
min = -child.getTotalScrollRange();
max = min + child.getDownNestedPreScrollRange();
} else {
// We're scrolling up
min = -child.getUpNestedPreScrollRange();
max = 0;
}
consumed[1] = scroll(coordinatorLayout, child, dy, min, max);
}
}

@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed) {
if (dyUnconsumed < 0) {
// If the scrolling view is scrolling down but not consuming, it's probably be at
// the top of it's content
scroll(coordinatorLayout, child, dyUnconsumed,
-child.getDownNestedScrollRange(), 0);
// Set the expanding flag so that onNestedPreScroll doesn't handle any events
mSkipNestedPreScroll = true;
} else {
// As we're no longer handling nested scrolls, reset the skip flag
mSkipNestedPreScroll = false;
}
}

接着往下看的时候,我们发现谷歌将我们卸载onNestedScroll的逻辑分开写了,那么分别每个里面是如何操作的呢?

  • onNestedPreScroll():判断当前滑动方向,更新min以及max值,之后调用scroll()进行相应的处理,关于scroll()我们稍后查看具体实现;
  • onNestedScroll:主要进行逻辑处理判断,也就是有效滑动范围,通俗来讲就是,在向下滑动时,没有偏移量的产生从而认为当前处于顶部状态,接着调用scroll进行处理,随即更新标识位

那么这个scroll()里面到底干了什么事儿呢?我们接着往下看。

final int scroll(CoordinatorLayout coordinatorLayout, V header,
int dy, int minOffset, int maxOffset) {
return setHeaderTopBottomOffset(coordinatorLayout, header,
getTopBottomOffsetForScrollingSibling() - dy, minOffset, maxOffset);
}

直接return一个方法,此方法从名字上来说,含义为:设置头部顶部、底部偏移量,具体我们往下看,有木有一种感觉,我们找到了AppBarLayout的命脉???

int setHeaderTopBottomOffset(CoordinatorLayout parent, V header, int newOffset,
int minOffset, int maxOffset) {
final int curOffset = getTopAndBottomOffset();
int consumed = 0;
if (minOffset != 0 && curOffset >= minOffset && curOffset <= maxOffset) {
// If we have some scrolling range, and we're currently within the min and max
// offsets, calculate a new offset
newOffset = MathUtils.constrain(newOffset, minOffset, maxOffset);
if (curOffset != newOffset) {
setTopAndBottomOffset(newOffset);
// Update how much dy we have consumed
consumed = curOffset - newOffset;
}
}
return consumed;
}

首先获取到偏移量,其次如果当前偏移量满足以下三个条件则进行最小、最大偏移量之前计算一个新的偏移量,随后得出的偏移量如果不等于实际顶部和底部时进行设置setTopAndBottomOffset(newOffset);

  • 最小偏移量不等于0;
  • 当前偏移量大于等于最小偏移量;
  • 当前偏移量小于最大偏移量

而关于这里所引用的数学工具类,LZ看了下比较有意思,很是贴近生活哈~

class MathUtils {

static int constrain(int amount, int low, int high) {
return amount < low ? low : (amount > high ? high : amount);
}

static float constrain(float amount, float low, float high) {
return amount < low ? low : (amount > high ? high : amount);
}

}

而接下来的处理相对来说比较easy了,只是设置偏移量之后更新偏移量,如下:

public boolean setTopAndBottomOffset(int offset) {
if (mViewOffsetHelper != null) {
return mViewOffsetHelper.setTopAndBottomOffset(offset);
} else {
mTempTopBottomOffset = offset;
}
return false;
}

/**
* Set the top and bottom offset for this {@link ViewOffsetHelper}'s view.
*
* @param offset the offset in px.
* @return true if the offset has changed
*/
public boolean setTopAndBottomOffset(int offset) {
if (mOffsetTop != offset) {
mOffsetTop = offset;
updateOffsets();
return true;
}
return false;
}

private void updateOffsets() {
ViewCompat.offsetTopAndBottom(mView, mOffsetTop - (mView.getTop() - mLayoutTop));
ViewCompat.offsetLeftAndRight(mView, mOffsetLeft - (mView.getLeft() - mLayoutLeft));
}

二、HeaderBehavior Behavior分析

extends HeaderBehavior,继承HeaderBehavior传入AppBarLayout,代表当前只为AppBarLayout添加相关行为。

这里上来创建俩个构造:

public HeaderBehavior() {}

public HeaderBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}

接着就是对事件的处理,包含onInterceptTouchEvent()、onTouchEvent(),接着我们直接去看ViewOffsetBehavior。

三、ViewOffsetBehavior源码简述

这部分主要的工作内容,便是测量子控件高度、摆放子控件的位置

四、CoordinatorLayout.Behavior 浅入

public static abstract class Behavior<V extends View> {

/*
* 用于实例化Behaviors的默认构造函数
*/
public Behavior() {
}

/*
* 用于从布局中扩充Behaviors的默认构造函数。 Behaviors将解析特别定义的布局参数。 这些参数将出现在子视图标签上。
*/
public Behavior(Context context, AttributeSet attrs) {
}


/*
* 在调度到子视图之前,对CoordinatorLayout触摸事件做出响应
*/
public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
return false;
}

/*
* 行为开始后,回应CoordinatorLayout触摸事件。
*/
public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
return false;
}

/*
* 对子控件进行绘制、摆放
*/
public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
return false;
}

/*
* 当CoordinatorLayout的子控件尝试启动嵌套滚动时调用
*/
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
V child, View directTargetChild, View target, int nestedScrollAxes) {
return false;
}

/*
* 当嵌套滚动已被CoordinatorLayout接受时调用
*/
public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, V child,
View directTargetChild, View target, int nestedScrollAxes) {
// Do nothing
}

/*
* 嵌套滚动结束时调用
*/
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) {
// Do nothing
}

/*
* 当正在进行嵌套的滚动更新并且目标已经滚动或试图滚动时调用
*/
public void onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target,
int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
// Do nothing
}

/*
* 当正在进行嵌套滚动即将更新时,在目标消耗任何滚动距离之前调用
*/
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target,
int dx, int dy, int[] consumed) {
// Do nothing
}

}

首先可以看到,此类是一个abstract方法,默认提供俩种构造,无参以及属性构造函数,方便调用时的实例化操作。

而关于一些的事件,统统return false,代表其他子控件可自行处理事件。

其他的暂时没什么好说的了,我们开启下一小节,如何自定义我们的Behavior。


Android Study Material Design 十五 深入Behavior以及自定义Behavior_behavior_05

自定义Behavior

我们之前说过,Behavior好比一个桥梁,主要的作用就是CoordingatorLayout其包含所有子控件 产生的联动效果,当然也可以监听其他的View,那么在实际的使用过程中,它将已如下俩种方式进行呈现:

  • 第一种情况:

View需要监听另外一个View的状态,例如监听位置、大小以及显示的效果等;

需要监听layoutDependsOn()、onDependentViewChanged()。

  • 第二种情况:

View需要监听CoordingatorLayout里面所包含的所有控件的某些状态,例如触摸滑动等。

需要监听onStartNestedScroll()、onNestedPreScroll(),如涉及到快速滑动则需要onNestedFling()。

这里再次举例说明下,方便理解:


Android Study Material Design 十五 深入Behavior以及自定义Behavior_控件_06

这里LZ就犯了一个嘀咕,明明监听的是RecycleView,怎么还成了被监听者?

我们之前说过,监听谁,就给谁设置behavior。而实际意义上,我们需要的是如上图的AppBarLayout以及FAB由于RecyclerView被触发后产生的一系列的联动效果。

关键,只需要记住一句话,监听谁,就给谁设置~!!!

下面这对俩种情况进行举例说明:

View需要监听另一个View

1. 布局搭建

只是为了演示效果,这里就直接放置俩个TextView。虽然我们没创建自定义Behavior,但是也写好,具体查看第二个TextView。

<?xml version="1.0" encoding="utf-8"?>
<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:context="com.materialdesignstudy.custombehavior.CustomBehaviorActivity">

<TextView
android:id="@+id/id_tv"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:layout_gravity="top|left"
android:background="@color/colorPrimary_pink"
android:padding="15dp"
android:text="被监听者" />

<TextView
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_gravity="top|right"
android:background="@color/bg1_green"
android:clickable="true"
android:onClick="getNestedScrollView"
android:padding="15dp"
android:text="监听者"
app:layout_behavior="com.materialdesignstudy.custombehavior.CustomBehavior" />

</android.support.design.widget.CoordinatorLayout>

2. 创建自定义Behavior

自定义Behavior,首先需要继承自CoordinatorLayout.Behavior,而为了方便其他控件使用,View Type设置为View。

而关于在layoutDependsOn()方法中,我们只需要去校验当前控件类型,针对示例,只需要校验是否是TextView即可,剩下让它自己玩去~当然,这里你可以使用tag,也可以使用ViewID去校验~

而最后在onDependentViewChanged()方法中,我们首先去关心的是我们当前监听View状态,也就是滑动方向。其次需要拿到实际的偏移量,最后我们玩个小效果~

public class CustomBehavior extends CoordinatorLayout.Behavior<View> {

public CustomBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}

/**
* 用来决定需要监听哪儿些控件或者容器的状态 监听who 什么状态change
*
* @param parent 父容器
* @param child 子控件 具体代表监听dependency的View 观察者
* @param dependency 被监听的View
* @return
*/
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency instanceof TextView || super.layoutDependsOn(parent, child, dependency);
}

/**
* layoutDependsOn 返回true时被回调
* 当被监听的View发生改变的时候回调 可在此设置相应联动动画等效果
*
* @param parent
* @param child
* @param dependency
* @return
*/
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
// 获取被监听的View的状态 只关心 滑动位置 也就是 垂直方向
int offset = dependency.getTop() - child.getTop();
// 让child进行平移
ViewCompat.offsetTopAndBottom(child, offset);
// 旋转
child.animate().rotation(child.getTop() * 10);
return true;
}
}

3. 监听事件处理

关于事件处理,我们需要做的只是在A的点击时候设置B即可。

private void initView() {
mTv1 = (TextView) findViewById(R.id.id_tv);
mTv1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ViewCompat.offsetTopAndBottom(v, 10);
}
});
}

4. 效果查看


Android Study Material Design 十五 深入Behavior以及自定义Behavior_android_07

View需要监听CoordingatorLayout里面所包含的所有控件的某些状态

首先我们假设一个需求:

滑动左侧View时,右侧同步进行联动

基于以上需求,我们将采用NestedScrollView来实现,左右各一个。

1. 布局搭建

<?xml version="1.0" encoding="utf-8"?>
<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:context="com.materialdesignstudy.custombehavior.NestedScrollViewAct">

<android.support.v4.widget.NestedScrollView
android:layout_width="80dp"
android:layout_height="match_parent"
android:layout_gravity="left"
android:background="#FDFD">

<android.support.v7.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<TextView style="@style/customBehaviorStyle" />

<TextView style="@style/customBehaviorStyle" />

<TextView style="@style/customBehaviorStyle" />

<TextView style="@style/customBehaviorStyle" />

<TextView style="@style/customBehaviorStyle" />

<TextView style="@style/customBehaviorStyle" />

<TextView style="@style/customBehaviorStyle" />

<TextView style="@style/customBehaviorStyle" />

<TextView style="@style/customBehaviorStyle" />

</android.support.v7.widget.LinearLayoutCompat>

</android.support.v4.widget.NestedScrollView>

<android.support.v4.widget.NestedScrollView
android:layout_width="80dp"
android:layout_height="match_parent"
android:layout_gravity="right"
android:background="@color/bg3_green"
app:layout_behavior="com.materialdesignstudy.custombehavior.SyncScrollBeahvior">

<android.support.v7.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<TextView style="@style/customBehaviorStyle" />

<TextView style="@style/customBehaviorStyle" />

<TextView style="@style/customBehaviorStyle" />

<TextView style="@style/customBehaviorStyle" />

<TextView style="@style/customBehaviorStyle" />

<TextView style="@style/customBehaviorStyle" />

<TextView style="@style/customBehaviorStyle" />

<TextView style="@style/customBehaviorStyle" />

<TextView style="@style/customBehaviorStyle" />

</android.support.v7.widget.LinearLayoutCompat>

</android.support.v4.widget.NestedScrollView>

</android.support.design.widget.CoordinatorLayout>

2. 自定义Behavior

相对而言,此种方式比较简单,因为我们之前就已经实现过一次。

但是针对于NestedScrollView,我们需要监听如下三种事件:

  • onStartNestedScroll():滑动的开始时进行回调;
  • onNestedPreScroll():判断当前滑动方向;
  • onNestedFling():快速滑动的惯性移动监听事件,指的是松开手指后还会有滑动效果;

而关于滑动方向,我们只关心垂直滑动,其他的交给系统处理即可。

public class SyncScrollBeahvior extends CoordinatorLayout.Behavior<View> {

public SyncScrollBeahvior(Context context, AttributeSet attrs) {
super(context, attrs);
}

@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
return (nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL)
|| super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
}

@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
// child 要执行动画
int scrollY= target.getScrollY();
child.setScrollY(scrollY);
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
}

// 快速滑动的惯性移动 松开手指后还会有滑动效果
@Override
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float velocityY, boolean consumed) {
((NestedScrollView)child).fling((int) velocityY);
return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
}
}

GitHub查看地址

​​https://github.com/HLQ-Struggle/MaterialDesignStudy​​

举报

相关推荐

0 条评论