CoordinatorLayout和 Behavior 的源码分析及使用

2,032 阅读18分钟

CoordinatorLayout

终于到这个控件了,其实我的内心是忐忑的,因为我其实一直想要深入的理解 CoordinatorLayout+Behavior的原理,但是又苦于太难懂了,以前也零零碎碎研究过几次,最后都以失败告终。这次是没办法,MaterialDesign 篇到这里也快结束了,做事还是要有始有终,于是这两天好好研究了一下,发现这东西其实也没那么复杂。

CoordinatorLayout直接继承了ViewGroup,说明最少重写了 onMeasure和 onLayout 方法,说不定还有 onMeasureChild和 onLayoutChild 方法。然后我们在看,CoordinatorLayout 是用来处理嵌套滑动的,那么onTouchEvent()和 onInterceptTouchEvent()方法肯定也跑不掉。

好了,按照惯例,我们先从构造方法以及 attributes 属性开始看吧。

attributes

<declare-styleable name="CoordinatorLayout">
    <attr format="reference" name="keylines"/>
    <attr format="reference" name="statusBarBackground"/>
</declare-styleable>

<declare-styleable name="CoordinatorLayout_Layout">
    <attr name="android:layout_gravity"/>
    <attr format="string" name="layout_behavior"/>
    <attr format="reference" name="layout_anchor"/>
    <attr format="integer" name="layout_keyline"/>
    <attr name="layout_anchorGravity">
        <flag name="top" value="0x30"/>            
        <flag name="bottom" value="0x50"/>            
        <flag name="left" value="0x03"/>            
        <flag name="right" value="0x05"/>            
        <flag name="center_vertical" value="0x10"/>            
        <flag name="fill_vertical" value="0x70"/>           
        <flag name="center_horizontal" value="0x01"/>            
        <flag name="fill_horizontal" value="0x07"/>            
        <flag name="center" value="0x11"/>          
        <flag name="fill" value="0x77"/>            
        <flag name="clip_vertical" value="0x80"/>            
        <flag name="clip_horizontal" value="0x08"/>            
        <flag name="start" value="0x00800003"/>            
        <flag name="end" value="0x00800005"/>
    </attr>
    <attr format="enum" name="layout_insetEdge">          
        <enum name="none" value="0x0"/>            
        <enum name="top" value="0x30"/>            
        <enum name="bottom" value="0x50"/>            
        <enum name="left" value="0x03"/>            
        <enum name="right" value="0x03"/>            
        <enum name="start" value="0x00800003"/>            
        <enum name="end" value="0x00800005"/>
    </attr>
    <attr name="layout_dodgeInsetEdges">            
        <flag name="none" value="0x0"/>            
        <flag name="top" value="0x30"/>            
        <flag name="bottom" value="0x50"/>            
        <flag name="left" value="0x03"/>            
        <flag name="right" value="0x03"/>            
        <flag name="start" value="0x00800003"/>            
        <flag name="end" value="0x00800005"/>            
        <flag name="all" value="0x77"/>
    </attr></declare-styleable>

可以直接设置在 CoordinatorLayout 节点上的属性有两个

  • keylines 一个比较奇怪的属性,好像是一个布局解决方案吧。比较鸡肋,没有人用过它
  • statusBarBackground 状态栏背景颜色

剩下的都是只能作用在 CoordinatorLayout 的直接子节点上的属性

  • layout_behavior 这个属性大家都很熟悉,因为Behavior 是嵌套滑动的精华。辅助Coordinator对View进行layout、nestedScroll的处理
  • layout_anchor 将其固定在某个 view 上面,可以理解成依附
  • layout_keyline 同上
  • layout_anchorGravity 这个容易理解,依附在控件上的位置
  • layout_insetEdge 用于避免布局之间互相遮盖
  • layout_dodgeInsetEdges 用于避免布局之间互相遮盖

布局这一块没什么说的,CoordinatorLayout作为顶级节点,然后根据实际需求使用对应的控件和属性就行了,这里我就不做过多的赘述。

源码

