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

news2024/11/23 19:43:08

前言


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

原理


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

在这里插入图片描述

协调者布局的功能

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

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

对应关系如下

infoflow 2024-01-30 15-28-46.png

什么是 Behavior 插件

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

infoflow 2024-01-31 09-48-25.png

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;
}

infoflow 2024-01-31 09-53-03.png

CoordinatorLayout 下的嵌套滑动原理

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

infoflow 2024-01-31 10-04-17.png

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

CoordinatorLayout 下子控件的测量与布局

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

infoflow 2024-01-31 10-16-28.png

CoordinatorLayout 下子控件的事件拦截

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

infoflow 2024-01-31 10-18-28.png

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 实战

国际惯例,先上效果:

在这里插入图片描述

布局实现如下:

<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

欢迎三两

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1422714.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ChatGPT可与自定义GPTs一起使用,智能AI代理时代来啦!

1月31日凌晨&#xff0c;OpenAI在社交平台公布了一个超强新功能&#xff0c;可以在ChatGPT中输入“GPTs名字”的方法&#xff0c;调用多个自定义GPTs一起协同工作。 例如&#xff0c;我想开发一款社交APP&#xff0c;1&#xff09;可以先用专业分析GPTs做一下市场调研&#xf…

呼吸灯--FPGA

目录 1.breath_led.v 2.tb_breath_led.v 呼吸灯就是从完全熄灭到完全点亮&#xff0c;再从完全点亮到完全熄灭。具体就是通过控制PWM的占空比控制亮灭程度。 绘制PWM波的步骤就是&#xff0c;首先灯是在第一个时钟周期保持高电平熄灭状态&#xff0c;在第二个时钟周期保持1/1…

OpenGL 入门(三)— Textures(纹理)

文章目录 前言纹理环绕方式纹理过滤多级渐远纹理(Mipmap)加载与创建纹理stb_image.h库生成纹理 应用纹理顶点着色器片元着色器完整脚本 纹理单元 前言 纹理(Texture)。纹理是一个2D图片&#xff08;甚至也有1D和3D的纹理&#xff09;&#xff0c;它可以用来添加物体的细节。 你…

Visual Studio无法调试Unity的可能原因和解决办法

问题描述&#xff1a; 在unity和vs都安装了相关插件的情况下&#xff0c;vs在启动了“附加到Unity”后却并没有进入调试模式。 可能的原因及解决办法&#xff1a; 1、Unity未设置成调试模式 将Unity编辑器的右下角这个debug标志设置成debug模式: 设置后变成了&#xff1a; 注…

最小步数模型

AcWing 1107. 魔板 #include <bits/stdc.h> using namespace std;char g[2][4]; const int N 10; unordered_map<string, pair<char, string> > pre; unordered_map<string, int> d;void Set(string s) {for(int i0; i<4; i) g[0][i] s[i];for(in…

深入理解指针(1)

⽬录&#xff1a; 1. 内存和地址 2. 指针变量和地址 3. 指针变量类型的意义 4. const修饰指针 5. 指针运算 6. 野指针 7. 指针的使⽤和传址调⽤ 1. 内存和地址 1.1 内存 在讲内存和地址之前&#xff0c;我们想有个⽣活中的案例&#xff1a; 假设有⼀栋宿舍楼&#xff…

Nodejs基于Vue的物料物资生产销售制造执行系统

本系统具有的功能有&#xff1a; 后台&#xff1a; 1. 用户登录模块&#xff1a;账号密码的登录验证提示。 2. 基础数据模块&#xff1a;部门、员工、存货类型、计量单位、存货档案的增删改查。 3. 工程数据模块&#xff1a;工序、工艺路线、BOM管理的增删改查。 4. …

初探分布式链路追踪

本篇文章&#xff0c;主要介绍应用如何正确使用日志系统&#xff0c;帮助用户从依赖、输出、清理、问题排查、报警等各方面全面掌握。 可观测性 可观察性不单是一套理论框架&#xff0c;而且并不强制具体的技术规格。其核心在于鼓励团队内化可观察性的理念&#xff0c;并确保由…

聊聊java中的Eureka和Nacos

本文主要来自于黑马课程中 1.提供者与消费者 在服务调用关系中&#xff0c;会有两个不同的角色&#xff1a; 服务提供者&#xff1a;一次业务中&#xff0c;被其它微服务调用的服务。&#xff08;提供接口给其它微服务&#xff09; 服务消费者&#xff1a;一次业务中&#xff0…

