# 自定义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);
    }
Last Updated: 1/9/2024, 11:22:13 AM