View体系(上)|青训营笔记
这是我参与「第四届青训营 」笔记创作活动的的第1天
View 体系是较为复杂的,但是又非常重要的一个知识点。我们把这部分知识吃透吃熟是十分必要的,打卡第一天,我把View体系的第一部分知识整理出来,快来和我一起学习吧。
View树结构

官方给出我们使用的各种布局和各种 View 都是继承自 ViewGroup 、 View 或者他们的派生类。所以,了解View体系是极其重要的任务
如下图的 View部分继承关系 ,我们可以看到常用的 View 组件、布局组件是如何继承的。

坐标系
学习 View,首先需要知道 View 的位置在 Android 中是如何定义和测量的。
上图之中的蓝色和绿色是有着不同作用含义,我们平时使用也是在不同的地方调用
绿色:在
View中获得View到其父控件之间的距离蓝色:来自于点击事件
MotionEvent内部的方法,可以在重写View事件分发体系的的三大方法的时候,利用传入的事件调用上图的蓝色方法,获取点击的位置坐标
获取坐标绘制View的滑动
//自定义一个View,点击该View可以随意滑动其位置
//下面有5个方法可以实现,其中两个由于理解为移动的是屏幕框,会使得其他元素一起偏移
public class CoutomView extends View {
private int lastX;
private int lastY;
Scroller mScroller;
public CoutomView(Context context) {
super(context);
}
public CoutomView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mScroller = new Scroller(context);
}
public CoutomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public CoutomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()){
((View)getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
invalidate();
}
}
@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;
// M1
layout(getLeft()+offsetX,getTop()+offsetY,
getRight()+offsetX,getBottom()+offsetY);
// M2
// offsetLeftAndRight(offsetX);
// offsetTopAndBottom(offsetY);
// M3
// ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams)getLayoutParams();
// layoutParams.leftMargin = getLeft()+offsetX;
// layoutParams.topMargin = getTop()+offsetY;
// setLayoutParams(layoutParams);
// M4 会使得其他元素一起偏移
// ((View)getParent()).scrollBy(-offsetX,-offsetY);
break;
}
return true;
}
/**
* M5
* 提供给Activity调用滑动,也会使得其他元素一起偏移
*
* @param destX
* @param destY
*/
public void smoothScrollTo(int destX,int destY){
int scrollX = getScrollX();
int delta = destX - scrollX;
mScroller.startScroll(scrollX,0,delta,0,2000);
invalidate();
}
}
属性动画
区别
View动画:只展示普通动画效果,响应的点击时间的位置依旧在原来的地方。所以无法左交互效果
属性动画:利用属性和对象来控制 View ,同时使得动画执行后可交互
属性动画的执行,可以带上属性以及属性参数
public static ObjectAnimator ofFloat(Object target, String propertyName, float... values)
//使用传入
ObjectAnimator.ofFloat(binding.coutomView,"translationX",0f,300f).setDuration(1000).start()
ObjectAnimator
常用属性
translationX和translantionY沿轴平移rotatian、rotatianX和rotatianY沿着某支点进行旋转PrivotX和PrivotY可以控制支点位置,围绕支点旋转和缩放,支点默认为中心位置、alpha透明度,默认为1,1不透明 0 全透明x,yView的终点位置
使用
ObjectAnimator时,要调用某个属性,该属性需要有对应的get()和set()方法。若是没有,我们就需要自定义一个属性类或者包装类添加该方法
//MainActivity
val myView = MyView(binding.button)
ObjectAnimator.ofInt(myView,"width",500).setDuration(500).start()
//MyView,给MyView里面的 width 添加一个 set() 和 get() 功能
class MyView(private val mTarget: View) {
var width: Int
get() = mTarget.layoutParams.width
set(width) {
mTarget.layoutParams.width = width
mTarget.requestLayout()
}
}
ValueAnimator
这个方法不提供动画效果,类似数值发生器,你需要根据里面的 AnimatorUpdateListener 来监听数值,设置动画变化
//传入的值被 a.animatedValue 获取到,根据该值设置做动画
val animator = ValueAnimator.ofFloat(0f,100f).apply {
setTarget(binding.button2)
duration = 1000
start()
addUpdateListener { a ->
val mFloat = a.animatedValue as Float
binding.button2.rotation = mFloat
binding.button2.translationX = 100f
}
}
//复杂些的动画
binding.button8.setOnClickListener {
val anim = ValueAnimator.ofFloat(0f, 360f)
anim.addUpdateListener { animation ->
val angle = animation.animatedValue as Float
binding.layer.rotation = angle
binding.layer.scaleX = 1 + (180 - Math.abs(angle - 180)) / 20f
binding.layer.scaleY = 1 + (180 - Math.abs(angle - 180)) / 20f
var shift_x = 500 * Math.sin(Math.toRadians((angle * 5).toDouble())).toFloat()
var shift_y = 500 * Math.sin(Math.toRadians((angle * 7).toDouble())).toFloat()
binding.layer.translationX = shift_x
binding.layer.translationY = shift_y
}
anim.duration = 4000
anim.start()
}
动画的监听
//完整的监听,四个过程
ObjectAnimator.ofFloat(binding.layer,"alpha",1.5f).addListener(object : Animator.AnimatorListener{
override fun onAnimationStart(p0: Animator?) {
TODO("Not yet implemented")
}
override fun onAnimationEnd(p0: Animator?) {
TODO("Not yet implemented")
}
override fun onAnimationCancel(p0: Animator?) {
TODO("Not yet implemented")
}
override fun onAnimationRepeat(p0: Animator?) {
TODO("Not yet implemented")
}
})
//不完整的监听,匿名类中,重写其中的一个方法
ObjectAnimator.ofFloat(binding.layer,"alpha",0f,1f,0f,1f,0f,1f).apply {
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
super.onAnimationEnd(animation)
}
})
duration = 10000
start()
}
AnimatorSet 组合动画
我们可以调用 AnimatorSet 里面的 play() 方法,以及该方法内部的类,就可以完成多个动画的组合展示功能
//eg,下面的执行顺序是 a3 -> a2 -> a1
val a1 = ObjectAnimator.ofFloat(binding.coutomView,"translationX",0f,300f,0f)
val a2 = ObjectAnimator.ofFloat(binding.coutomView,"scaleX",1.0f,2.0f)
val a3 = ObjectAnimator.ofFloat(binding.coutomView,"rotationX",0.0f,90f,0.0f)
val set = AnimatorSet().apply {
duration = 1000
play(a1).with(a2).after(a3)
start()
}
//简单展示下对应方法的结构
//play()
public AnimatorSet.Builder play(Animator anim){
if(anim != null) return new Builder(anim);
return null;
}
//Builder 结构,对应源码可自行查看
public class Builder {
Builder() {
throw new RuntimeException("Stub!");
}
public AnimatorSet.Builder with(Animator anim) {
throw new RuntimeException("Stub!");
}
public AnimatorSet.Builder before(Animator anim) {
throw new RuntimeException("Stub!");
}
public AnimatorSet.Builder after(Animator anim) {
throw new RuntimeException("Stub!");
}
public AnimatorSet.Builder after(long delay) {
throw new RuntimeException("Stub!");
}
}
after(Animator anim)当下Builder的动画放到传入的动画之后执行
after(long delay)当下Builder的动画延迟指定的毫秒执行
before(Animator anim)当下Builder的动画放到传入的动画之前执行
with(Animator anim)当下Builder的动画与传入的动画并行执行
根据这个属性,浅析一下这段代码的逻辑。play(a1).with(a2).after(a3),
- 首先传入
play()的是a1,会返回一个含有a1的Builder对象,我们简称这个对象为b1 - 再次调用
with()传入a2,其实就是传入b1的with()中。那当前动画就是a1,传入的是a2,两个并行执行。最后会返回this即为b1 - 最后再调用
after()传入a3,也还是b1内部的after()中。即当前动画还是a1,传入的是a3,a1在a3后面执行。最后会返回this即为b1
所以最终的顺序是 :
a3->a1/a2由于这几个方法都是同一个对象内的,所以当前动画
currentNode是不变的,一直都是a1。那么其他需要组合的动画,都还会是以a1为主题,看是插入到他的前或者后。如果有两个动画是放置与同一个位置,即
play(a1).after(a2).after(a3)。那么a2和a3是并行执行的,即顺序为a2/a3->a1
PropertyValuesHolder 组合动画
该动画无法实现前后关系,都是并行执行的。用法如下
val valuesHolder1 = PropertyValuesHolder.ofFloat("scaleX",1.0f,1.5f)
val valuesHolder2 = PropertyValuesHolder.ofFloat("rotationX",0.0f,90.0f,0.0f)
val valuesHolder3 = PropertyValuesHolder.ofFloat("alpha",1.0f,0.3f,1.0f)
ObjectAnimator.ofPropertyValuesHolder(binding.coutomView,valuesHolder1,valuesHolder2,valuesHolder3).apply {
duration = 2000
start()
}
xml 使用属性动画
//aimator.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>
AnimatorInflater.loadAnimator(this,R.animator.scale).apply {
setTarget(binding.coutomView)
start()
}
Scroller
graph LR
A[startScroll] --> B[invalidate] --> C[draw] --> D[computeScroll]
public void smoothScrollTo(int destX,int destY){
int scrollX = getScrollX();
int delta = destX - scrollX;
mScroller.startScroll(scrollX,0,delta,0,2000);
invalidate();
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()){
((View)getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
invalidate();
}
}
Scroller 并不能直接实现滑动,他最大的功能是在 startScroll() 处保存传入的滑动信息。后面再不断调用到 computeScroll() 这个方法,使用其中的 scrollTo() 来实现滑动。
computeScroll() 中,在判断方法中会调用到 mScroller.computeScrollOffset() ,这是用于获取 scrollX 和 scrollY 两个位置参数以及做出判断是否滑动结束。若是未滑动结束,就会让 computeScroll() 不断滑动重绘。
View事件分发

