Android 自定义控件onTouch事件浅析和个人总结

本文探讨了一个按钮的点击效果实现及其事件分发机制。通过自定义Button类,实现了按钮按下缩放和松手恢复原状的效果。文章详细分析了事件处理流程,特别是事件返回值对按钮状态的影响,以及事件分发机制的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文主要参考博客:http://www.cnblogs.com/sunzn/archive/2013/05/10/3064129.html
  http://blog.youkuaiyun.com/guolin_blog/article/details/9097463
 http://blog.youkuaiyun.com/guolin_blog/article/details/9153747


哦对了,我基础很差,所以如果有不对的地方,那是相当正常的!当然也希望有研究的朋友看到我这篇毫无技术含量的文章给予一点点评(笑)。


事情是这样的,最近在搞一个按钮的点击效果,当然效果很简单。按下去缩小,松手就返回原来的大小,一般情况都是自定义一个Button,然后重写OnTouch事件,监听手势ACTION_DOWN和ACTION_UP。
代码很简单
public class CustomButton extends Button{


public CustomButton(Context context) {
super(context);
// TODO Auto-generated constructor stub
init(context);
}


public CustomButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// TODO Auto-generated constructor stub
init(context);
}


public CustomButton(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
init(context);
}

ScaleAnimation sa, sa2;
private void init(Context context){
sa = new ScaleAnimation(1.0f, 0.75f, 1.0f, 0.75f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
sa.setDuration(200);
sa.setFillAfter(true);
sa2 = new ScaleAnimation(0.75f, 1.0f, 0.75f, 1.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
sa2.setDuration(200);
sa2.setFillAfter(true);

   }
   
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// TODO Auto-generated method stub
System.out.println( "Childs | dispatchTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
return super.dispatchTouchEvent(ev);
}



   @Override
   public boolean onTouchEvent(MotionEvent event) {
if(event.getAction() == MotionEvent.ACTION_DOWN){
   startAnimation(sa);
   
}else if(event.getAction() == MotionEvent.ACTION_UP){
   startAnimation(sa2);
}
   System.out.println("~~~~~~~~~~~~~"+super.onTouchEvent(event));
       return super.onTouchEvent(event);
       
       
   }
   


}




后来突发奇想,想要这个Button隐藏掉。这时候奇怪的事情就发生了。
先说说操作流程吧。
我有按钮A,B,C,总共3个按钮,都是自定义上面代码的Button。
ButtonA啥也不干。
ButtonB用于显示ButtonA
ButtonC用于隐藏ButtonA
A一开始是显示的
假如我直接点了C,A会隐藏掉,在按B,A又出来了。
但是,假如我一开始点了一下A,然后再去点击C。A是不会被隐藏的
这就很诡异了。
会不会是重写的OnTouchEvent里面返回的布尔值的问题呢?
我首先打印了super的返回
<span style="font-size:18px;">System.out.println("~~~~~~~~~~~~~"+super.onTouchEvent(event));</span>
<span style="font-size:18px;"> return super.onTouchEvent(event);</span>
得到的打印结果是true。3个都如此。
那我试试把return 改成false好了。
打印的结果还是true,不过和上一次不同的是。
在上一次。按下和松手的动画效果都能出来。但是当返回值改为false的时候,按钮的效果只有按下缩小。在我松手之后,按钮并没有执行回到原来大小的动画,而且我松手之后也没有继续打印了。
这是为什么呢?
度娘了一下关于OnTouchEvent返回值的问题,得到这样的答案:


如果事件传递到当前 View 的 onTouchEvent 方法,而该方法返回了 false,那么这个事件会从当前 View 向上传递,并且都是由上层 View 的 onTouchEvent 来接收,如果传递到上面的 onTouchEvent 也返回 false,这个事件就会“消失”,而且接收不到下一次事件。
如果返回了 true 则会接收并消费该事件。
如果返回 super.onTouchEvent(ev) 默认处理事件的逻辑和返回 false 时相同。


还有一种说法就是


1,return false说明你还没消费onTouch事件,在执行完你onTouch里面的代码之后,onTouch事件并没有结束。就是会自动地执行Gallery这个view里onTouch代码(这个为默认).所以这就是为什么没增加你的处理的时候就只自动地调用Gallery的onTouch,若你在onTouch里面增加你的代码并且return false就会执行你的处理和默认的处理。 


2,return true说明你已经消费完了onTouch事件,在执行完你的onTouch里面的代码之后,这个onTouch事件就结束了。也就是说不会再调用默认的onTouch事件了。在onTouch里面有很多种的处理比如move,down,up....,若你在move里面return false,那么接着的fling,up等后面的事件也不会处理的。 


好神奇。明明我打印super.onTouchEvent(event)是返回true啊,这是什么情况?(暂时不知道,先放一边)
综合上面2种解释,我总结一下我的思路,
1.在返回false的时候。其实已经执行了ACTION_DOWN了,根据最最最最基本的java自上而下的流程,先走完down事件(没有down哪里什么up、move的后续操作嘛对不对),然后得到返回false之后,下一次的事件就交给上一层View的OnTouchEvent事件去处理,但是上一层返回的是super。而super的默认操作逻辑就跟false是一样的,所以继续往上传递,一直到Activity,交给Activity的onTouchEvent去处理。
2.在返回true的时候,就等于告诉上一层view,后面的事件就交给我吧。然后就能够继续接收后续的事件


假如在方法里面我返回false,就是我按下的动画被执行之后,因为false的原因,事件以冒泡的形式传递到上层的view。上层的view我没有设置onTouchEvent方法,所以默认的就是super.onTouchEvent(ev),的方法。由于super默认处理的逻辑和false是相同的。所以继续传递到上层Activity。然后被消费掉了。所以导致我的按钮按下后只执行了缩小。


参考上面的博文。我决定了解一下事件分发的机制。


先说说onTouch是如何找到相对应的控件的。我们在点击Activity的时候,从Activity开始。然后到Viewgroup,再到view一层层地往下传递。而onTouchEvent事件则相反地一层层从最小的控件,以冒泡的形式往上层传递。


而每一层都有dispatchTouchEvent(MotionEvent ev)、onInterceptTouchEvent(MotionEvent ev)、onTouchEvent(MotionEvent ev);三个方法。


这3个方法都是布尔类型,一般来说不是true就是false的啦。但是它们还有一个super的默认返回,这个返回目前对我而言就很诡异了。
就好像布尔类型true为1,false为0.而这个super就感觉像是个-1一样。
下面简单说说3个方法不同返回值带来不同的效果


事件分发:dispatchTouchEvent(MotionEvent ev);
Touch事件发生时,从Activity的Touch 事件发生时 Activity 的 dispatchTouchEvent(MotionEvent ev) 方法会以隧道方式(从根元素依次往下传递直到最内层子元素或在中间某一元素中由于某一条件停止传递)将事件传递给最外层 View 的 dispatchTouchEvent(MotionEvent ev) 方法,并由该 View 的 dispatchTouchEvent(MotionEvent ev) 方法对事件进行分发。dispatchTouchEvent 的事件分发逻辑如下:


如果 return true,事件会分发给当前 View 并由 dispatchTouchEvent 方法进行消费,同时事件会停止向下传递;(分不分出去啊?分,才怪呢!停在这吧!)
如果 return false,事件分发分为两种情况:(分不分出去啊?不分啊~又不关我事~)
如果当前 View 获取的事件直接来自 Activity,则会将事件返回给 Activity 的 onTouchEvent 进行消费;
如果当前 View 获取的事件来自外层父控件,则会将事件返回给父 View 的 onTouchEvent 进行消费。
如果返回系统默认的 super.dispatchTouchEvent(ev),事件会自动的分发给当前 View 的 onInterceptTouchEvent 方法。(分不分啊?啊?不知道诶~问问我朋友先~)




事件拦截: onInterceptTouchEvent(MotionEvent ev)


在外层 View 的 dispatchTouchEvent(MotionEvent ev) 方法返回系统默认的 super.dispatchTouchEvent(ev) 情况下,事件会自动的分发给当前 View 的 onInterceptTouchEvent 方法。onInterceptTouchEvent 的事件拦截逻辑如下:


如果 onInterceptTouchEvent 返回 true,则表示将事件进行拦截,并将拦截到的事件交由当前 View 的 onTouchEvent 进行处理;(截不截下来?肯定截啊,我这边解决就好啦)
如果 onInterceptTouchEvent 返回 false,则表示将事件放行,当前 View 上的事件会被传递到子 View 上,再由子 View 的 dispatchTouchEvent 来开始这个事件的分发;(不截了,放行下去看看下面的人要不要吧)
如果 onInterceptTouchEvent 返回 super.onInterceptTouchEvent(ev),事件默认会被拦截,并将拦截到的事件交由当前 View 的 onTouchEvent 进行处理。(还是截吧~)
 




事件响应:onTouchEvent(MotionEvent ev)


在 dispatchTouchEvent 返回 super.dispatchTouchEvent(ev) 并且 onInterceptTouchEvent 返回 true 或返回 super.onInterceptTouchEvent(ev) 的情况下 onTouchEvent 会被调用。onTouchEvent 的事件响应逻辑如下:


如果事件传递到当前 View 的 onTouchEvent 方法,而该方法返回了 false,那么这个事件会从当前 View 向上传递,并且都是由上层 View 的 onTouchEvent 来接收,如果传递到上面的 onTouchEvent 也返回 false,这个事件就会“消失”,而且接收不到下一次事件。(不搞了,问问上面的人搞不搞吧)
如果返回了 true 则会接收并消费该事件。(把这事搞定好了)
如果返回 super.onTouchEvent(ev) 默认处理事件的逻辑和返回 false 时相同。(看看上面的人怎么说的)




onTouchEvent举个例子。今天你中了500万彩票(我也想中啊!),你不告诉你爸爸,自己解决掉,为true,如果你告诉你爸爸,就为false,同理,你爸爸告诉了你爷爷,就代表你爸爸也为false。很好懂吧


看着好像很好理解的样子吧,如果不需要深入了解,的确明白了这3个方法就可以了。
但是只要稍微往下想就觉得有点不太对劲。比如,为什么我点击控件,系统能知道呢?为什么响应的不是父控件呢?好的,让我接着往下分析。




当一个Touch事件(触摸事件为例)到达根节点,即Acitivty的ViewGroup时,它会依次下发,下发的过程是调用子View(ViewGroup)的dispatchTouchEvent方法实现的。简单来说,就是ViewGroup遍历它包含着的子View,调用每个View的dispatchTouchEvent方法,而当子View为ViewGroup时,又会通过调用ViwGroup的dispatchTouchEvent方法继续调用其内部的View的dispatchTouchEvent方法。dispatchTouchEvent方法只负责事件的分发,它拥有boolean类型的返回值,当返回为true时,顺序下发会中断。在此可以看出,ViewGroup的dispatchTouchEvent是真正在执行“分发”工作,而View的dispatchTouchEvent方法,并不执行分发工作,或者说它分发的对象就是自己,决定是否把touch事件交给自己处理,而处理的方法,便是onTouchEvent事件,

咱们来看看ViewGroup和View的dispatchTouchEvent的区别吧。

先看看ViewGroup里面的方法

public boolean dispatchTouchEvent(MotionEvent ev) {  
    final int action = ev.getAction();  
    final float xf = ev.getX();  
    final float yf = ev.getY();  
    final float scrolledXFloat = xf + mScrollX;  
    final float scrolledYFloat = yf + mScrollY;  
    final Rect frame = mTempRect;  
    boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;  
    if (action == MotionEvent.ACTION_DOWN) {  
        if (mMotionTarget != null) {  
            mMotionTarget = null;  
        }  
        if (disallowIntercept || !onInterceptTouchEvent(ev)) {  
            ev.setAction(MotionEvent.ACTION_DOWN);  
            final int scrolledXInt = (int) scrolledXFloat;  
            final int scrolledYInt = (int) scrolledYFloat;  
            final View[] children = mChildren;  
            final int count = mChildrenCount;  
            for (int i = count - 1; i >= 0; i--) {  
                final View child = children[i];  
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE  
                        || child.getAnimation() != null) {  
                    child.getHitRect(frame);  
                    if (frame.contains(scrolledXInt, scrolledYInt)) {  
                        final float xc = scrolledXFloat - child.mLeft;  
                        final float yc = scrolledYFloat - child.mTop;  
                        ev.setLocation(xc, yc);  
                        child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
                        if (child.dispatchTouchEvent(ev))  {  
                            mMotionTarget = child;  
                            return true;  
                        }  
                    }  
                }  
            }  
        }  
    }  
    boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||  
            (action == MotionEvent.ACTION_CANCEL);  
    if (isUpOrCancel) {  
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;  
    }  
    final View target = mMotionTarget;  
    if (target == null) {  
        ev.setLocation(xf, yf);  
        if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {  
            ev.setAction(MotionEvent.ACTION_CANCEL);  
            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
        }  
        return super.dispatchTouchEvent(ev);  
    }  
    if (!disallowIntercept && onInterceptTouchEvent(ev)) {  
        final float xc = scrolledXFloat - (float) target.mLeft;  
        final float yc = scrolledYFloat - (float) target.mTop;  
        mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
        ev.setAction(MotionEvent.ACTION_CANCEL);  
        ev.setLocation(xc, yc);  
        if (!target.dispatchTouchEvent(ev)) {  
        }  
        mMotionTarget = null;  
        return true;  
    }  
    if (isUpOrCancel) {  
        mMotionTarget = null;  
    }  
    final float xc = scrolledXFloat - (float) target.mLeft;  
    final float yc = scrolledYFloat - (float) target.mTop;  
    ev.setLocation(xc, yc);  
    if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {  
        ev.setAction(MotionEvent.ACTION_CANCEL);  
        target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
        mMotionTarget = null;  
    }  
    return target.dispatchTouchEvent(ev);  
}  
这个方法代码比较长,我们只挑重点看。首先在第13行可以看到一个条件判断,如果disallowIntercept和!onInterceptTouchEvent(ev)两者有一个为true,就会进入到这个条件判断中。disallowIntercept是指是否禁用掉事件拦截的功能,默认是false,也可以通过调用requestDisallowInterceptTouchEvent方法对这个值进行修改。那么当第一个值为false的时候就会完全依赖第二个值来决定是否可以进入到条件判断的内部,第二个值是什么呢?竟然就是对onInterceptTouchEvent方法的返回值取反!也就是说如果我们在onInterceptTouchEvent方法中返回false,就会让第二个值为true,从而进入到条件判断的内部,如果我们在onInterceptTouchEvent方法中返回true,就会让第二个值为false,从而跳出了这个条件判断。