首先,刚刚我们猜测了肯定会重写 onMeasure 方法,那么我们就从 onMeasure 方法开始看。 onMeasure 方法里面有这么一段方法,我 copy 出来给大家看一下

for (int i = 0; i < childCount; i++) {
    final View child = mDependencySortedChildren.get(i);
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();

    int keylineWidthUsed = 0;
    if (lp.keyline >= 0 && widthMode != MeasureSpec.UNSPECIFIED) {
        final int keylinePos = getKeyline(lp.keyline);
        final int keylineGravity = GravityCompat.getAbsoluteGravity(
                    resolveKeylineGravity(lp.gravity), layoutDirection)
                    & Gravity.HORIZONTAL_GRAVITY_MASK;
        if ((keylineGravity == Gravity.LEFT && !isRtl)
                    || (keylineGravity == Gravity.RIGHT && isRtl)) {
                keylineWidthUsed = Math.max(0, widthSize - paddingRight - keylinePos);
        } else if ((keylineGravity == Gravity.RIGHT && !isRtl)
                    || (keylineGravity == Gravity.LEFT && isRtl)) {
                keylineWidthUsed = Math.max(0, keylinePos - paddingLeft);
        }
    }

    int childWidthMeasureSpec = widthMeasureSpec;
    int childHeightMeasureSpec = heightMeasureSpec;
    if (applyInsets && !ViewCompat.getFitsSystemWindows(child)) {
        // We're set to handle insets but this child isn't, so we will measure the
        // child as if there are no insets
        final int horizInsets = mLastInsets.getSystemWindowInsetLeft()
                    + mLastInsets.getSystemWindowInsetRight();
        final int vertInsets = mLastInsets.getSystemWindowInsetTop()
                    + mLastInsets.getSystemWindowInsetBottom();

        childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                    widthSize - horizInsets, widthMode);
        childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                    heightSize - vertInsets, heightMode);
    }

    final Behavior b = lp.getBehavior();
    if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
                childHeightMeasureSpec, 0)) {
            onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
                    childHeightMeasureSpec, 0);
    }

    widthUsed = Math.max(widthUsed, widthPadding + child.getMeasuredWidth() +
                lp.leftMargin + lp.rightMargin);

    heightUsed = Math.max(heightUsed, heightPadding + child.getMeasuredHeight() +
                lp.topMargin + lp.bottomMargin);
    childState = ViewCompat.combineMeasuredStates(childState,
                ViewCompat.getMeasuredState(child));
}

这是一段遍历子 View 的操作,首先判断 keyLine,这个属性我们不关心,直接跳过,然后就是获取子 view 的 Behavior,然后判断是否为空,在根据 Behavior 去 measure 子 view。这里我们能看到子 view 的 Behavior 是保存在 LayoutParams里面的,所以这个 LayoutParams 肯定是重写的。然后我们 Behavior 一般是直接写到 xml 布局的子节点上对吧,所以可以判断子 view 的 Behavior 是在View 解析 xml 的时候,读取到 Behavior 节点,然后赋值给 LayoutParams。LayoutInflate 的源码我就不带着大家去读了,我贴出关键代码

ViewGroup.LayoutParams params = null;
try {
    params = group.generateLayoutParams(attrs);
} catch (RuntimeException e) {
    // Ignore, just fail over to child attrs.
}

这里的group 就是 parent强转的,子 View 的 LayoutParams 是通过父 view 的generateLayoutParams()创建,于是我们去看 CoordinatorLayout 的generateLayoutParams方法。

@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new LayoutParams(getContext(), attrs);
}

额,尴尬了,直接去看构造方法把

