likes
comments
collection
share

如何应对Android面试官-> CoordinatorLayout详解,我用 Behavior 实现了手势跟随

作者站长头像
站长
· 阅读数 1

前言


本章主要讲解下 CoordinatorLayout 的基础用法、工作原理和自定义Behavior

原理


使用很简单,百度上可以搜索下基础使用

如何应对Android面试官-> CoordinatorLayout详解,我用 Behavior 实现了手势跟随

协调者布局的功能

  1. 作为应用的顶层布局
  2. 作为一个管理容器,管理与子 View 或者子 View 之间的交互
  3. 处理子控件之间依赖下的交互
  4. 处理子控件之间的嵌套滚动
  5. 处理子控件的测量和布局
  6. 处理子控件的事件拦截与响应

以上 3、4、5、6的支持全部基于 CoordinatorLayout 中提供了一个叫作 Behavior 的插件,Behavior 内部也提供了相应的方法来对应这四个不同的功能;

对应关系如下

如何应对Android面试官-> CoordinatorLayout详解,我用 Behavior 实现了手势跟随

什么是 Behavior 插件

CoordinatorLayout 可以看做一个平台,在这个平台下的 ChildView 想要具备什么行为,就使用什么 Behavior(插件),集成不同的插件,实现不同的功能;

如何应对Android面试官-> CoordinatorLayout详解,我用 Behavior 实现了手势跟随

CoordinatorLayout 下依赖交互原理

当 CoordinatorLayout 中子控件 depandency 位置、大小发生改变的时候,那么在 CoordinatorLayout 内部会通知所有依赖 depandency 的控件,并调用对应声明的 Behavior,告知其依赖的 depandency 发生了改变。那么如何判断依赖(layoutDependsOn),接受到通知后如何处理(onDependentViewChanged/onDependentViewRemoved),这些都交给 Behavior 来处理;

layoutDependsOn

@Override
public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
    // 判断是不是依赖的 View
    return dependency instanceof DependedView;
}

onDependentViewChanged

@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
    // 被依赖发生了变化,依赖的 child 做出相应的改变
    child.setY(dependency.getBottom() + 50);
    child.setX(dependency.getX());
    return true;
}

如何应对Android面试官-> CoordinatorLayout详解,我用 Behavior 实现了手势跟随

CoordinatorLayout 下的嵌套滑动原理

CoordinatorLayout 实现了 NestedScrollingParent2 接口。那么当事件(scroll或fling)产生后,内部实现了 NestedScrollingChild 接口的子控件会将事件分发给 CoordinatorLayout,CoordinatorLayout 又会将事件传递给所有的 Behavior,然后在 Behavior 中实现子控件的嵌套滑动;

如何应对Android面试官-> CoordinatorLayout详解,我用 Behavior 实现了手势跟随

相当于 NestedScrolling 机制(参与角色只有子控件和父控件),CoordinatorLayout 中的交互玩出了新高度,在 CoordinatorLayout 下的子控件可以与多个兄弟控件进行交互;

CoordinatorLayout 下子控件的测量与布局

CoordinatorLayout 主要负责的是子控件之间的交互,内部控件的测量与布局其实非常简单,在特殊情况下,如子控件需要处理宽高和布局的时候,那么交给 Behavior 内部的 onMeasureChild、onChildLayout 方法进行处理;

如何应对Android面试官-> CoordinatorLayout详解,我用 Behavior 实现了手势跟随

CoordinatorLayout 下子控件的事件拦截

也是一样的处理逻辑,当 CoordinatorLayout 内部的 onTouchEvent、onInterceptTouchEvent 被调用的时候,如果子控件需要处理相关事件,会通过 Behavior 的对应的方法交给子 View 进行处理;

如何应对Android面试官-> CoordinatorLayout详解,我用 Behavior 实现了手势跟随

CoordinatorLayout 源码解析

View 的生命周期开始是从 onAttachToWindow 开始的,所以我们可以直接进入 CoordinatorLayout 的 onAttachToWinodw 方法看下:

