# 自定义View
# Android View层级图
# 测量、布局、绘制
在Android中,所有View都继承自View.java这个类,包括ViewGroup。 在View这个类中有三个方法onMeasure、onLayout、onDarw。 分别负责实现View的测量、布局、和绘制逻辑。这三个方法会在从安卓的最顶层View(DecorView)依次递归执行到最底层的子View,来完成整个页面布局的展示。
在我们自定义View时,如果是自定义ViewGroup,则需要继承自ViewGroup或者其子类并实现这三个方法。 如果是自定义View,则需要继承自View或其子类并实现测量和绘制这两个方法。
# 测量方法
在测量方法中, 如果是自定义ViewGroup,那么要先调用每一个子view的测量方法,其中的参数是widthMeasureSpe 和 heightMeasureSpec, 这个两个参数就是父view对子view的宽高限制。 它是一个32位的int值, 前两位表示限制的模式,后30位表示宽高的值。 限制模式有三种:不限制、限制最高不能超过多少和限制具体值。 在自己的这个onMeasure方法中传进来的这两个参数也是父View传进来的。 根据父View传进来的对自己的宽高限制,再根据子View的布局参数,match,warp,具体值。 就可以得出应该给子view的widthMeasureSpe和heightMeasureSpec。 得出这个值后,调用子view的测量方法,让自View进行自我测量,会把测量结果保存在measureWidth和measureHeight这两个变量,可以通过调用子View的getMeasureWidth 和 getMeasureHeight获取.
# 布局方法
布局方法比较简单, 就是调用每一个子view的onLayout布局方法, 把这个子View相对于自身的位置参数左上右下传给它。 具体要怎么摆放子view根据需求计算得出即可。
# 绘制方法
绘制就是通过画笔和画布这两个对象来配合使用,完成绘制。 画笔是设置的一些公共参数, 颜色,大小,是否填充等。 而通过画布的.drawxxx这些方法就可以画具体的图形。 要注意,画复杂图形的画要用drawPath方法。 先把path组合出来再绘制。
# 事件分发
事件分发就是安卓中对事件事件处理的一套机制。 事件会从上往下从Activity页面分发到ViewGroup,再从ViewGroup分发到View。
# 从Activity分发到ViewGroup
事件会最先被分发到Activity的分发方法中, 在这个分发方法中,调用了phoneWindow的分发方法, phoneWindow的分发方法调用了DecorView的分发方法。 DecorView继承自FrameLayout,FrameLayout继承自ViewGroup,并且在这个继承关系中,没有重写ViewGroup的分发方法。 所以总结来说就是:Activity中的分发方法拿到事件后, 调用了DecorView这个安卓最顶层的视图组的父类的ViewGroup这个类的分发方法,这样就完成了事件从Activity分发到ViewGroup的过程。
# 从ViewGroup分发到View
ViewGroup拿到事件后, 要分发给它这个容器中的每一个子View, 在它的分发方法中, 有三块核心代码处理它的分发逻辑。
第一块代码:判断是否拦截,如果是down事件, 会清除标志位,调用自己的拦截方法。如果是其他事件会根据标志位来判断是否走拦截方法。
第二块代码:根据第一块代码的是否拦截这个返回值, 来判断是否走第二块第二块代码。 第二块代码是循环遍历自己的每一个子view并调用它们的分发方法,把事件分发给它们。这样就把事件从视图组分发给了视图。
第三块代码:根据第二块代码子view是否处理事件的返回值,来判断是否走自己消费事件, 自己消费事件是调用父类的分发方法super.dispatchTouchEvent, 因为ViewGroup的父类是View, View的分发方法中,处理的就是事件的消费, 它会先调用ouTouch方法, 根据onTouch方法的返回值, 来决定是否执行onTouchEvent方法, 在onTouchEvent方法中,就会处理点击,长按这些事件逻辑。 所以如果onTouch返回true的话, onTouchEvent就不会被执行, 点击事件就不会被执行。
**这样经过ViewGroup的分发方法后, 事件就完成了从ViewGroup分发到View的过程。 ** 每一个Android中的视图都是继承自ViewGroup或者View, 所以事件会从上往下的递归的走这个分发逻辑。
# 事件分发伪代码
View.java 分发方法(主要负责处理事件,把事件交给给自己的onTouch方法或者onTouchEvent处理)
事件如果到了View, 就不需要往下分发了, 因为没有子view。 所以View的分发方法和消费方法都是处理事件。 ViewGroup如果需要把事件交给View处理, 只需要调用这个view的分发方法。
public boolean dispatchTouchEvent(MotionEvent event) {
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
View.java的分发方法如上,调用onTouch,onTouch返回ture,则onTouchEvent不执行,false则执行。
在onTouchEvent中的up事件通过performClick处理了点击事件的回调。
所以, onTouch,onTouchEvent, onClick 的执行顺序是依次的。
并且, 这三个方法都不一定会执行, 因为必须走View.java的分发方法才会走这些逻辑, 如果分发方法被复写并且没有执行super.分发方法, 这块的逻辑就不会触发。
ViewGroup分发方法(三块代码)
1. 检查是否拦截, 执行拦截方法, 获取是否拦截。
2. 根据是否拦截和是否down事件,进行遍历子view,调用子view的分发方法,把事件分发给子view, 进入子view的分发方法(在没重写分发的情况下, 如果是继承自ViewGroup则重复这个过程,如果是继承自View,则进入View.java的分发方法)。
3. 根据子view是否消费事件,判断是自己消费事件还是子view消费事件。
第一块代码
// Check for interception.
public boolean dispatchTouchEvent(MotionEvent event) {
if(action==Down){
resetTouchState() //如果是down事件,重置状态, 会把FLAG_DISALLOW_INTERCEPT重置为初始化状态(允许拦截)
}
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
第二块代码 (不拦截,并且是down事件,才执行, 调用 dispatchTransformedTouchEvent来调用子view的分发方法,分发给子view,子view分发方法返回true,则break退出遍历,在这里赋值了newTouchTarget)
if (!canceled && !intercepted) {
if (actionMasked == MotionEvent.ACTION_DOWN) {
for (int i = childrenCount - 1; i >= 0; i--) {
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
}
第三块代码 (根据第二块代码的子view是否处理,判断自己消费还是子view消费,最后返回ViewGroup.java中这个分发方法的结果为handled)
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
}
}
}
# 解决事件冲突
根据事件分发原理,处理事件冲突可以有两种解决思路。
第一种是:在父view的拦截方法中判断是否拦截,来处理事件交给自身还是子view处理的逻辑
第二种是:父view不拦截,事件都分发到子view,子view调用是否允许父view拦截的方法来控制是否走父view的拦截方法让父view拦截。如果不允许父view拦截,则不会走父view的拦截方法,事件不会被拦截,会分发到子view给子view处理。 如果允许拦截则会走父view的拦截方法,事件被父view拦截,父view自己处理。
# 第一种思路代码
父ViewGroup.java
private int mLastPositionX;
private int mLastPositionY;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercept = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
intercept = false;
super.onInterceptTouchEvent(ev);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastPositionX;
int deltaY = y - mLastPositionY;
//这里写父view需要事件条件即可
if (Math.abs(deltaX) > Math.abs(deltaY)) {
intercept = true;
} else {
intercept = false;
}
break;
case MotionEvent.ACTION_UP:
intercept = false;
break;
default:
break;
}
mLastPositionX = x;
mLastPositionY = y;
return intercept;
}
# 第二种思路代码
父ViewGroup.java
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercept = false;
if (MotionEvent.ACTION_DOWN == ev.getAction()) {
super.onInterceptTouchEvent(ev);
return false;
}
return true;
}
子View.java
int mLastX,mLastY;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
final int action = ev.getAction() & MotionEvent.ACTION_MASK;
switch (action) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
//这里写父view需要事件条件即可
if (Math.abs(deltaX) > Math.abs(deltaY)){
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(ev);
}