LayoutParams(Context context, AttributeSet attrs) {
    super(context, attrs);

    final TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.CoordinatorLayout_Layout);

    this.gravity = a.getInteger(
                R.styleable.CoordinatorLayout_Layout_android_layout_gravity,
                Gravity.NO_GRAVITY);
    mAnchorId = a.getResourceId(R.styleable.CoordinatorLayout_Layout_layout_anchor,
                View.NO_ID);
    this.anchorGravity = a.getInteger(
                R.styleable.CoordinatorLayout_Layout_layout_anchorGravity,
                Gravity.NO_GRAVITY);

     this.keyline = a.getInteger(R.styleable.CoordinatorLayout_Layout_layout_keyline,
                -1);

    insetEdge = a.getInt(R.styleable.CoordinatorLayout_Layout_layout_insetEdge, 0);
        dodgeInsetEdges = a.getInt(
                R.styleable.CoordinatorLayout_Layout_layout_dodgeInsetEdges, 0);
    mBehaviorResolved = a.hasValue(
                R.styleable.CoordinatorLayout_Layout_layout_behavior);
    if (mBehaviorResolved) {
        mBehavior = parseBehavior(context, attrs, a.getString(
                    R.styleable.CoordinatorLayout_Layout_layout_behavior));
    }
    a.recycle();

    if (mBehavior != null) {
        // If we have a Behavior, dispatch that it has been attached
            mBehavior.onAttachedToLayoutParams(this);
    }
}

好,这里我们可以看到,我们之前设置的一些layout_anchor、anchorGravity、layout_keyline、layout_behavior等属性,我就不过多赘述了,今天的重点是 Behavior 呢,我们看parseBehavior()方法,这个方法创建了一个 Behavior

static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
    if (TextUtils.isEmpty(name)) {
        return null;
    }

    final String fullName;
    if (name.startsWith(".")) {
        // 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) {
            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);
    }
}

可以看到,内部是通过反射的方式创建的Behavior,然后调用的两个参数的构造函数,所以如果想要试用behavior就必须实现它的构造函数,不然就会报异常。哈哈,反正我第一次创建 Behavior 的时候运行时报错了,说找不到两个参数的构造方法。

好,接下来开始到重点了。measure 方法结束了之后,应该开始布局了,所以我们解析来去看 onLayout()方法

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    final int layoutDirection = ViewCompat.getLayoutDirection(this);
    final int childCount = mDependencySortedChildren.size();
    for (int i = 0; i < childCount; i++) {
        final View child = mDependencySortedChildren.get(i);
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        final Behavior behavior = lp.getBehavior();

        if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
            onLayoutChild(child, layoutDirection);
        }
    }
}

遍历子 view,如果 behavior.onLayoutChild()方法返回true,则不会调用 CoordinatorLayout 的 onLayouChild()方法,由此可得出结论,重写 Behavior 的 onLayoutChild 方法是用来自定义当前 View 的布局方式

此时,布局结束,我们的 CoordinatorLayout 静态页面已经完成,接下来,我们要看的是滑动的时候,CoordinatorLayout 怎么处理。
我们来简单回顾一下 ViewGroup 的事件分发机制,首先 disPatchTouchEvent()被调用,然后调用 onInterceptTouchEvent 判断是否允许事件往下传,如果允许则丢给子 View的disPatchTouchEvent 来处理,如果不允许或者允许后子 view没有消费掉事件,则 先后调用自己的 onTouchListener 和 OnTouchEvent来消费事件。
然后我们来根据这个顺序看 CoordinatorLayout 的事件处理顺序,首先看 disPatchTouchEvent 方法, 这个方法,没有重写,那么略过直接看 onInterceptTouchEvent 方法。

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    MotionEvent cancelEvent = null;

    final int action = MotionEventCompat.getActionMasked(ev);

    // Make sure we reset in case we had missed a previous important event.
    if (action == MotionEvent.ACTION_DOWN) {
    //重置状态
        resetTouchBehaviors();
    }

    final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);

    if (cancelEvent != null) {
        cancelEvent.recycle();
    }

    if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
    //重置状态
        resetTouchBehaviors();
    }

    return intercepted;
}

没什么好说的,继续追performIntercept()方法

private boolean performIntercept(MotionEvent ev, final int type) {
    boolean intercepted = false;
    boolean newBlock = false;

    MotionEvent cancelEvent = null;

    final int action = MotionEventCompat.getActionMasked(ev);

    final List<View> topmostChildList = mTempList1;
    getTopSortedChildren(topmostChildList);

    // Let topmost child views inspect first
    final int childCount = topmostChildList.size();
    for (int i = 0; i < childCount; i++) {
        final View child = topmostChildList.get(i);
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        final Behavior b = lp.getBehavior();

        if ((intercepted || newBlock) && action != MotionEvent.ACTION_DOWN) {
            // Cancel all behaviors beneath the one that intercepted.
            // If the event is "down" then we don't have anything to cancel yet.
            if (b != null) {
                if (cancelEvent == null) {
                    final long now = SystemClock.uptimeMillis();
                    cancelEvent = MotionEvent.obtain(now, now,
                            MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
                }
                switch (type) {
                    case TYPE_ON_INTERCEPT:
                        b.onInterceptTouchEvent(this, child, cancelEvent);
                        break;
                    case TYPE_ON_TOUCH:
                        b.onTouchEvent(this, child, cancelEvent);
                        break;
                }
            }
            continue;
        }

        if (!intercepted && b != null) {
            switch (type) {
                case TYPE_ON_INTERCEPT:
                    intercepted = b.onInterceptTouchEvent(this, child, ev);
                    break;
                case TYPE_ON_TOUCH:
                    intercepted = b.onTouchEvent(this, child, ev);
                    break;
            }
            if (intercepted) {
                mBehaviorTouchView = child;
            }
        }

        // Don't keep going if we're not allowing interaction below this.
        // Setting newBlock will make sure we cancel the rest of the behaviors.
        final boolean wasBlocking = lp.didBlockInteraction();
        final boolean isBlocking = lp.isBlockingInteractionBelow(this, child);
        newBlock = isBlocking && !wasBlocking;
        if (isBlocking && !newBlock) {
            // Stop here since we don't have anything more to cancel - we already did
            // when the behavior first started blocking things below this point.
            break;
        }
    }

    topmostChildList.clear();

    return intercepted;
}

遍历所有子 View,调用了符合条件的 view 的 Behavior.onInterceptTouchEvent/onTouchEvent方法 然后我们来看 onTouchEvent 方法

@Override
public boolean onTouchEvent(MotionEvent ev) {
    boolean handled = false;
    boolean cancelSuper = false;
    MotionEvent cancelEvent = null;

    final int action = MotionEventCompat.getActionMasked(ev);

    if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) {
        // Safe since performIntercept guarantees that
        // mBehaviorTouchView != null if it returns true
        final LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams();
        final Behavior b = lp.getBehavior();
        if (b != null) {
            handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
        }
    }

    // Keep the super implementation correct
    if (mBehaviorTouchView == null) {
        handled |= super.onTouchEvent(ev);
    } else if (cancelSuper) {
        if (cancelEvent == null) {
            final long now = SystemClock.uptimeMillis();
            cancelEvent = MotionEvent.obtain(now, now,
                    MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
        }
        super.onTouchEvent(cancelEvent);
    }

    if (!handled && action == MotionEvent.ACTION_DOWN) {

    }

    if (cancelEvent != null) {
        cancelEvent.recycle();
    }

    if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
        resetTouchBehaviors();
    }

    return handled;
}

重点在if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))这句话上,如果先前有子 view 的Behavior 的 onInterceptTouchEvent 返回了 true,则直接调用这个子 view 的 Behavior 的 onTouchEvent。否则就继续走一遍performIntercept(ev, TYPE_ON_TOUCH),即:执行所有含有 Behavior 的子 view 的 Behavior.onTouchEvent方法。

咳咳~~好了, 上面两个方法的各种逻辑判断有点绕,我也是被绕了很久,没看懂没事,直接看杰伦 我们再来回过头看这两个方法,其最终都是调用了 Behavior 的 onInterceptTouchEvent 和 onTouchEvent 方法,然后各种条件判断就是什么时候调用这两个方法。

  • onInterceptTouchEvent
    1.在 CoordinatorLayout 的 onInterceptTouchEvent 方法中杯调用 。
    2.调用顺序:按照 CoordinatorLayout 中 child 的添加倒叙进行调用
    3.运行原理:
    如果此方法在 down 事件返回 true,那么它后面的 view 的 Behavior 都执行不到此方法;并且执行 onTouchEvent 事件的时候只会执行此 view 的 Behavior 的 onTouchEvent 方法。
    如果不是 down 事件返回 true,那么它后面的 view 的 Behavior 的 onInterceptTouchEvent 方法都会执行,但还是只执行第一个 view 的 Behavior 的 onTouchEvent 方法
    如果所有的 view 的 Behavior 的onInterceptTouchEvent 方法都没有返回 true,那么在 CoordinatorLayout 的 onTouchEvent 方法内会回调所有 child 的 Behavior 的 onTouchEvent 方法
    4.CoordinatorLayout 的 onInterceptTouchEvent 默认返回 false,返回值由child 的 Behavior 的 onInterceptTouchEvent 方法决定

  • onTouchEvent
    1.在 CoordinatorLayout 的 onTouchEvent 方法中被调用
    2.调用顺序:同上
    3.在上面 onInterceptTouchEvent 提到的所有 Behavior 的 onTouchEvent 都返回 false 的情况下,会遍历所有 child 的此方法,但是只要有一个 Behavior 的此方法返回 true,那么后面的所有 child 的此方法都不会执行
    4.CoordinatorLayout 的 onTouchEvent默认返回super.onTouchEvent(),如果有 child 的 Behavior 的此方法返回 true,则返回 true。

然后再来说一下嵌套滑动把,我们都知道 CoordinatorLayout 的内嵌套滑动只能用 NestedScrollView 和 RecyclerView,至于为什么呢。我相信很多人肯定点开过 NestedScrollView 和 RecyclerView 的源码,细心的同学肯定会发现这两个类都实现了NestedScrollingChild接口,而我们的 CoordinatorLayout 则实现了NestedScrollingParent的接口。这两个接口不是这篇文章的重点,我简单说一下,CoordinatorLayout 的内嵌滑动事件都是被它的子NestedScrollingChild实现类处理的。而子View 在滑动的时候,会调用NestedScrollingParent的方法,于是 CoordinatorLayout 再NestedScrollingParent的实现方法中,调用了 Behavior 的对应方法。

总结

好了,分析到这里,其实我感觉,我们更应该去了解一下NestedScrollingParent和NestedScrollingChild的嵌套滚动机制。简单点说,就是 child(RecycleView) 在滚动的时候调用了 parent(CoordinatorLayout) 的 对应方法,而我们的 Behavior,则是在 parent 的回调方法中,处理了其他child 的伴随变化。 本质上,我们可以通过自定义控件的方式实现,但是 Google帮我们封装的这一套控件很解耦啊、很牛逼啊、很方便啊,所以我用它。


Behavior

直接看官网描述吧

  • Interaction behavior plugin for child views of CoordinatorLayout. 对 CoordinatorLayout 的 child 交互行为的插件。

  • 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. 一个child 的Behavior 可以实现一个或者多个 child 的交互,这些交互可以包括拖动、滑动、惯性以及其他手势。

按照国际惯例,我们应该先看 Public methods。但是,As a Developer,我们是不需要持有 Behavior 引用的。所有的 Behavior CoordinatorLayout都已经帮我们管理好了,所以,我们可以先不用关心公共方法。

好了,那我们聊两点大家需要关心的。

Dependent

Dependent: adj.依赖的

顾名思义,Dependent 就是依赖的意思。在 Behavior 中,就是使某个 View依赖一个指定的 view,使得被依赖的 view 的大小位置改变发生变化的时候,依赖的 view 也可以做出相应的动作。 常见的方法有两个

  • public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency)

确定所提供的child 是否有另一个特点兄弟 View 的依赖
在一个CoordinatorLayout 布局里面,这个方法最少会被调用一次,如果对于一个给定的 child 和依赖返回 true,则父CoordinatorLayout 将:
1.在被依赖的view Layout 发生改变后,这个 child 也会重新 layout。
2.当依赖关系视图的布局或位置变化时,会调用 onDependentViewChange

  • public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency)

响应依赖 view 变化的方法
无论是依赖 view的尺寸、大小或者位置发生改变,这个方法都会被调用,一个 Behavior 可以使用此方法来适当的更新响应child
view 的依赖关系由layoutDependsOn 或者child 设置了another属性来确定。
如果 Behavior 改变了 child 的大小或位置,它应该返回 true,默认返回 false。

好了,说了这么久,我们来动手写一个小 Demo 吧。 大家都知道 FloatActionBar 在 CoordinatorLayout 布局中,处于屏幕底部时,然后 SnackBar 弹出来之后,会自动把 FloatActionBar 顶上去。就像酱紫

FloatActionBar.gif
FloatActionBar.gif

布局文件和代码都贼简单~如下

<android.support.design.widget.CoordinatorLayout
 xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:tools="http://schemas.android.com/tools"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 xmlns:app="http://schemas.android.com/apk/res-auto"
 tools:context=".TestActivity">

 <android.support.design.widget.FloatingActionButton
    android:id="@+id/bt"
    android:layout_gravity="bottom|right"
    app:layout_behavior=".behavior.FABBehavior"
    android:layout_width="wrap_content"
    android:text="FloatActionBar"
    android:layout_height="wrap_content"/>

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

findViewById(R.id.bt).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Snackbar.make(v,"自定义 FloatActionBar", 1500).show();
        }
    });

这里就不一步一步去纠结了,FloatingActionButton内部有一个默认的 Behavior,这个 Behavior 实现了很多效果,我们先跳过吧。 然后我们来自定义一个 FloatingActionButton,实现 SnackBar 弹出的时候顶上去。

实现思路:我们要在SnackBar 在弹出的时候跟着一起往上位移,也就是说我们要监听 SnackBar 的位移事件,那么我们可以layoutDependsOn判断目前发生变化的 view 是不是 SnackBar,如果是,则返回 true,onDependentViewChanged方法,在onDependentViewChanged里面改变 MyFloatingActionButton 的位置。

我要自己实现的效果:

MyFloatActionBar.gif
MyFloatActionBar.gif

代码实现,xml 布局(这里我图方便,就用一个 Button 代替了,样式也没改。。。主要是因为懒)

<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".TestActivity">

<Button
    android:id="@+id/bt"
    android:layout_gravity="bottom|right"
    app:layout_behavior=".behavior.FABBehavior"
    android:layout_width="wrap_content"
    android:text="FloatActionBar"
    android:layout_height="wrap_content"/>
</android.support.design.widget.CoordinatorLayout>

这里其实就是一个 CoordinatorLayout 里面包裹了一个 Button,关键代码就是“app:layout_behavior=".behavior.FABBehavior"”,然后我们需要去写这个 FABBehavior 就行了。逻辑很简单,代码如下:

public class FABBehavior extends CoordinatorLayout.Behavior {

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

@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
    return dependency instanceof Snackbar.SnackbarLayout;
}

@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
    float translationY = Math.min(0, dependency.getTranslationY() - dependency.getHeight());
    child.setTranslationY(translationY);
    return true;
}
}

很简单的代码逻辑,我就不写注释了,这里记得写两个参数的构造方法就行了。

然后我们再来看一个基于 Dependent 的 Demo

BottomBehavior.gif
BottomBehavior.gif

好了,直接贴代码吧,很简单的。

<?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"
android:layout_width="match_parent"
android:layout_height="match_parent">

<android.support.design.widget.AppBarLayout
    android:id="@+id/appbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">


    <android.support.v7.widget.Toolbar
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        app:layout_scrollFlags="scroll|enterAlways"
        app:navigationIcon="@mipmap/abc_ic_ab_back_mtrl_am_alpha"
        app:title="标题">

    </android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>

<android.support.v7.widget.RecyclerView
    android:id="@+id/recyclerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:tag="Tag"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"/>