Activity 的层级基本如上所示,在 xml 文件中构建的布局就是在 contentParent 位置,也就是 contentView 位置。
分发机制
首先需要了解的是 MotionEvent ,当屏幕被点击 ->产生点击事件 ->MotionEvent 产生。
点击事件产生后层层下发,不断传递到根 ViewGroup 。
graph LR
A[MotionEvent] --> B[Activity]
B --> C[PhoneWindow]
C --> D[DecorView]
D --> E[ViewGroup]
事件分发的三大方法
dispatchTouchEvent(MotionEvent event): 用以事件分发。下面简称dTE()方法onInterceptTouchEvent(MotionEvent e): 用以拦截事件,在dispatchTouchEvent(MotionEvent event)中被调用来拦截。该方法只有ViewGroup中有,View中没有onTouchEvent(MotionEvent e): 用以处理点击事件,在dispatchTouchEvent(MotionEvent event)中被调用。这个方法是View中的,但是由于ViewGroup是继承自View的,所以ViewGroup可以使用。下面简称oTE()方法

下面简述一下 dispatchTouchEvent(MotionEvent event) 方法,
//ViewGroup内
public boolean dispatchTouchEvent(MotionEvent ev){
//拦截部分
...
onInterceptTouchEvent(ev);
...
//点击处理事件
...
if(dispatchTransformedTouchEvent(ev,false,child,idBitsToAssign))
...
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child,int desiredPointerIdBits){
...
if(child == null){
handled = super.dispatchTouchEvent(event);
}else{
handled = child..dispatchTouchEvent(event);
}
...
}
//View内
public boolean dispatchTouchEvent(MotionEvent ev){
if(... && li.mOnTouchListener != null && li.mOnTouchListener.onTouch(this,event)){
result = true;
}
if(!result && onTouchEvent(event)){
result = true;
}
}
拦截的处理逻辑
st=>start: 传入ev
req=>condition: 允许拦截?
op=>operation: 发起拦截事件
cond=>condition: 没被拦截 或 DOWN事件?
intY=>operation: 设置被拦截为 true
intN=>operation: 设置被拦截为 false
e=>end: end
st->cond->req->op
cond(yes)->req
cond(no)->intY
req(yes)->op
req(no)->intN
这其中的
允许拦截?,一般通过子View的requestDisallowInterceptTouchEvent来设置,这也是处理滑动冲突的方法之一。当事件在
ViewGroup被拦截之后,后续的事件序列都交给其处理了
点击事件处理逻辑
st=>start: 事件已被当前VG拦截
conLoop=>condition: 子View数量C >= 0?
conView=>condition: 触点在子View内 或 子View在播动画?
op1=>operation: 执行dTTE()方法
conSC=>condition: 子View == null?
opC=>operation: 执行子View的dTE()方法
opS=>operation: 执行父View的dTE()方法
conOnTouchListener=>condition: (OTListener!=null)&&(.onTouch==true)
opRdT1=>operation: View的dTE层级result = true,表示事件被消费
opOTE=>operation: 执行oTE()方法
conUPL=>condition: 点击或长按?
opperformClick=>operation: performClick
opRoTt=>operation: OTE层 return true,表示消耗该事件
opRoTf=>operation: OTE层 return false,表示不消耗该事件
conOnClick=>condition: OnClickListener被设置?
opOnClick=>operation: 执行onClick()方法
opOnClickT=>operation: performClick层 result = true
opOnClickF=>operation: performClick层 result = false
opRdT2=>operation: View的dTE层级result = true,表示事件被消费
opRdT3=>operation: View的dTE层级result = false,事件未被消费
opSuperdTE=>operation: 父View的OTE处理
opOmit=>operation: ......
sub=>operation: C--
end1=>end: END
end2=>end: END
end3=>end: END
st->conLoop->conView->op1->conSC->opS->conOnTouchListener->opOTE->conUPL->opperformClick->conOnClick
opRoTt->opRdT2->end2
conLoop(yes)->conView
conLoop(no)->end1
conView(yes)->op1
conView(no)->sub->conLoop
conSC(yes)->opS->conOnTouchListener
conSC(no)->opC->conOnTouchListener
conOnTouchListener(yes)->opRdT1->end3
conOnTouchListener(no)->opOTE
conUPL(yes)->opperformClick->conOnClick
conUPL(no)->opRoTf->opRdT3->opSuperdTE->opOmit
conOnClick(yes)->opOnClick->opOnClickT->opRoTt
conOnClick(no)->opOnClickF->opRoTt
事件被拦截后会被当前的
ViewGroup处理,上图就是详细的点击事件处理流程图。注意:onTouch() 优先级大于 onClick(),事实上, View.OnClickListener()等常用点击时间都是对交互时间的二次封装
事件分发传递规则
View 的事件分发是,首先 View 层层分发下来,若是 onInterceptTouchEvent(ev) 为 true 就拦截,为 false 就继续下发。
当某一层级拦截后,就调用 onTouchEvent(event) 来处理,若是该层无法处理,就传递给父层的 onTouchEvent(event) 来处理。如此层层传递直到有对应可以处理的父层。
这是一个V型结构,先下后上。
整个事件的分发过程看起来复杂,当最终归于三大方法可以用下面的伪代码表示
public boolean dispatchTouchEvent(MotionEvent ev){
boolean result = false;
if(onInterceptTouchEvent(ev)){
result = onTouchEvent(ev);
}else{
result = child.dispatchTouchEvent(ev);
}
return result;
}
问题
设置点击事件逻辑的时候,最基础的办法就是先用 findViewById() 来绑定实例,其次就是设置一个匿名内部类来监听点击,继而处理事件。那么我们可以提出以下的问题。
findViewById()如何找到并绑定对应的 View首先,
findViewById()会做一个层层代理,执行到DecorView这一层的findViewById()中。然后,
DecorView本质上是ViewGroup,那么就变成了在ViewGoup上寻找对应的View。由于
ViewGroup是继承自View的,我们先来查看View的代码//View public <T extends View> T findViewById(@IdRes int id) { if(id == NO_ID){ return Null; } return findViewTraversal(id); } public <T extends View> T findViewTraversal(@IdRes int id) { if(id == mID){ return (T)this; } return null; }从上述的代码可以看出,View 中如果 id 不存在就会返回null;如果存在且等于自己,就会返回自己。
下面我们来看 ViewGroup 中的代码
//ViewGroup /** * {@hide} */ @Override protected <T extends View> T findViewTraversal(@IdRes int id) { if (id == mID) { return (T) this; } final View[] where = mChildren; final int len = mChildrenCount; for (int i = 0; i < len; i++) { View v = where[i]; if ((v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) { v = v.findViewById(id); if (v != null) { return (T) v; } } } return null; }ViewGroup是继承自View的,直接使用了 View 的findViewById()方法,所以只复写了ViewGroup的findViewTraversal()。其处理逻辑是,迭代查看它的哪个子View符合,然后返回;否则返回 null。我们常重写的 onClick() 是和 onLongClick() 他们同时设置的话会同时执行吗?执行逻辑有何不同?
这两个方法如何执行的,我们需要查看它的源码
//View public boolean onTouchEvent(MotionEvent event) { ... if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { case MotionEvent.ACTION_UP: mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; ... if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { ... if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // This is a tap, so remove the longpress check removeLongPressCallback();//标注2 // Only perform take click actions if we were in the pressed state if (!focusTaken) { // Use a Runnable and post this rather than calling // performClick directly. This lets other visual state // of the view update before click actions start. if (mPerformClick == null) { mPerformClick = new PerformClick();//标注1 } if (!post(mPerformClick)) { performClickInternal(); } } ... mIgnoreNextUpEvent = false; break; case MotionEvent.ACTION_DOWN: ... if (!clickable) { checkForLongClick(//标注3 ViewConfiguration.getLongPressTimeout(), x, y, TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS); break; } ... break; ... } return true; } return false; }在上文我们知道
onClick()方法是通过上述代码标注1处来设置的,可知手指离开屏幕的时候,此处执行的是普通的点击事件。下面我们说下普通点击和长按点击分别如何执行。现在我们再看回标注3,此处执行的是
checkForLongClick(),即一点击屏幕就会执行该方法,该方法会开启一个线程来计时和处理onLongClick()。然后就看到条件为手指离开屏幕处的标注2,此处是判断此时是否已经执行了onLongClick(),若是未执行,就说明是未能到达触发条件,此时移除不执行长按事件。移除后,就会去到标注3处,执行onClick()普通的点击事件。常用的交互事件和触摸事件有哪些
交互事件

