Android进阶之光——View体系(View事件分发机制)
View与ViewGroup
View是Android所有控件的基类
ViewGroup是View的组合,ViewGroup可以包含很多View以及ViewGroup,而包含的ViewGroup又可以包含View和ViewGroup
View树
坐标系
Android系统中有两种坐标系:Android坐标系和View坐标系。
Android坐标系
在Android中,将屏幕左上角的顶点作为Android坐标系的原点,这个原点向右是X轴正方向,向下是Y轴正方向
Android坐标系
View坐标系
View坐标系与Android坐标系并不冲突,两者是共同存在的
View坐标系
View获取自身的宽高
width=getRight()-getLeft();
height=getBottom()-getTop();
这样做比较麻烦,因为系统已经向我们提供了获取View宽高的方法:getHeight()、getWidth()
View自身的坐标
- getTop():获取View自身顶边到其父布局顶边的距离
- getLeft():获取View自身左边到其父布局左边的距离
- getRight():获取View自身右边到其父布局左边的距离
- getBottom():获取View自身底边到其父布局顶边的距离
MotionEvent
- getX() 获取点击事件距离控件左边的距离,即视图坐标
- getY() 获取点击事件距离控件顶边的距离,视图坐标
- getRawX() 获取点击事件距离整个屏幕左边的距离 绝对坐标
- getRawY() 获取点击事件距离整个屏幕顶边的距离 绝对坐标
View的滑动
在处理View的滑动时,基本思路都是类似的:当点击事件传到View时,系统记下触摸点的坐标,手指移动时记下移动后触摸的坐标并计算偏移量,并通过偏移量来修改View的坐标
layout()方法
View进行绘制的时候会调用onLayout()来设置显示的位置。
我们自定义一个view
- java代码 CustomView.java
package com.probuing.androidlight.view;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.Nullable;
public class CustomView extends View {
private int lastX;
private int lastY;
public CustomView(Context context) {
super(context);
}
public CustomView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//获取手指触摸点的横坐标和纵坐标
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
//计算移动距离
int offsetX = x - lastX;
int offsetY = y - lastY;
//调用layout方法重新绘制位置
layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
break;
}
return true;
}
}
- 随后在布局文件中引用自定义View
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
tools:context=".activity.ViewLSNActivity">
<com.probuing.androidlight.view.CustomView
android:id="@+id/customview"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_margin="50dp"
android:background="@android:color/holo_red_light"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
offsetLeftAndRight()和offsetTopAndBottom()
其实也可以用这两种方法来替换layout()方法
- java代码
@Override
public boolean onTouchEvent(MotionEvent event) {
//获取手指触摸点的横坐标和纵坐标
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
//计算移动距离
int offsetX = x - lastX;
int offsetY = y - lastY;
offsetLeftAndRight(offsetX);
offsetTopAndBottom(offsetY);
//调用layout方法重新绘制位置
// layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
break;
}
return true;
}
LayoutParams(改变布局参数)
LayoutParams主要保存了一个View的布局参数,因此我们可以通过LayoutParams来改变View的布局参数从而达到改变View位置的效果
因为我们自定义的View的父控件是LinearLayout,所以我们使用了LinearLayout.LayoutParams。
- java代码
@Override
public boolean onTouchEvent(MotionEvent event) {
//获取手指触摸点的横坐标和纵坐标
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
//计算移动距离
int offsetX = x - lastX;
int offsetY = y - lastY;
/* offsetLeftAndRight(offsetX);
offsetTopAndBottom(offsetY);*/
//调用layout方法重新绘制位置
// layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft()+offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
break;
}
return true;
}
动画
我们也可以采用View动画来移动
- res目录创建anim目录 并创建translate.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="1000"
android:fromXDelta="0"
android:toXDelta="300" />
</set>
View动画不能改变View的位置参数。但是属性动画可以解决位置问题
@Override
protected void onStart() {
super.onStart();
ObjectAnimator.ofFloat(customview,"translationX",0,300).setDuration(1000)
.start();
}
scrollTo与scrollBy
scrollTo(x,y)表示移动到一个具体的坐标点。而scrollBy(dx,dy)则表示移动的增量为dx、dy。
属性动画
随着Android3.0属性动画的提出,View之前的动画带来的问题,例如响应事件位置依然在动画发生前的地方,不具备交互性等也随之解决。
ObjectAnimator
ObjectAnimator是属性动画最重要的类,创建一个ObjectAnimator只需要通过其静态工厂类直接返还一个ObjectAnimator对象。参数包括一个对象和对象的属性名字,这个属性必须有get和set方法
ObjectAnimator.ofFloat(customview,"translationX",0,300)
.setDuration(1000)
.start();
下面就是一些常用的可以直接使用的属性动画的属性值
- translationX和translationY:用来沿着X轴或Y轴进行平移
- rotation、rotationX、rotationY:用来围绕View的支点进行旋转
- PrivotX和PrivotY:控制View对象的支点位置,围绕这个支点进行旋转和缩放变换处理。
- alpha:透明度,默认是1,0是代表完全透明
- x和y:描述View对象在其容器中的最终位置
ValueAnimator
ValueAnimator不提供任何动画效果,它是一个数值发生器,用来产生一定的有规律的数字。
动画的监听
完整的动画具有start、repeat、end、cancel这4个过程
@Override
protected void onStart() {
super.onStart();
ObjectAnimator translationX = ObjectAnimator.ofFloat(customview, "translationX", 0, 300).setDuration(1000);
translationX.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
translationX.start();
}
一般情况下 我们比较常用的是onAnimationEnd事件,Android也提供了AnimatorListenterAdapter来让我们选择必要的事件进行监听
@Override
protected void onStart() {
super.onStart();
ObjectAnimator translationX = ObjectAnimator.ofFloat(customview, "translationX", 0, 300).setDuration(1000);
translationX.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
Toast.makeText(ViewLSNActivity.this, "end", Toast.LENGTH_SHORT).show();
}
});
translationX.start();
}
组合动画——AnimatorSet
AnimatorSet类提供了一个play()方法,如果我们向这个方法中传入一个Animator对象,将会返回一个AnimatorSet.Builder的实例,每次调用方法时都会返回Builder自身用于构建
- after(Animator anim)将现有动画插入到传入的动画后执行
- after(long delay)将现有动画延迟指定毫秒后执行
- before(Animator anim)将现有动画插入到传入的动画之前执行
- with(Animator anim)将现有动画和传入的动画同时执行
private void animBuilder() {
ObjectAnimator animator1 = ObjectAnimator.ofFloat(customview, "translationX", 0.0f, 200.0f, 0f);
ObjectAnimator animator2 = ObjectAnimator.ofFloat(customview, "scaleX", 1.0f, 2.0f);
ObjectAnimator animator3 = ObjectAnimator.ofFloat(customview, "rotationX", 0.0f, 90.0f, 0.0f);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.setDuration(3000);
animatorSet.play(animator1).with(animator2).after(animator3);
animatorSet.start();
}
组合动画——PropertyValuesHolder
除了使用AnimatorSet类之外,还可以使用PropertyValuesHolder类来实现组合动画。使用PropertyValuesHolder类只能是多个动画一起执行。使用PropertyValuesHolder只能是多个动画一起执行。得结合ObjectAnimator.ofPropertyValuesHolder()
private void propertyValuesHolder() {
PropertyValuesHolder valuesHolder1 = PropertyValuesHolder.ofFloat("scaleX", 1.0f, 1.5f);
PropertyValuesHolder valueHolder2 = PropertyValuesHolder.ofFloat("rotationX", 0.0f, 90.0f, 0.0f);
ObjectAnimator objectAnimator = ObjectAnimator.ofPropertyValuesHolder(customview, valuesHolder1, valueHolder2);
objectAnimator.setDuration(2000).start();
}
在XML中使用属性动画
在res中新建animator目录(属性动画必须放在animator目录下),新建scale.xml
<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="3000"
android:propertyName="scaleX"
android:valueFrom="1.0"
android:valueTo="2.0"
android:valueType="floatType"
>
</objectAnimator>
在程序中引用XML定义得属性动画
private void startXMLAnimator() {
Animator animator = AnimatorInflater.loadAnimator(this, R.animator.scale);
animator.setTarget(customview);
animator.start();
}
View的事件分发机制
先来看看Activity组成
Activity的构成一个Activity包含一个Window对象,这个对象是由PhoneWindow实现的。PhoneWindow将DecorView作为整个应用窗口的根View。而这个DecorView又将屏幕划分为两个区域:一个是TitleView另一个是ContentView。我们平常做应用所写的布局就是展示在ContentView中的
解析View的事件分发机制
当我们点击屏幕时,就产生了点击事件,这个事件被封装成了一个类:MotionEvent,而当这个MotionEvent产生后,那么系统就会将这个MotionEvent传递给View的层级。MotionEvent在View中的层级传递过程就是点击事件的分发
点击事件有3个重要的方法
- dispatchTouchEvent(MotionEvent ev):用于事件的分发
- onInterceptTouchEvent(MotionEvent ev):用于事件的拦截,在dispatchTouchEvent中调用
- onTouchEvent(MotionEvent ev):用来处理点击事件,在dispatchTouchEvent()方法中进行调用
View的事件分发机制
当点击事件产生后,事件首先会传递给当前的Activity,这会调用Activity的dispatchTouchEvent()方法(也就是交由Activity中的PhoneWindow来完成,然后PhoneWindow再把事件处理工作交给DecorView,然后再由DecorView将事件处理工作交给根ViewGroup)
注意:一个完整的事件的序列是以DOWN开始以UP结束
- 如果ViewGroup要拦截事件的时候,那么后续的事件序列都会交给它处理,而不用再调用onInterceptTouchEvent()方法了。
点击事件分发的传递规则
伪代码表示
public boolean dispatchTouchEvent(MotionEvent ev){
boolean result = false;
if(onInterceptTouchEvent(ev)){
result=super.onTouchEvent(ev)
}else{
result=child.dispatchTouchEvent(ev)
}
return result;
}
事件自上而下传递过程
当点击事件产生后会由Activity来处理,传递给PhoneWindow,再传递给DecorView,最后传递给顶层的ViewGroup。
对于根ViewGroup,点击事件首先传递给它的dispatchTouchEvent(),该ViewGroup的onInterceptTouchEvent()
- 如果返回true,则表示要拦截这个事件,这个事件就会交给它的onTouchEvent()方法处理
- 如果返回false,则表示不拦截这个事件,这个事件会交给子元素的dispatchTouchEvent()处理
如此反复下去,如果传递给底层的View,View是没有子View的,就会调用这个View的dispathTouchEvent()方法,一般最终会调用View的onTouchEvent()
事件自下而上传递过程
当点击事件传递给底层的View时,如果底层的View的onTouchEvent()方法返回true,则表示事件由底层的View消耗并处理。
如果返回false则表示该View不做处理,事件会传递给父View的onTouchEvent()处理,如果父View的onTouchEvent()返回false表示父View也不处理,则继续传递给该父View的父View处理,如此反复
事件分发机制
- Activity
- 没有onInterceptTouchEvent方法
- 只有dispatchTouchEvent、onTouchEvent方法
- ViewGroup
- 有 onInterceptTouchEvent、dispatchTouchEvent、onTouchEvent方法
- View
- 没有 onInterceptTouchEvent方法
事件分发流程
Activity
dispatchTouchEvent:
- 返回值true/false:事件由自己消费
- 返回值super:交由子ViewGroup的dispatchTouchEvent处理
ViewGroup
dispatchTouchEvent:
- 返回值true:事件由自己消费
- 返回值false:交由父View的onTouchEvent()处理
- 返回值super:传递给自己的onInterceptTouchEvent()进行分发
onInterceptTouchEvent:
- 返回值true:表示拦截事件,交由自己的onTouchEvent处理
- 返回值false/super:表示不拦截事件,交由子View的dispatchTouchEvent()处理
onTouchEvent:
- 返回值true:表示事件自己处理
- 返回值false/super:将事件交由父onTouchEvent处理
View
dispatchTouchEvent:
- 返回值为true:事件由自己消费
- 返回值为false:事件交由父View的onTouchEvent处理
- 返回值为super:交由自己的onTouchEvent处理
onTouchEvent
- 返回值true:事件自己消费
- 返回值false、super:事件交由父view的onTouchEvent处理,直至传递到Activity的onTouchEvent()
OnTouchListener和onClickListener执行顺序
当一个View需要处理事件时,如果设置了OnTouchListener,那么OnTouchListener中的OnTouch会被回调。
- 如果onTouch返回false,则当前View的onTouchEvent方法会被调用
- 如果onTouch返回true,那么onTouchEvent方法将不会调用
由此可看出onTouchListener要比onTouchEvent优先级高
在onTouchEvent方法中,如果当前设置的有onClickListener那么onClick就会被调用,由此可以看出onClick的优先级最低,处于事件传递的尾端
onTouch->onTouchListener->onTouchEvent->onClick->onClickListener