<TextView
    android:layout_width="match_parent"
    android:layout_height="48dp"
    android:layout_gravity="bottom"
    android:background="@color/colorPrimary_pink"
    android:gravity="center"
    android:text="我是底部导航栏"
    android:textColor="@color/white"
    app:layout_behavior=".behavior.BottomBehavior"/>
</android.support.design.widget.CoordinatorLayout>

好了,很简单的布局,需要注意的是,这个的 RecycleView 也设置了一个 Behavior 哦,这个 Behavior 的使用我们在上一篇博客已经讲过了哦。然后给我们的“底部导航栏设置一个 Behavior,使其在 RecycleView 向上滚动的时候能够隐藏起来。直接贴代码吧

public class BottomBehavior extends CoordinatorLayout.Behavior {
public BottomBehavior(Context context, AttributeSet attrs) {
    super(context, attrs);
}

//依赖 AppBarLayout 的写法
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
    return dependency instanceof AppBarLayout;
}

@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
    int delta = dependency.getTop();
    child.setTranslationY(-delta);
    return true;
}

//-----------依赖 RecycleView 的写法-----------
//    @Override
//    public boolean layoutDependsOn(CoordinatorLayout parent, final View child, View dependency) {
//        boolean b = "Tag".equals(dependency.getTag());
//        if (b && this.child == null) {
//            this.child = child;
//            ((RecyclerView) dependency).addOnScrollListener(mOnScrollListener);
//        }
//        return b;
//    }
//
//    View child;
//    RecyclerView.OnScrollListener mOnScrollListener = new RecyclerView.OnScrollListener() {
//        @Override
//        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
//            super.onScrolled(recyclerView, dx, dy);
//            float translationY = child.getTranslationY();
//            translationY += dy;
//            translationY = translationY > 0 ? (translationY > child.getHeight() ? child.getHeight() : translationY) : 0;
//            Log.e("translationY", translationY + "");
//            child.setTranslationY(translationY);
//        }
//    };

}

这里我用了两种实现方式,推荐使用第一种。因为第二种其实在 Activity 里面也是可以实现的。之所以讲第二种写法是因为,我们在layoutDependsOn里面是可以获取到 CoordinatorLayout 里面所有child 的引用,直接 CoordinatorLayout.findViewWithTag()即可,然后我们可以为所欲为,哈哈哈,看实际需求吧。

Nested

Nested 机制要求 CoordinatorLayout 包含了一个实现了 NestedScrollingChild 接口的滚动控件,如 RecycleView、NestedScrollView、SwipeRefreshLayout等。然后我们 Behavior 里面的如下几个方法会被回调哦~

onStartNestedScroll(View child, View target, int nestedScrollAxes)
onNestedPreScroll(View target, int dx, int dy, int[] consumed)
onNestedPreFling(View target, float velocityX, float velocityY)
onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed)
onNestedFling(View target, float velocityX, float velocityY, boolean consumed)
onStopNestedScroll(View target)

哦,对了,onStartNestedScroll 方法返回 ture表示要接受这个事件,后面的方法才会被调用哦。 看名字应该都能看得懂这些方法在哪里会被回调吧,好了,那我们来实现一个小 Demo 吧,看图~

NestedDemo.gif
NestedDemo.gif

还是上面这个 Demo,xml 布局里面添加一个 FAB,给 FAB 设置一个 Tag,然后CoordinatorLayout.findViewWithTag()找到 FAB 的引用,onStarNestedScroll 返回 true 表示要接受这个事件,onNestedPreScroll 里面根据滚动的参数 dy 判断是上滑还是下拉,然后调用 FAb 的 hide 和 show 方法即可。

public class BottomBehavior extends CoordinatorLayout.Behavior {
public BottomBehavior(Context context, AttributeSet attrs) {
    super(context, attrs);
}

FloatingActionButton mFab;

@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
    if (mFab == null)
        mFab = (FloatingActionButton) parent.findViewWithTag("FAB");
    return dependency instanceof AppBarLayout;
}

@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
    int delta = dependency.getTop();
    child.setTranslationY(-delta);
    return true;
}