上文方法中的
onTouch()和onClick()之类的方法的关系是前者包含后者,包括onLongClick()都是基于此封装的。onTouch()是他们的入口。触摸事件

总结
以上就是 View 体系的基础内容,理解 View 的事件分发原理,是我们能化用 View 的前提。View 处理事件层层下发的思想,是非常具有借鉴学习价值的,我们代码的设计也可以借鉴这套思想,提高代码的质量。
View 事件分发的一些结论
一般一个事件序列只能被一个 View 拦截并消耗,消耗的地点在
onTouchEvent。同时如果在onTouchEvent做处理的话,可以将事件传递给其他View,比如返回false,就可以将事件传递给上层View做处理。当
View不消耗ACTION_DOWN的时候,那么就是onTouchEvent会返回false,即将事件传递给父元素处理了。如果消耗的话,那么同一时间序列都会被处理掉。ViewGroup的onInterceptTouchEvent默认返回false。View没有onInterceptTouchEvent方法,收到事件后,就会立即调用onTouchEvent消耗事件。View默认消耗事件,即为onTouchEvent返回true。但当它是不可点击的时候,则返回false,即clickable和longClickable同时为false时候。注意enable属性不会对其返回值产生影响。当
View是可点击的,且收到down和up的事件时候,onClick才会发生。requestDisallowInterceptTouchEvent方法会改变FLAG_DISALLOW_INTERCEPTER标记位,当该FLAG被设置时候,就无法拦截除了 ACTION_DOWN 之外的其他事件。因为 ACTION_DOWN 会重置FLAG_DISALLOW_INTERCEPTER标记位,导致标记位无效。所以,一般是在 ACTION_DOWN 中调用
onInterceptTouchEvent来判断是否做拦截。onInterceptTouchEvent不是每次都会被调用的,所以最稳妥想处理事件的方式是在dispatchTouchEvent处理。OnTouchListener>OnTouchEvent>OnClickListener,这样放置的原因,是为了让 OnTouchListener 方便在外部调用。
参考文章
Android中View的继承关系图_Huangrong_000的博客-CSDN博客
布局 | Android 开发者 | Android Developers (google.cn)
android之View坐标系(view获取自身坐标的方法和点击事件中坐标的获取)_炸斯特的博客-CSDN博客
Activity 的组成 - 简书 (jianshu.com)
《Android进阶之光(第2版) (博文视点出品)》(刘望舒)【摘要 书评 试读】- 京东图书 (jd.com)