那我们重点来看下条件判断的内部是怎么实现的。在第19行通过一个for循环,遍历了当前ViewGroup下的所有子View,然后在第24行判断当前遍历的View是不是正在点击的View,如果是的话就会进入到该条件判断的内部,然后在第29行调用了该View的dispatchTouchEvent,然后需要注意一下,调用子View的dispatchTouchEvent后是有返回值的。我们已经知道,如果一个控件是可点击的,那么点击该控件时,dispatchTouchEvent的返回值必定是true。因此会导致第29行的条件判断成立,于是在第31行给ViewGroup的dispatchTouchEvent方法直接返回了true。这样就导致后面的代码无法执行到了,如果View的点击事件得到执行,就会把ViewGroup的touch事件拦截掉。

那如果我们点击的不是view,而是空白区域呢?这种情况就一定不会在第31行返回true了,而是会继续执行后面的代码。那我们继续往后看,在第44行,如果target等于null,就会进入到该条件判断内部,这里一般情况下target都会是null,因此会在第50行调用super.dispatchTouchEvent(ev)。这句代码会调用到哪里呢?当然是View中的dispatchTouchEvent方法了,因为ViewGroup的父类就是View。之后的处理逻辑又和前面所说的是一样的了,也因此ViewGroup中注册的onTouch方法会得到执行。之后的代码在一般情况下是走不到的了,我们也就不再继续往下分析。
借用一下大神的文章里面的图来帮助了解一下:



接着咱们继续看看View当中dispatchTouchEvent方法的源码

<span style="font-size:18px;">public boolean dispatchTouchEvent(MotionEvent event) {  
    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
            mOnTouchListener.onTouch(this, event)) {  
        return true;  
    }  
    return onTouchEvent(event);  
}  </span>
这个方法的代码就非常少了,3个条件都满足的情况下,就会返回true。

咱们先看第一个条件,mOnTouchListener这个变量,说白了就是只要我们给控件设置了setOnTouchListener,就能够被赋值了。

第二个,是判断当前的控件是否enable。

至于第三个条件,其实是回调控件注册touch时间时的onTouch方法。

如果在onTouch方法里面返回true,则3个条件都会成立,然后返回true。

但是如果onTouch方法里面返回的是false,那就说明3个条件没有被满足,然后代码往下执行 onTouchEvent(event)的方法。

由此我们也可以知道了,onTouch方式是优先于onTouchEvent执行的。

还有一点是控件的onClickListener事件,是在onTouchEvent源码方法里面执行的,这里就不分析了


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值