@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
    return true;
}

@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
    if (dy>10){
        mFab.hide();
    }else if (dy<-10){
        mFab.show();
    }
}
}

好了,写完这几个 Demo,Behavior 的知识点算是基本上讲完了,可能有些同学还是一头雾水。Wtah?这就讲完了? 我还没学会定制那些牛逼的 Behavior动画。 好吧,其实我也是先学了简书、掘金上面好几个热门的 Behavior 应用,才开始研究源码的。说实话,在写这篇文章之前,我真的不会用 Behavior。

废话说得有点多,没讲重点。那我先说重点吧,看懂了 Behavior 的原理再去看 Behavior 的 Demo 简直不要太简单。其实就是一些基本功底和一些属性动画的集成,让就成了高大上的 Behavior 动画。

来吧,分析几个别人的 Behavior Demo。 分析之前先郑重声明:1、我没有获得作者的授权,如果觉得我侵权了,请联系我。2、如果有评论措辞不当的地方还请多多包涵,仅代表个人观点,不针对任何人。3、还没想好,以后想到再添加


Demo1

Demo1.gif
Demo1.gif

原文地址:传送门

我的项目中也用到了这个效果,当时我是基于 RecycleView 做了二次封装实现的。蜜汁尴尬,看到前两天学习 Behavior 的时候果断重构了代码。

Demo 实现分析:就是一个 AppBarLayout 使用了ScrollFlags 属性实现了折叠效果,然后CollapsingToolbarLayout实现了图片的视差滚动。关于 AppBarLayout 的使用请看我上一篇文章。然后再加了一个下拉放大的效果,这个需要我们自己定制。
1.继承AppBarLayout.Behavior,在任意包含 CoordinatorLayout 的方法里面获取使用 CoordinatorLayout.findViewWithTag()方法获取到需要下拉放大的 ImageView,记得设置 ImageView 的ClipChildren属性为 false。(不知道这个属性功能的朋友自行找度娘或者 Google)
2.重新onNestedPreScroll,在这个方法里面去根据下拉距离Scale ImageView 即可。
3.手指松了之后还原,一个属性动画解决
4.没有4了,已经实现,具体代码去看别人的源码吧,真的很容易实现,只是不知道的人不会而已。


Demo2

Demo2.gif
Demo2.gif

哈哈,是不是很酷炫,反正当时我看完之后感觉这一定是一个很酷炫的动画。
不用,其实两个 Behavior 就可以搞定了。
先来分析一下有哪些效果吧。
1.下拉放大封面图片
2.上拉头像跟着个人信息栏目往上走
3.上拉头像缩小
4.个人信息栏和工具栏视差滚动
5.RecycleView 等其他栏目折叠完成再滚动
6.TabLayout 固定在顶部
实现:
1.参考上一个 Demo
2.给头像设置layout_anchor属性悬挂在某个控件上即可
3.给头像设置一个 Behavior,使起 DependentAppBarLayout ,然后 Scale 即可。
4.给两个 view 设置不同的layout_collapseParallaxMultiplier 系数即可,不知道这个属性的回头看我上一篇博客
5.给 RecycleView 设置AppBarLayout.ScrollingViewBehavior
6.可以放在 AppBarLayout 里面,layoutflag 不设置 scroll 属性即可。

具体实现请参考源码:传送门


Demo3

Demo3.gif
Demo3.gif
传送门
哈哈,大家自己去看吧,这些效果的实现用到的知识点我的 MaterialDesign系列都讲过的。
另外,这几个 Demo 以及我文中的那几个 Demo,大家记得都去敲一遍代码,敲了一篇就会了,真的,不骗你,

有话说

MaterialDesign 系列到这里就结束了,以后有时间再补充一下UI 篇吧。不过我这方面我也还是一知半懂的状态,等以后有心得了会动笔的,这里我推荐扔物线的 HenCoder,我也在跟着学,业界良心之作,看完之后再也跟自定义 View 说 “No”,产品再也不会说“ 为什么 iOS 都实现了你们 Android 实现不了”。