在 CoordinatorLayout的简单使用 文章中我们笼统的认识了一下 CoordinatorLayout 协调者布局,并学会了它配合 AppBarLayout 与 RecyclerView 的使用。
这篇文章,我就来带大家系统的了解一下 CoordinatorLayout 布局,他是如何协调他的 子View 的。
效果展示
原理分析
毫无疑问,实现上述功能我使用了 CoordinatorLayout 这个系统为我们提供的布局。使用它的核心是编写 Behavior,即行为。这个时候聪明的你肯定会思考,当我的 Behavior 写好了之后,我该把它赋予谁呢?想弄清楚这个问题,你就得先理解这两个概念:Child
和 Dependency
,(严格来说 Child、Dependency都是
CoordinatorLayout 的子 View)。我们从简解释:就是如过Dependency
这个View发生了变化,那么Child
这个View
就要相应发生变化。发生变化是具体发生什么变化呢?这里就要引入Behavior
,Child
发生变化的具体执行的代码都是放在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 具体动作的实现,我们先来试一下当前代码写的是否可行。
由此可见,到目前为止,我们的代码都是没毛病的。
第三步: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);
}
}
到此为止,我们的大致效果就可以出来了,但是存在诸多问题,下面我就带大家来看看:
问题一:起初两个方块是相隔 100dp 的,为什么运行起来之后靠在一起了?
正常排版如下:
原因很简单,当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 ->");
}
}
最后,完美解决: