在 CoordinatorLayout的简单使用 文章中我们笼统的认识了一下 CoordinatorLayout 协调者布局,并学会了它配合 AppBarLayout 与 RecyclerView 的使用。

这篇文章,我就来带大家系统的了解一下 CoordinatorLayout 布局,他是如何协调他的 子View 的。

效果展示

android CoordinatorLayout使用时底部会空一个固定高度 coordinatorlayout原理_移动开发

原理分析

毫无疑问,实现上述功能我使用了 CoordinatorLayout 这个系统为我们提供的布局。使用它的核心是编写 Behavior,即行为。这个时候聪明的你肯定会思考,当我的 Behavior 写好了之后,我该把它赋予谁呢?想弄清楚这个问题,你就得先理解这两个概念:Child 和   Dependency,(严格来说 Child、Dependency都是 CoordinatorLayout  的子 View)。我们从简解释:就是如过Dependency这个View发生了变化,那么Child这个View就要相应发生变化。发生变化是具体发生什么变化呢?这里就要引入BehaviorChild发生变化的具体执行的代码都是放在Behavior这个类里面。

代码展示

自定义可拖拽布局  CanDragView


package com.wust.SelfCoordinator;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Scroller;
import android.widget.TextView;

import androidx.annotation.Nullable;

/**
 * ClassName: CanDragView <br/>
 * Description: <br/>
 * date: 2021/6/29 9:45<br/>
 *
 * @author yiqi<br />
 */
public class CanDragView extends View {

    int startX;
    int startY;
    private Scroller scroller;
    private int moveX;
    private int moveY;

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

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

    public CanDragView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
            {
                startX =(int) event.getX();
                startY =(int) event.getY();
            }
                break;
            case MotionEvent.ACTION_MOVE:
            {
                int endX = (int) event.getX();
                int endY = (int) event.getY();
                moveX = endX - startX;
                moveY = endY - startY;
//                System.out.println("moveX ->" + moveX + "moveY ->" + moveY);
                //移动布局的关键性代码
                layout(getLeft()+moveX,getTop()+moveY,getRight()+moveX,getBottom()+moveY);
            }
                break;
        }
        return true;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        System.out.println("onDraw ->");
    }
}

上面的属于自定义View的内容,和 CoordinatorLayout 无关。


有了上面基本知识的理解,下面我们来对着代码分析,毕竟 实践是检验真理的位移标准。

第一步:准备布局文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <com.wust.SelfCoordinator.CanDragView
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:background="#f00"
            android:text="hello"/>
        <TextView
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:background="#0f0"
            android:layout_marginTop="200dp"
            app:layout_behavior=".MyBehavior"/>

    </androidx.coordinatorlayout.widget.CoordinatorLayout>


</LinearLayout>

上面的布局很简单,在 CoordinatorLayout 中存在两个布局,其中 我把 CoordinatorLayout 作为Dependency,TextView 作为 Child,也就是说 TextView 要随着 CoordinatorLayout 变化 根据 Behavior 的指示来做出相应的动作。所以大家可以看到,app:layout_behavior=".MyBehavior" 是加在 TextView  上的。

第二步:编写 MyBehavior代码

package com.wust.SelfCoordinator;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.coordinatorlayout.widget.CoordinatorLayout;

/**
 * ClassName: MyBehavior <br/>
 * Description: <br/>
 * date: 2021/6/29 19:27<br/>
 *
 * @author yiqi<br />
 */
public class MyBehavior extends CoordinatorLayout.Behavior<TextView> {

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

    /**
    * 判断child的布局是否依赖dependency
    */
    @Override
    public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull TextView child, @NonNull View dependency) {
        //根据逻辑判断返回值
        //返回false表示child不依赖dependency,ture表示依赖
        return dependency instanceof CanDragView;
    }

    /**
    * 当dependency发生改变时(位置、宽高等),执行这个函数
    * 返回true表示child的位置或者是宽高要发生改变,否则就返回false(网上别人是这么说的),可是我试了一下 true 和 false 都可以,所以说:实践是检验真理的唯一标准 完全没毛病
    */
    @Override
    public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull TextView child, @NonNull View dependency) {
        System.out.println("onDependentViewChanged我被调用了");
        //child要执行的具体动作
        return super.onDependentViewChanged(parent, child, dependency);
    }
}

从上面代码我们还可以很清楚的看到 Behavior 关键的两个方法:layoutDependsOn() 和 onDependentViewChanged()

我们目前没写 child 具体动作的实现,我们先来试一下当前代码写的是否可行。

android CoordinatorLayout使用时底部会空一个固定高度 coordinatorlayout原理_安卓_02

由此可见,到目前为止,我们的代码都是没毛病的。