public void onAttachedToWindow() {
    super.onAttachedToWindow();
    resetTouchBehaviors(false);
    if (mNeedsPreDrawListener) {
        if (mOnPreDrawListener == null) {
            mOnPreDrawListener = new OnPreDrawListener();
        }
        // 关键点
        final ViewTreeObserver vto = getViewTreeObserver();
        vto.addOnPreDrawListener(mOnPreDrawListener);
    }
    //
    ...
    // 省略部分代码
}

这里有一个比较关键的点 ViewTreeObserver ,调用它的 addOnPreDrawListener 添加了一个监听,这是一个视图树监听器,

ViewTreeObserver

注册一个观察者来监听视图树,当视图树的布局、焦点、绘制、滚动等发生改变的时候,ViewTreeObserver 都会收到通知,ViewTreeObserver 不能被实例化,可以调用 View.getViewTreeObserver() 来获得;

ViewTreeObserver.onPreDrawListener 当视图树将要被绘制的时候,回调 onPreDraw 接口;

class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
    @Override
    public boolean onPreDraw() {
        onChildViewsChanged(EVENT_PRE_DRAW);
        return true;
    }
}

这里面会调用 onChildViewsChanged 方法,我们进入这个方法看下,这个方法传入了一个 int 类型的 type,这个 type 有三种类型

static final int EVENT_PRE_DRAW = 0;
static final int EVENT_NESTED_SCROLL = 1;
static final int EVENT_VIEW_REMOVED = 2;

页面将要绘制的时候,传 0;

页面滚动的时候,传 1;

页面移除的时候,传 2;

也就说以上这三个状态都会调用 onChildViewChanged 方法的执行;

final void onChildViewsChanged(@DispatchChangeEvent final int type) {
    final int layoutDirection = ViewCompat.getLayoutDirection(this);
    final int childCount = mDependencySortedChildren.size();
    final Rect inset = acquireTempRect();
    final Rect drawRect = acquireTempRect();
    final Rect lastDrawRect = acquireTempRect();
    // 遍历所有的子 View
    for (int i = 0; i < childCount; i++) {
        // 获取每一个子 View
        final View child = mDependencySortedChildren.get(i);
        //
        ...
        // 省略部分代码
        
        for (int j = i + 1; j < childCount; j++) {
            // 获取依赖的 View
            final View checkChild = mDependencySortedChildren.get(j);
            // 获取这个 View 的布局参数
            final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
            // 获取这 View 的 Behavior
            final Behavior b = checkLp.getBehavior();
            // 调用 layoutDependsOn 判断是不是要依赖的 View
            if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
                    // 只是进行了状态重置
                    checkLp.resetChangedAfterNestedScroll();
                    continue;
                }
                
                final boolean handled;
                switch (type) {
                    case EVENT_VIEW_REMOVED:
                        // 移除的时候收到通知后的处理
                        b.onDependentViewRemoved(this, checkChild, child);
                        handled = true;
                        break;
                    default:
                        handled = b.onDependentViewChanged(this, checkChild, child);
                        break;
                }
                // 滚动的时候收到通知后的处理
                if (type == EVENT_NESTED_SCROLL) {
                    checkLp.setChangedAfterNestedScroll(handled);
                }
            }
        }
    }  
    //
    ...
    // 省略部分代码

}

看到这里,就能解释为什么在依赖的控件下设置一个Behavior,DependedView 位置发生改变的时候能通知到对方;

我们接下来进入这个获取 Behavior 的方法

final Behavior b = checkLp.getBehavior();

可以看到 Behavior 的初始化是在 LayoutParams 的构造方法中实例化的:

public static class LayoutParams extends MarginLayoutParams {
    /**
     * A {@link Behavior} that the child view should obey.
     */
    Behavior mBehavior;
    
    LayoutParams(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        //
        ...
        // 省略部分代码
        
       // 实例化 Behavior 
       if (mBehaviorResolved) {
            mBehavior = parseBehavior(context, attrs, a.getString(
                R.styleable.CoordinatorLayout_Layout_layout_behavior));
        }
    }
}

我们接着来看下 mDependencySortedChildren 是什么?

private final List<View> mDependencySortedChildren = new ArrayList<>();

它是一个集合,用来存放所有的子 View,那么问题来了,通过获取 View 不是通过 getChildAt(i) 来获取吗,这里为什么要多此一举在搞一个集合呢?

因为在 CoordinatorLayout 中,它管理的并不单单是一个 View 了,它管理是 View -> 依赖view 这样的一个关系,是一个 1:N 的关系图;

它还管理着这个图的数据结构

private final DirectedAcyclicGraph<View> mChildDag = new DirectedAcyclicGraph<>();

它管理的就是 childView -> dependency 的有向无环关系图,然后将这个关系图添加到 mDependencySortedChildren 中,我们可以来看下它俩是如何添加的

private void prepareChildren() {
    mDependencySortedChildren.clear();
    mChildDag.clear();

    for (int i = 0, count = getChildCount(); i < count; i++) {
        // 这里它会拿到每个 View
        final View view = getChildAt(i);
        // 获取每个 View 的布局参数
        final LayoutParams lp = getResolvedLayoutParams(view);
        lp.findAnchorView(this, view);
        mChildDag.addNode(view);

        for (int j = 0; j < count; j++) {
            if (j == i) {
                continue;
            }
            final View other = getChildAt(j);
            // 寻找每个 View 的依赖关系
            if (lp.dependsOn(this, view, other)) {
                if (!mChildDag.contains(other)) {
                    // 添加到这个图数据结构中
                    mChildDag.addNode(other);
                }
                // 有向无环图需要一个边的概念
                mChildDag.addEdge(other, view);
            }
        }
    }

    mDependencySortedChildren.addAll(mChildDag.getSortedList());
    Collections.reverse(mDependencySortedChildren);
}

数据结构的概念这里先不详细讲解,感兴趣的后面单独写一篇,这里就是收集 View 获取依赖关系并保存到集合中;

Behavior 实战

国际惯例,先上效果:

如何应对Android面试官-> CoordinatorLayout详解,我用 Behavior 实现了手势跟随

布局实现如下:

<androidx.coordinatorlayout.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">


    <com.example.llc.android_r.coordinatorlayout.DependencyView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_gravity="center"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="150dp"
        android:layout_gravity="center"
        android:text="我是科比"
        android:textColor="@color/colorAccent"
        app:layout_behavior=".coordinatorlayout.FollowBehavior"/>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

就是 TextView 跟随 DependencyView 的移动而移动

DependencyView 的实现如下:

public class DependencyView extends androidx.appcompat.widget.AppCompatImageView {

    private float mLastX;
    private float mLastY;
    private final int mDragSlop;

    public DependencyView(Context context) {
        this(context, null);
    }

    public DependencyView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DependencyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setImageResource(R.mipmap.kobe);
        mDragSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }


    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mLastX = event.getX();
                mLastY = event.getY();
                break;

            case MotionEvent.ACTION_MOVE:
                int dx = (int) (event.getX() - mLastX);
                int dy = (int) (event.getY() - mLastY);
                if (Math.abs(dx) > mDragSlop || Math.abs(dy) > mDragSlop) {
                    ViewCompat.offsetTopAndBottom(this, dy);
                    ViewCompat.offsetLeftAndRight(this, dx);
                }
                mLastX = event.getX();
                mLastY = event.getY();
                break;
            default:
                break;

        }
        return true;
    }
}

一个简单的跟随手势移动而移动的自定义 ImageView

接下来我们来看下自定义 Behavior 的实现:

public class FollowBehavior extends CoordinatorLayout.Behavior<View> {

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

    @Override
    public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
        return dependency instanceof DependencyView;
    }

    @Override
    public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
        child.setX(dependency.getX());
        child.setY(dependency.getY() + 200);
        return true;
    }
}

实现也比较简单,就是在 onDependentViewChanged 回调的时候修改依赖View的 X 和 Y 的坐标值,从而实现跟随移动;

原理,其他的例如颜色的跟随变动等等也是参考这样实现;

简历润色

深度理解 CoordinatorLayout 原理,并可以自定义 Behavior

下一章预告

自定义双指缩放的 PhotoView

欢迎三两

来都来了,点个关注,点个赞吧,你的支持是我最大的动力~