JAVA和C#怎么开发SECS/GEM:recipe配方处理 S7F1、S7F19

recipe是什么内容呢&#xff1f; recipe是机台加工不同产品时的对应程式&#xff0c;指的是由制造工程师提前在机台上设置&#xff0c;并且EAP控制生产时会自动根据货的类型选择并控制机台按照制造工程师提前设置的方式进行加工。 recipe也称为配方&#xff0c;配方是怎么来的…

【springboot网页时装购物系统】

前言 &#x1f31e;博主介绍&#xff1a;✌全网粉丝15W,CSDN特邀作者、211毕业、高级全栈开发程序员、大厂多年工作经验、码云/掘金/华为云/阿里云/InfoQ/StackOverflow/github等平台优质作者、专注于Java、小程序技术领域和毕业项目实战&#xff0c;以及程序定制化开发、全栈…

最小化安装BCLinux-for-Euler-21.10-dvd-x86_64-230731版

本文记录最小化安装BCLinux-for-Euler-21.10-dvd-x86_64-230731版。 一、镜像获取 1、下载镜像 移动云官方网站 最新镜像为2023-11-02 15:04:56更新的BCLinux-for-Euler-21.10-dvd-x86_64-230731版 直接下载地址&#xff1a;https://mirrors.cmecloud.cn/bclinux/oe21.10/I…

【思科】 GRE VPN 的实验配置

【思科】GRE VPN 的实验配置 前言报文格式 实验需求配置拓扑GRE配置步骤R1基础配置GRE 配置 ISP_R2基础配置 R3基础配置GRE 配置 PCPC1PC2 抓包检查OSPF建立GRE隧道建立 配置文档 前言 VPN &#xff1a;&#xff08;Virtual Private Network&#xff09;&#xff0c;即“虚拟专…

闪耀ISE 2024,起立CEOLED OLED透明屏引领欧洲视听市场新潮流

ISE 2024欧洲国际视听及系统集成展 2024年1月30日至2月2日 西班牙 巴塞罗那 2024年1月30日至2月2日&#xff0c;欧洲国际视听及系统集成展&#xff08;ISE 2024&#xff09;将在西班牙巴塞罗那盛大举行。作为全球视听行业的盛会&#xff0c;这次展会将汇集来自世界各地的顶级…

上门服务系统|如何搭建一款高质量的上门服务软件

预约上门系统源码开发是一项复杂而有挑战性的任务&#xff0c;但也是实现智能化预约服务的关键一步。通过自主开发预约上门系统的源码&#xff0c;企业可以完全定制系统的功能、界面和安全性&#xff0c;从而为用户提供更高效、便捷、个性化的预约体验。本文将带你深入了解预约…

【lesson26】学习MySQL事务前的基础知识

文章目录 CURD不加控制&#xff0c;会有什么问题&#xff1f;CURD满足什么属性&#xff0c;能解决上述问题&#xff1f;什么是事务&#xff1f;为什么会出现事务事务的版本支持 CURD不加控制&#xff0c;会有什么问题&#xff1f; CURD满足什么属性&#xff0c;能解决上述问题&…

数据结构day7

1.思维导图 1.二叉树递归创建 2.二叉树先中后序遍历 3.二叉树计算节点 4.二叉树计算深度。 5.编程实现快速排序降序

WPF自定义圆形百分比进度条

先看效果图 1.界面代码 <UserControl x:Class"LensAgingTest.CycleProcessBar1"xmlns"http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x"http://schemas.microsoft.com/winfx/2006/xaml"xmlns:mc"http://schemas.op…

人脸识别技术在网络安全中有哪些应用前景?

人脸识别技术在网络安全中有广泛的应用前景。以下是一些主要的应用方向&#xff1a; 1. 身份验证和访问控制&#xff1a;人脸识别可以用作一种更安全和方便的身份验证方法。通过将用户的人脸与事先注册的人脸进行比对&#xff0c;可以实现强大的身份验证&#xff0c;避免了传统…

【刷题】牛客网 NC132 环形链表的约瑟夫问题

NC132 环形链表的约瑟夫问题 题目描述思路一&#xff08;链表直通版&#xff09;思路二&#xff08;数组巧解版&#xff09;思路三&#xff08;变态秒杀版&#xff09;Thanks♪(&#xff65;ω&#xff65;)&#xff89;谢谢阅读下一篇文章见&#xff01;&#xff01;&#xff…