学习资料:
- dodo_lihao同学:CoordinatorLayout使用系列
- r17171709 同学:CoordinatorLayout——小试牛刀
最近一直在看Java
的知识,在看到上面两位同学的博客。CoordinatorLayout
只是简单使用过一次,也学习了解一下。
十分感谢两位同学 :)
1. 简单使用
在之前也了解过一点怎么使用CoordinatorLayout
,并写了一篇简单入门使用的博客 CoordinatorLayout、Tablayout、Toolbar简单组合使用
CoordinatorLayout
作为一个中间桥梁性质的布局,协调着内部的childView
。之前对CoordinatorLayout
有点误解,以为需要配合AppBarLayout
才有一些比较炫酷的特效,大错特错,Behavior
是CoordinatorLayout
能够有协调作用以及能支持各种炫酷特效的的关键因素
dodo_lihao同学
的思路很好,博客中也把他自己收集的学习资料整理了出来,就直接看着他的博客,跟着他的思路来学习的,感觉他系列博客中写的案例很容易表现出Behavior
的特点,所以思路和代码照搬的dodo_lihao同学
的,效果图也就一样了,有点剽窃成果的感觉,哈哈
依赖控件:红色的MoveView
绑定控件:蓝色的TextView
MoevView
受手指的控制,手指怎么移动就怎么移动;而绑定的TextView
则是由MoveView
通过Behavior
来控制
1.1 布局文件中使用
布局文件:
最关键的地方就在于app:layout_behavior
,利用这个属性来确定的绑定的目标childView
指定绑定目标有3种方式:
- 在
xml
布局通过app:layout_behavior
- 在
Java
代码中,child.getLayoutParams().setBehavior()
来指定 - 在目标
childView
类上,通过@DefaultBehavior
来指定
1.2 MoveView
MoveView就是一个继承TextView
的很简单的自定义View
public class MoveView extends TextView {
private float lastX, lastY;
public MoveView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
float x = event.getRawX();
float y = event.getRawY();
if (action == MotionEvent.ACTION_MOVE) {
CoordinatorLayout.MarginLayoutParams layoutParams = (CoordinatorLayout.MarginLayoutParams) getLayoutParams();
//计算当前的左上角坐标
float left = layoutParams.leftMargin + x - lastX;
float top = layoutParams.topMargin + y - lastY;
//设置坐标
layoutParams.leftMargin = (int) left;
layoutParams.topMargin = (int) top;
setLayoutParams(layoutParams);
}
lastX = x;
lastY = y;
return true;
}
}
主要就是重写onTouchEvent()
来使MoveView
可以根据手指滑动在屏幕改变位置
1.3 一个简单的自定义Behavior
LearnBehavior
代码:
public class LearnBehavior extends CoordinatorLayout.Behavior {
private int width, height;
public LearnBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
DisplayMetrics display = context.getResources().getDisplayMetrics();
width = display.widthPixels;
height = display.heightPixels;
}
/**
* 绑定
*
* @param parent CoordinatorLayout
* @param child 使用Behavior的childView,绑定对象
* @param dependency 依赖的childView
* @return true 绑定
*/
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, TextView child, View dependency) {
return dependency instanceof MoveView;
}
/**
* 依赖的childView 发生改变时
*/
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, TextView child, View dependency) {
int top = dependency.getTop();
int left = dependency.getLeft();
int x = width - left - child.getWidth();
int y = height - top - child.getHeight();
Log.e("x,y", "--->" + x + "-->" + y);
setPosition(child, x, y);
return true;
}
/**
* 设置坐标
*/
private void setPosition(View v, int x, int y) {
CoordinatorLayout.MarginLayoutParams layoutParams = (CoordinatorLayout.MarginLayoutParams) v.getLayoutParams();
layoutParams.leftMargin = x;
layoutParams.topMargin = y;
layoutParams.width = y / 2;
v.setLayoutParams(layoutParams);
}
}
CoordinatorLayout.Behavior
这里使用泛型将绑定的childView
限制为了TextView
,可以根据实际需求来指定类型,也可以直接指定为View
注意:
当在布局文件中使用了Behavior
后,Behavior
代码中确定的交互行为便直接奏效,初始化第一次加载CoordinatorLayout
时,使用了Behavior
的ChildView
受到onDependentViewChanged()
方法的影响,第一次加载的位置也会受到影响,导致和布局文件中指定的位置不相同
官方有好几个非常好的学习资料,例如:
android.support.design.widget.AppBarLayout$ScrollingViewBehavior
一个依赖AppBarLayout
后,处理滑动事件的Behavior
,对Behavior
中的属性及方法有了大概了解后,可以学习具体细节的设计和优化
2. Behavior 行为
直译就是行为
的意思
源码中的注释:
/**
* Interaction behavior plugin for child views of {@link CoordinatorLayout}.
* 用于CoordinatorLayout中ChildView交互的行为的插件
*
* A Behavior implements one or more interactions that a user can take on a child view.
*一个ChildView可以实现一个或者多个Behavior
*
* These interactions may include drags, swipes, flings, or any other gestures.
*交互的行为包括点击,拖动,滑动或者一些其他的收拾操作
*
* @param The View type that this Behavior operates on
* 泛型就是指定使用当前Behavior的ChildView类型
*/
public static abstract class Behavior {
...
方法省略
...
}
需要注意的是Behavior
可以几乎包括所有的交互行为,配合ViewDragHelper
应该能够实现出一些很炫酷的交互效果
2.1 常用的方法
构造方法有两个:
默认:public Behavior() {}
布局:public Behavior(Context context, AttributeSet attrs) { }
两个构造方法也比较容易理解,一个是默认的空参的构造方法,一个是带有布局属性AttributeSet
的方法,有了这个构造方法,可以直接在布局文件中使用
根据Behavior
的特性,可以将内部的方法分以下类:
- 测量与布局:
测量:public boolean onMeasureChild(){}
布局:public boolean onLayoutChild(){}
- 特定状态:
//当Behavior添加到参数实例时,回调
public void onAttachedToLayoutParams(){}
//当Behavior与参数实例分离时,回调
public void onDetachedFromLayoutParams(){}
//当Behavior关联的对象想要定位到特定的矩形时,回调
public boolean onRequestChildRectangleOnScreen(){}
//当一个ChildView设置为回避属性时,回调
public boolean getInsetDodgeRect(){}
//当窗口发生改变时,回调
public WindowInsetsCompat onApplyWindowInsets(){}
//需要保存临时状态信息,回调
public Parcelable onSaveInstanceState(){}
//需要恢复临时状态信息,回调
public void onRestoreInstanceState(){}
//作用未知
public int getScrimColor(){}
//作用未知
public float getScrimOpacity(){}
- 确定依赖与绑定对象:
//根据参数来确定依赖与绑定对象
public boolean layoutDependsOn(){}
- 当依赖对象发生改变时:
//当依赖对象发生改变,包括位置,大小,颜色,进行回调
public boolean onDependentViewChanged(){}
//当依赖对象被移除时,进行回调
public void onDependentViewRemoved(){}
- 事件相关:
//拦截事件,在CoordinatorLayout把事件分发到childView之前
public boolean onInterceptTouchEvent(){}
//消费事件
public boolean onTouchEvent(){}
- 嵌套滑动:
//CoordinatorLayout中的滑动嵌套childView开始启动一次嵌套滚动时,回调
public boolean onStartNestedScroll(){}
//嵌套滑动结束时,回调
public void onStopNestedScroll(){}
//当一次嵌套滑动被CoordiantorLayout识别并确定时,进行回调
public void onNestedScrollAccepted(){}
//嵌套滚动正在进行中并且绑定目标childView已经开始滚动或者被CoordinatorLayout接受后试图滚动
public void onNestedScroll(){}
//嵌套滚动正在准备更新进度,并且是在绑定目标childView已经出现滚动距离之前,回调
public void onNestedPreScroll(){}
//当嵌套滚动的childView正在开始fling或者一个动作确认为fling
public boolean onNestedFling(){}
//当滑动嵌套childView检测到适当的条件,马上开始一次fling事件前回调
public boolean onNestedPreFling(){}
暂时就这么分,分类并不算合理,也无所谓,目的是以后自己回头来看时,能比较清晰能快速定位方法是干嘛的
3. 事件相关
需求:CoordinatorLayout
内有一个可以点击的TextView
,长按之后,可以拖动,此时蓝色的TextView
要依然可以点击
布局代码:
布局代码中,并没有添加Behaivor
,一旦添加了,在加载布局之时,Behaivor
便开始作用于依赖目标childView
Activity代码:
public class CoordinatorLayoutLActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_coordinator_layout_l);
init();
}
/**
* 初始化
*/
private void init() {
initTextView(R.id.tv_coordinator_activity, "TextView-->蓝色被点击");
CoordinatorLayout layout = (CoordinatorLayout) findViewById(R.id.cl_coordinator_activity);
layout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ToastUtils.show(CoordinatorLayoutLActivity.this, "CoordinatorLayout被点击");
}
});
}
/**
* TextView进行初始化
*/
private void initTextView(int id, final String str) {
final TextView tv = (TextView) findViewById(id);
tv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ToastUtils.show(CoordinatorLayoutLActivity.this, str);
}
});
/**
* 长按 ,提示动画效果结束后 ,动态添加 Behavior
*/
tv.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
//动画提示效果
animation(tv);
return true;
}
});
}
private void animation(final TextView tv) {
AnimatorSet set = new AnimatorSet();
set.setInterpolator(new BounceInterpolator());
set.setDuration(1000);
set.playTogether(
ObjectAnimator.ofFloat(tv, "scaleX", 1, 1.5f),
ObjectAnimator.ofFloat(tv, "scaleY", 1, 1.5f),
ObjectAnimator.ofFloat(tv, "scaleX", 1.5f, 1),
ObjectAnimator.ofFloat(tv, "scaleY", 1.5f, 1)
);
//动画监听,结束时添加 Behavior
set.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
addBehavior(tv);
}
});
set.start();
}
/**
* 为TextView添加Behavior
*/
private void addBehavior(TextView tv) {
tv.setLongClickable(false);
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) tv.getLayoutParams();
//为TextView设置Behaior
lp.setBehavior(new LongBehavior());
ToastUtils.show(CoordinatorLayoutLActivity.this, "可以开始拖动了");
}
}
代码很简单,都是一眼能看明白的
LongBehavior代码:
private float lastX, lastY;
private float moveX, moveY;
public LongBehavior() {
Log.e("LongBehavior", "新建");
}
public LongBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, TextView child, MotionEvent ev) {
int action = ev.getAction();
boolean isIntercept = false;
float x = ev.getX();
float y = ev.getY();
switch (action) {
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
//判断落点是否在TextView范围内
//若不在 就进行拦截 返回true
isIntercept = !isInChildView(child, ev);
if (isIntercept) {
Log.e("MotionEvent.ACTION_DOWN", "---->MotionEvent.ACTION_DOWN--->进行拦截");
} else {
Log.e("MotionEvent.ACTION_DOWN", "---->MotionEvent.ACTION_DOWN--->不拦截");
}
break;
case MotionEvent.ACTION_MOVE:
//滑动距离大于10
if (Math.abs(lastX - x) >= 10 || Math.abs(lastY - y) >= 10) {
isIntercept = true;
}
break;
}
return isIntercept;
}
/**
* 判断落点是否在childView范围内
*/
private boolean isInChildView(TextView child, MotionEvent ev) {
return ev.getX() >= child.getLeft() && ev.getX() <= child.getRight()
&& ev.getY() >= child.getTop() && ev.getY() <= child.getBottom();
}
@Override
public boolean onTouchEvent(CoordinatorLayout parent, TextView child, MotionEvent ev) {
//根据是否拦截来执行
if (onInterceptTouchEvent(parent, child, ev)) {
int action = ev.getAction();
float x = ev.getX();
float y = ev.getY();
switch (action) {
case MotionEvent.ACTION_DOWN:
moveX = x;
moveY = y;
break;
case MotionEvent.ACTION_MOVE:
//计算偏移量
float offsetX = x - moveX;
float offsetY = y - moveY;
// Log.e("offset", "&&&--" + offsetX + "-->" + offsetY);
if (Math.abs(offsetX)>= 10 || Math.abs(offsetY) >= 10) {
CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
layoutParams.leftMargin = (int) (offsetX);
layoutParams.topMargin = (int) (offsetY);
child.setLayoutParams(layoutParams);
}
break;
}
}
return true;
}
}
当拦截了DOWN
事件之后,后续的事件便都由CoordinatorLayout
来消费,onTouchEvent
返回了True
,事件也就终止了,onClik
便也接收不到事件了,CoordinatorLayout
自身的点击事件不能执行了,
4.最后
嵌套滚动事件,下一篇进行记录学习
本人很菜,有错误请指出
共勉 :)