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

前言


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

原理


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

协调者布局的功能

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

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

对应关系如下

什么是 Behavior 插件

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

CoordinatorLayout 下依赖交互原理

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

layoutDependsOn

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

onDependentViewChanged

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

CoordinatorLayout 下的嵌套滑动原理

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

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

CoordinatorLayout 下子控件的测量与布局

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

CoordinatorLayout 下子控件的事件拦截

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

CoordinatorLayout 源码解析

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

scss 复制代码
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 接口;

typescript 复制代码
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
    @Override
    public boolean onPreDraw() {
        onChildViewsChanged(EVENT_PRE_DRAW);
        return true;
    }
}

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

arduino 复制代码
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 方法的执行;

ini 复制代码
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 的方法

ini 复制代码
final Behavior b = checkLp.getBehavior();

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

less 复制代码
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 是什么?

swift 复制代码
private final List<View> mDependencySortedChildren = new ArrayList<>();

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

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

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

swift 复制代码
private final DirectedAcyclicGraph<View> mChildDag = new DirectedAcyclicGraph<>();

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

ini 复制代码
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 实战

国际惯例,先上效果:

布局实现如下:

ini 复制代码
<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 的实现如下:

csharp 复制代码
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 的实现:

less 复制代码
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

欢迎三两

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

相关推荐
测开小菜鸟14 分钟前
使用python向钉钉群聊发送消息
java·python·钉钉
数据猎手小k1 小时前
AndroidLab:一个系统化的Android代理框架,包含操作环境和可复现的基准测试,支持大型语言模型和多模态模型。
android·人工智能·机器学习·语言模型
P.H. Infinity1 小时前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq
生命几十年3万天1 小时前
java的threadlocal为何内存泄漏
java
caridle1 小时前
教程:使用 InterBase Express 访问数据库(五):TIBTransaction
java·数据库·express
^velpro^2 小时前
数据库连接池的创建
java·开发语言·数据库
你的小102 小时前
JavaWeb项目-----博客系统
android
苹果醋32 小时前
Java8->Java19的初步探索
java·运维·spring boot·mysql·nginx
秋の花2 小时前
【JAVA基础】Java集合基础
java·开发语言·windows
小松学前端2 小时前
第六章 7.0 LinkList
java·开发语言·网络