第三步:child具体动作的实现

package com.wust.SelfCoordinator;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.coordinatorlayout.widget.CoordinatorLayout;

/**
 * ClassName: MyBehavior <br/>
 * Description: <br/>
 * date: 2021/6/29 19:27<br/>
 *
 * @author yiqi<br />
 */
public class MyBehavior extends CoordinatorLayout.Behavior<TextView> {

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

    @Override
    public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull TextView child, @NonNull View dependency) {

        return dependency instanceof CanDragView;
    }

    @Override
    public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull TextView child, @NonNull View dependency) {
        // child 动作的实现
        int dependX = dependency.getLeft();
        int dependY = dependency.getTop();
        int X = dependX;
        int Y = dependY + dependency.getHeight();
        System.out.println("dependX ->" + dependX);
        setChildPosition(child,X,Y);

        return false;
    }

    private void setChildPosition(TextView view, int x, int y) {
        CoordinatorLayout.MarginLayoutParams params = (CoordinatorLayout.MarginLayoutParams) view.getLayoutParams();
        params.leftMargin = x;
        params.topMargin = y;
        view.setLayoutParams(params);
    }
}

到此为止,我们的大致效果就可以出来了,但是存在诸多问题,下面我就带大家来看看:

android CoordinatorLayout使用时底部会空一个固定高度 coordinatorlayout原理_ide_03

问题一:起初两个方块是相隔 100dp 的,为什么运行起来之后靠在一起了?

正常排版如下:

android CoordinatorLayout使用时底部会空一个固定高度 coordinatorlayout原理_android_04

原因很简单,当Activity启动的时候,系统会自动帮我们调用一次 onDependentViewChanged() 这个方法,所以,当我们把 behavior 写完运行程序之后,两个方块就靠在一起了。

从这个问题中我们可以看到:onDependentViewChanged()会在两个时候被调用:程序启动的时候 和 Dependency 移动的时候。你可以加入一个标志变量,过滤掉初次调用即可。

问题二:两个方块为什么一直抖,并且最后位置没变?

这个问题的根本原因在于红色方块,因为我们绿色方块的位置使跟随红色方块位置计算得来的。所以,安排好红色方块就天下太平了。

红色方块是我们的自定义View,在文章的开始我就把源码给大家粘贴出来了。从源码中可以看到,我们实现布局的滑动使用的是 Android实现滑动的四种方式 中的 layout 法,这个方法代码简单,一句话就实现了滑动,可是有个毛病就是 布局的 LayoutParams 参数值没变啊。于是就有了下面这段插曲:

红色方块使用layout法实现了移动  -》通过 behavior 指导绿色方块移动,可是大家别忘了绿色方块之所以能动,是得益于 setLayoutParams() ,这个方法会导致布局重绘 -》 在布局重新绘制的时候,你红色方块 LayoutParams 参数值没变啊leftMargin,topMargin还是0 -》所以,红色方块就回到了原处 -》那绿色方块也不能闲着啊,就跟着一起回来了。

所以,大家就看到了他们俩在一直抖动。

解决方法很简单,改变红色方块移动的方式:代码如下:

package com.wust.SelfCoordinator;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;
import android.widget.TextView;

import androidx.annotation.Nullable;
import androidx.coordinatorlayout.widget.CoordinatorLayout;

/**
 * ClassName: CanDragView <br/>
 * Description: <br/>
 * date: 2021/6/29 9:45<br/>
 *
 * @author yiqi<br />
 */
public class CanDragView extends View {

    int startX;
    int startY;
    private Scroller scroller;
    private int moveX;
    private int moveY;

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

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

    public CanDragView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
            {
                startX =(int) event.getX();
                startY =(int) event.getY();
            }
                break;
            case MotionEvent.ACTION_MOVE:
            {
                int endX = (int) event.getX();
                int endY = (int) event.getY();
                moveX = endX - startX;
                moveY = endY - startY;
//                System.out.println("moveX ->" + moveX + "moveY ->" + moveY);
                //移动布局的关键性代码,scrollTo/scrollBy 移动的是 View中的内容 ViewGroup中的子View
                //这里不能使用这种方法,因为这种方法位置参数没有改变
//                layout(getLeft()+moveX,getTop()+moveY,getRight()+moveX,getBottom()+moveY);
                CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) getLayoutParams();
                layoutParams.leftMargin = getLeft() + moveX;
                layoutParams.topMargin = getTop() + moveY;
                setLayoutParams(layoutParams);
            }
                break;
        }
        return true;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        System.out.println("onDraw ->");
    }
}

最后,完美解决:

android CoordinatorLayout使用时底部会空一个固定高度 coordinatorlayout原理_移动开发