Android事件分发浅谈

   2016-09-29 0
核心提示:Android事件分发机制浅谈前言:可能Android的事件分发对于刚学Android的童鞋来说接触得不多,这样不奇怪。因为刚学的时候,一般人很难注意到或是会选择主动去了解。那么究竟什么是Android的事件分发呢?或许刚说出来,有点觉悟的新手会想到就是那些按钮的点击

Android事件分发机制浅谈

前言:可能Android的事件分发对于刚学Android的童鞋来说接触得不多,这样不奇怪。因为刚学的时候,一般人很难注意到或是会选择主动去了解。那么究竟什么是Android的事件分发呢?

或许刚说出来,有点觉悟的新手会想到就是那些按钮的点击事件、或是说监听。而这些也确实是Android事件分发的其中一部分。由于Android的事件分发其实是可以有很多变化的,特别是当你需要自定义View的时候,很多情况都需要具体分析,所以大体上它都不容易精确的掌握。但如果是主流的,大概的事件分发机制其实也没那么难理解,说到底这可以说是一个浅入深出的问题吧。那么接下来我们来浅谈一下这次的主题Android事件分发机制。

1.比喻

打个比方,Android的事件分发中的事件(用户的触屏)就像一块饼。假若有一家3代的人在饥荒的年代里,如果爷爷有一块饼,那么他会先给谁?那当然会是孙子。但爷爷年纪大视力不好,所以他把饼传递给了儿子,而儿子又把这块饼传给了孙子,最终由孙子吃下了那块饼。而像这样的父穿子,子传孙的方法,就如同我们Android中的事件分发一样。

Android事件分发浅谈

接下来我们新建一个工程并写好如上图的布局,这是一个最外层包着RelativeLayout(爷爷),中间是LinearLayout(儿子),最里面是Button(孙子)。

Android事件分发浅谈

图画得不是很好,大家就凑合这看吧。上面就是我们布局的上个View。而触摸事件(饼)就是通过RelativeLayout--->LinearLayout--->Button,这样一层层的传递的。而事件的分发就是这样通过disppatchTouchEvent(),OnInterceptTouchEvent(), OnTouchEvent()这三个方法去判断事件是否继续向下传递。

而当其中有一层View自己先截获了事件消费掉了(可理解为自己用掉了点击事件),那么事件则不会向下传递,而在OnTouchEvent中返回False则不会自己消费并返回到上一层去处理。

看到这里大家肯定还是不明白,接着到java代码里面,按着 Ctrl+ Shift+ T,查找一下View的API,这里我选API23的,其实API多少都没太大变化,我就选一个最新的。

Android事件分发浅谈

3.理解dispatchTouchEvent(),onInterceptTouchEvent(),OnTouchEvent()三个事件

这三个事件理解起来,首先要区分View与ViewGroup两种情况:

* View:dispatchTouchEvent,OnTouchEvent

* ViewGroup:dispatchTouchEvent,onInterceptTouchEvent,OnTouchEvent

其中View是没有 onInterceptTouchEvent方法的。 在Android中你只要触摸控件首先都会触发控件的dispatchTouchEvent方法,所以我们会找View的API,而不是找Button的API,因为这个方法一般在具体的控件类中是找不到的,他是父View的方法。

下面我贴出找到的 dispatchTouchEvent源码,看看官方是怎么解析的。

理解dispatchTouchEvent()
/**
     * Pass the touch screen motion event down to the target view, or this
     * view if it is the target.
     *
     * @param event The motion event to be dispatched.
     * @return True if the event was handled by the view, false otherwise.
     */
    public boolean dispatchTouchEvent(MotionEvent event) {
        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }
            // We have focus and got the event, then use normal event dispatch.
            event.setTargetAccessibilityFocus(false);
        }
        boolean result = false;
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }
        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }
        if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            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;
            }
        }
        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }
        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
        // of the gesture.
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }
        return result;
    }

看上面的官方解析,大概意思是:这个View接收的是从屏幕传递过来的事件并传给目标的View,或是这个View就是从屏幕传达过来的事件的目标。

这句话怎么理解呢?其实也就对应我上面画的图,两种情况:要么该View就是目标的View(自己消费掉事件),要么向下传递事件。 而这个方法默认是调用super. dispatchTouchEvent (event)的,需传入 super. dispatchTouchEvent (true),需要注意的是他如果直接返回true或flase而不是进过调用supper的都不会向下传递。这里可能有点难理解,不用急,下面会继续解析。

接着我们看看 onInterceptTouchEvent()方法。

理解 onInterceptTouchEvent ()

Android事件分发浅谈

同样的快捷键方法我们来到ViewGroup的源码:

/**
     * Implement this method to intercept all touch screen motion events.  This
     * allows you to watch events as they are dispatched to your children, and
     * take ownership of the current gesture at any point.
     *
     * <p>Using this function takes some care, as it has a fairly complicated
     * interaction with {@link View#onTouchEvent(MotionEvent)
     * View.onTouchEvent(MotionEvent)}, and using it requires implementing
     * that method as well as this one in the correct way.  Events will be
     * received in the following order:
     *
     * <ol>
     * <li> You will receive the down event here.
     * <li> The down event will be handled either by a child of this view
     * group, or given to your own onTouchEvent() method to handle; this means
     * you should implement onTouchEvent() to return true, so you will
     * continue to see the rest of the gesture (instead of looking for
     * a parent view to handle it).  Also, by returning true from
     * onTouchEvent(), you will not receive any following
     * events in onInterceptTouchEvent() and all touch processing must
     * happen in onTouchEvent() like normal.
     * <li> For as long as you return false from this function, each following
     * event (up to and including the final up) will be delivered first here
     * and then to the target's onTouchEvent().
     * <li> If you return true from here, you will not receive any
     * following events: the target view will receive the same event but
     * with the action {@link MotionEvent#ACTION_CANCEL}, and all further
     * events will be delivered to your onTouchEvent() method and no longer
     * appear here.
     * </ol>
     *
     * @param ev The motion event being dispatched down the hierarchy.
     * @return Return true to steal motion events from the children and have
     * them dispatched to this ViewGroup through onTouchEvent().
     * The current target will receive an ACTION_CANCEL event, and no further
     * messages will be delivered here.
     */
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getX(), ev.getY())) {
            return true;
        }
        return false;
    }

这里他的代码不是很多,但官方的注释却又很多,我们只看第一段,大概意思是:这个方法他拦截了所有的屏幕事件。他允许用户监测这些事件,并且可以分发到他的子View里面,作为子View的一个手势(或是说任何点)的手势。

所以从上面我们着重的可以知道, onInterceptTouchEvent()这个方法可以拦截事件的分发。而当 onInterceptTouchEvent()返回的是false的时候就说明不拦截这个View的子View,那么子View就可以获取到这个事件了。

OnTouchEvent()方法在View跟ViewGroup都有,所以要分开讨论。我们现在可以理解为,若是 OnTouchEvent()返回true则代表这一层的View自己消费掉事件,而返回false,那么事件会重新返回到该View的父View,也就是上一层的View中。

Android事件分发浅谈

当RelativeLayout在 onInterceptTouchEvent()里面不拦截子View的时候,事件就会传递到LinearLayout的 dispatchTouchEvent()事件里面。而同样LinearLayout也是继承ViewGroup的,所以他也有 onInterceptTouchEvent方法。

而LinearLayout的OnTouchEvent()里面,如果返回true则代表自己消费掉事件,而如果返回false则表示不作处理并返回给上层父View处理

4.结合例子理解

这个是我编写的上图的布局,里面三个都是简单的自定义View,作用是方便打印出信息

<org.dispatchtouchevent002.CustomRelativieLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="org.heima.dispatchtouchevent002.MainActivity" >
    <org.dispatchtouchevent002.CustomLinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical" >
        <org.dispatchtouchevent002.CustomButton
            android:id="@+id/btn"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="click Me" />
    </org.dispatchtouchevent002.CustomLinearLayout>
</org.dispatchtouchevent002.CustomRelativieLayout>

接下来我们在这些View里面重写 dispatchTouchEvent ()和 OnTouchEvent()两个方法,分别在里面打印Log,查看结果。

RelativeLayout

public class CustomRelativieLayout extends RelativeLayout {
	public CustomRelativieLayout(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
	}
	public CustomRelativieLayout(Context context, AttributeSet attrs) {
		super(context, attrs);
	}
	public CustomRelativieLayout(Context context) {
		super(context);
	}
	@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		return super.dispatchTouchEvent(ev);
	}
	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		return false;
	}
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		return super.onTouchEvent(event);
	}
}
LinearLayout
public class CustomLinearLayout extends LinearLayout{
	public CustomLinearLayout(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
	}
	public CustomLinearLayout(Context context, AttributeSet attrs) {
		super(context, attrs);
	}
	public CustomLinearLayout(Context context) {
		super(context);
	}
	
	@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		return super.dispatchTouchEvent(ev);
	}
	
	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		return super.onInterceptTouchEvent(ev);
	}
	
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		return super.onTouchEvent(event);
	}
}
Button
public class CustomButton extends Button{
	public CustomButton(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
	}
	public CustomButton(Context context, AttributeSet attrs) {
		super(context, attrs);
	}
	public CustomButton(Context context) {
		super(context);
	}
	
	@Override
	public boolean dispatchTouchEvent(MotionEvent event) {
		return super.dispatchTouchEvent(event);
	}
	
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		return super.onTouchEvent(event);
	}
}

1)首先我们来写三个View中的三个方法里的Log日志

RelativeLayout

@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomRelativieLayout:dispatchTouchEvent");
		return super.dispatchTouchEvent(ev);
	}
	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomRelativieLayout:onInterceptTouchEvent");
		return false;
	}
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		Log.d("TouchEvent", "CustomRelativieLayout:onTouchEvent");
		return super.onTouchEvent(event);
	}

LinearLayout

@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomLinearLayout:dispatchTouchEvent");
		return super.dispatchTouchEvent(ev);
	}
	
	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomLinearLayout:onInterceptTouchEvent");
		return super.onInterceptTouchEvent(ev);
	}
	
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		Log.d("TouchEvent", "CustomLinearLayout:onTouchEvent");
		return super.onTouchEvent(event);
	}
Button
@Override
	public boolean dispatchTouchEvent(MotionEvent event) {
		Log.d("TouchEvent", "CustomButton:dispatchTouchEvent");
		return super.dispatchTouchEvent(event);
	}
	
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		Log.d("TouchEvent", "CustomButton:onTouchEvent");
		return super.onTouchEvent(event);
	}

这里需要注意的是,在Activity中其实也含有 onInterceptTouchEvent()和 OnTouchEvent()这两个方法,所以我们也需要重写这两个方法。

MainActivity

public class MainActivity extends Activity {
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		Button btn=(Button) findViewById(R.id.btn);
		btn.setOnClickListener(new OnClickListener() {
			
			@Override
			public void onClick(View v) {
				Log.d("TouchEvent", "MainActivity:onClick");
			}
		});
	}
	@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "MainActivity:dispatchTouchEvent");
		return super.dispatchTouchEvent(ev);
	}
	
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		Log.d("TouchEvent", "MainActivity:onTouchEvent");
		return super.onTouchEvent(event);
	}
}

之后我们按下按钮。因为点击事件是由按下和抬起两部分组成的,所以上述的Log日志会打印两次。在Android里面,按下和抬起是分别处理的两个不同的事件,可以看到打印结果如下:

按下

09-27 17:27:41.222: D/TouchEvent(1493): MainActivity:dispatchTouchEvent
09-27 17:27:41.222: D/TouchEvent(1493): CustomRelativieLayout:dispatchTouchEvent
09-27 17:27:41.222: D/TouchEvent(1493): CustomRelativieLayout:onInterceptTouchEvent
09-27 17:27:41.222: D/TouchEvent(1493): CustomLinearLayout:dispatchTouchEvent
09-27 17:27:41.222: D/TouchEvent(1493): CustomLinearLayout:onInterceptTouchEvent
09-27 17:27:41.222: D/TouchEvent(1493): CustomButton:dispatchTouchEvent
09-27 17:27:41.222: D/TouchEvent(1493): CustomButton:onTouchEvent

抬起

09-27 17:27:41.321: D/TouchEvent(1493): MainActivity:dispatchTouchEvent
09-27 17:27:41.321: D/TouchEvent(1493): CustomRelativieLayout:dispatchTouchEvent
09-27 17:27:41.321: D/TouchEvent(1493): CustomRelativieLayout:onInterceptTouchEvent
09-27 17:27:41.321: D/TouchEvent(1493): CustomLinearLayout:dispatchTouchEvent
09-27 17:27:41.321: D/TouchEvent(1493): CustomLinearLayout:onInterceptTouchEvent
09-27 17:27:41.321: D/TouchEvent(1493): CustomButton:dispatchTouchEvent
09-27 17:27:41.321: D/TouchEvent(1493): CustomButton:onTouchEvent
09-27 17:27:41.331: D/TouchEvent(1493): MainActivity:onClick

总结1:

配合我画的图,结合上面的Log日志可以看出,点击事件最终被Button的onClick事件所消费。

MainActivity:

dispatchTouchEvent()为默认,向下传递。

Relative:

dispatchTouchEvent()为默认true,向下传递。

onInterceptTouchEvent()为默认flase。不拦截事件,向下传递。

LinearLayout:

dispatchTouchEvent()为默认true,向下传递。

onInterceptTouchEvent()为默认flase。不拦截事件,向下传递。

Button:

dispatchTouchEvent()为默认true,向下传递。

OnTouchEvent():为默认false,默认不处理

事件到了Button的OnTouchEvent()里面后,由于默认是false,OnTouchEvent()方法不处理。,又向最上层的父View返回了,

看到这里的Log日志再配合上面的图,是不是应该能稍微理解了一些呢?若是不了解的话,那还得自己多看几遍,或是自己也试试打印Log测试一下了。

2)接着我们来讨论上面理解dispatchTouchEvent()时出现的一种情况:dispatchTouchEvent()返回true或false時不向下传递事件,当只有调用super.dispatchTouchEvent()的时候才会。

在这里我试着修改RelativeLayout里面的dispatchTouchEvent(),其余布局不变

RelativeLayout: dispatchTouchEvent()返回true

@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomRelativieLayout:dispatchTouchEvent");
		return true;
	}

Log日志:

09-28 01:23:09.349: D/TouchEvent(1420): MainActivity:dispatchTouchEvent
09-28 01:23:09.349: D/TouchEvent(1420): CustomRelativieLayout:dispatchTouchEvent
09-28 01:23:09.630: D/TouchEvent(1420): MainActivity:dispatchTouchEvent
09-28 01:23:09.630: D/TouchEvent(1420): CustomRelativieLayout:dispatchTouchEvent
RelativeLayout: dispatchTouchEvent()返回false
@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomRelativieLayout:dispatchTouchEvent");
		return false;
	}
Log日志:
09-28 01:26:03.632: D/TouchEvent(1470): MainActivity:dispatchTouchEvent
09-28 01:26:03.632: D/TouchEvent(1470): CustomRelativieLayout:dispatchTouchEvent
09-28 01:26:03.632: D/TouchEvent(1470): MainActivity:onTouchEvent
09-28 01:26:03.822: D/TouchEvent(1470): MainActivity:dispatchTouchEvent
09-28 01:26:03.822: D/TouchEvent(1470): MainActivity:onTouchEvent

总结2:

当dispatchTouchEvent()返回true或者flase的时候,事件不向下传递,只有返回的是 super.dispatchTouchEvent()才会进行传递。并且返回false的时候事件会返回到上层View的onTouchEvent(),但如果父View的onTouchEvent为默认(默认不处理)。那么最终这个事件会没有响应,不被任何层的View所消费掉。

3)接下来我们调用super.dispatchTouchEvent(),但不把他返回,而是选择返回true或者false,观察情况如何:

RelativeLayout:调用super.dispatchTouchEvent(),返回true:

@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomRelativieLayout:dispatchTouchEvent");
		boolean event = super.dispatchTouchEvent(ev);
		Log.d("TouchEvent", "Touch:"+event);
		return true;
	}
Log日志:
09-28 02:26:36.926: D/TouchEvent(1580): CustomRelativieLayout:dispatchTouchEvent
09-28 02:26:36.926: D/TouchEvent(1580): CustomRelativieLayout:onInterceptTouchEvent
09-28 02:26:36.926: D/TouchEvent(1580): CustomLinearLayout:dispatchTouchEvent
09-28 02:26:36.926: D/TouchEvent(1580): CustomLinearLayout:onInterceptTouchEvent
09-28 02:26:36.926: D/TouchEvent(1580): CustomButton:dispatchTouchEvent
09-28 02:26:36.926: D/TouchEvent(1580): CustomButton:onTouchEvent
09-28 02:26:36.926: D/TouchEvent(1580): Touch:true
09-28 02:26:37.147: D/TouchEvent(1580): MainActivity:dispatchTouchEvent
09-28 02:26:37.147: D/TouchEvent(1580): CustomRelativieLayout:dispatchTouchEvent
09-28 02:26:37.147: D/TouchEvent(1580): CustomRelativieLayout:onInterceptTouchEvent
09-28 02:26:37.147: D/TouchEvent(1580): CustomLinearLayout:dispatchTouchEvent
09-28 02:26:37.147: D/TouchEvent(1580): CustomLinearLayout:onInterceptTouchEvent
09-28 02:26:37.147: D/TouchEvent(1580): CustomButton:dispatchTouchEvent
09-28 02:26:37.147: D/TouchEvent(1580): CustomButton:onTouchEvent
09-28 02:26:37.147: D/TouchEvent(1580): Touch:true
09-28 02:26:37.147: D/TouchEvent(1580): MainActivity:onClick

RelativeLayout:调用super.dispatchTouchEvent(),返回false:

@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomRelativieLayout:dispatchTouchEvent");
		boolean event = super.dispatchTouchEvent(ev);
		Log.d("TouchEvent", "Touch:"+event);
		return false;
	}
Log日志:
09-28 02:30:20.600: D/TouchEvent(1629): MainActivity:dispatchTouchEvent
09-28 02:30:20.600: D/TouchEvent(1629): CustomRelativieLayout:dispatchTouchEvent
09-28 02:30:20.600: D/TouchEvent(1629): CustomRelativieLayout:onInterceptTouchEvent
09-28 02:30:20.600: D/TouchEvent(1629): CustomLinearLayout:dispatchTouchEvent
09-28 02:30:20.600: D/TouchEvent(1629): CustomLinearLayout:onInterceptTouchEvent
09-28 02:30:20.600: D/TouchEvent(1629): CustomButton:dispatchTouchEvent
09-28 02:30:20.600: D/TouchEvent(1629): CustomButton:onTouchEvent
09-28 02:30:20.600: D/TouchEvent(1629): Touch:true
09-28 02:30:20.600: D/TouchEvent(1629): MainActivity:onTouchEvent
09-28 02:30:20.800: D/TouchEvent(1629): MainActivity:dispatchTouchEvent
09-28 02:30:20.800: D/TouchEvent(1629): MainActivity:onTouchEvent

总结3:

从日志来看,当有调用super.dispatchTouchEvent()并且返回true的情况下,跟我们结论1里,直接返回super.dispatchTouchEvent()的结果是一样的。

但当 super.dispatchTouchEvent()并且返回false时,事件走到一半就停止了,看到日志的第8行后ANCTION_DOWN(点下去)已经执行完了,而抬起来的事件只走到Activity里面的dispatchTouchEvent()就再也没有分发下去。

4)接着 onInterceptTouchEvent()返回true,则为拦截事件的分发。

RelativeLayout: onInterceptTouchEvent()返回ture:

@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomRelativieLayout:dispatchTouchEvent");
		return super.dispatchTouchEvent(ev);
	}
	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomRelativieLayout:onInterceptTouchEvent");
		return true;
	}
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		Log.d("TouchEvent", "CustomRelativieLayout:onTouchEvent");
		boolean touch = super.onTouchEvent(event);
		Log.d("TouchEvent", "onToucheEvent:"+touch);
		return super.onTouchEvent(event);
	}

Log日志:

09-28 03:07:40.314: D/TouchEvent(1803): MainActivity:dispatchTouchEvent
09-28 03:07:40.314: D/TouchEvent(1803): CustomRelativieLayout:dispatchTouchEvent
09-28 03:07:40.314: D/TouchEvent(1803): CustomRelativieLayout:onInterceptTouchEvent
09-28 03:07:40.314: D/TouchEvent(1803): CustomRelativieLayout:onTouchEvent
09-28 03:07:40.314: D/TouchEvent(1803): onToucheEvent:false
09-28 03:07:40.314: D/TouchEvent(1803): MainActivity:onTouchEvent
09-28 03:07:40.504: D/TouchEvent(1803): MainActivity:dispatchTouchEvent
09-28 03:07:40.504: D/TouchEvent(1803): MainActivity:onTouchEvent

结4:

这里,我在RelativeLayout里面的OnTouchEvent()方法打印了super.onTouchEvent的值,可以看到当为false。当onInterceptTouchEvent()为true时,事件不分发给下一层的子View,而选择走自己的onToucheEvent()方法,但又是默认的false不处理。导致事件回到上一层的父View中,最终父View的onTouchEvent()也是默认为false,不处理事件。最后导致事件没有任何View响应,也就没有消费。

5)在Activity里,给Button设置OnTouchListener,在onTouch()中返回false,默认不拦截。

MainActivity:onTouch()返回false:默认不拦截

btn.setOnClickListener(new OnClickListener() {
			
			@Override
			public void onClick(View v) {
				Log.d("TouchEvent", "MainActivity:onClick");
			}
		});
		
		btn.setOnTouchListener(new OnTouchListener() {
			
			@Override
			public boolean onTouch(View v, MotionEvent event) {
				Log.d("TouchEvent", "MainActivity:onTouch");
				return false;
			}
		});

RelativeLayout:onInterceptTouchEvent()返回false:不拦截事件

@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomRelativieLayout:dispatchTouchEvent");
		return super.dispatchTouchEvent(ev);
	}
 
	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		Log.d("TouchEvent", "CustomRelativieLayout:onInterceptTouchEvent");
		return true;
	}
 
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		Log.d("TouchEvent", "CustomRelativieLayout:onTouchEvent");s
		return super.onTouchEvent(event);
	}

Log日志:

09-28 04:01:34.433: D/TouchEvent(2172): MainActivity:dispatchTouchEvent
09-28 04:01:34.433: D/TouchEvent(2172): CustomRelativieLayout:dispatchTouchEvent
09-28 04:01:34.433: D/TouchEvent(2172): CustomRelativieLayout:onInterceptTouchEvent
09-28 04:01:34.433: D/TouchEvent(2172): CustomLinearLayout:dispatchTouchEvent
09-28 04:01:34.433: D/TouchEvent(2172): CustomLinearLayout:onInterceptTouchEvent
09-28 04:01:34.433: D/TouchEvent(2172): CustomButton:dispatchTouchEvent
09-28 04:01:34.433: D/TouchEvent(2172): MainActivity:onTouch
09-28 04:01:34.433: D/TouchEvent(2172): CustomButton:onTouchEvent
09-28 04:01:34.632: D/TouchEvent(2172): MainActivity:dispatchTouchEvent
09-28 04:01:34.632: D/TouchEvent(2172): CustomRelativieLayout:dispatchTouchEvent
09-28 04:01:34.632: D/TouchEvent(2172): CustomRelativieLayout:onInterceptTouchEvent
09-28 04:01:34.632: D/TouchEvent(2172): CustomLinearLayout:dispatchTouchEvent
09-28 04:01:34.632: D/TouchEvent(2172): CustomLinearLayout:onInterceptTouchEvent
09-28 04:01:34.632: D/TouchEvent(2172): CustomButton:dispatchTouchEvent
09-28 04:01:34.632: D/TouchEvent(2172): MainActivity:onTouch
09-28 04:01:34.632: D/TouchEvent(2172): CustomButton:onTouchEvent
09-28 04:01:34.632: D/TouchEvent(2172): MainActivity:onClick

总结5:

从上面的结果来看,事件在传递到Button的disptchTouchEvent(),然后回到Activity调用自己的onTouch(),dispatchTouchEvent结束后调用Button自己的onTouchEvent()(第8行),到此按下去的事件已经完成。

紧接着到抬起来的up事件。而执行的Log结果大致上也跟按下去的down事件差不多,但不同的是,up事件最终会在Button自己的OnTouchEvent()中响应,而onTouchEvent()会调用Buttton自己的onClick()方法,最后由onClick方法消费。

6)Button的onTouch()方法返回true

MainActivity:onTouchEvent返回true

btn.setOnTouchListener(new OnTouchListener() {
			@Override
			public boolean onTouch(View v, MotionEvent event) {
				Log.d("TouchEvent", "MainActivity:onTouch");
				return true;
			}
		});

Log日志:

09-28 04:23:15.712: D/TouchEvent(2223): MainActivity:dispatchTouchEvent
09-28 04:23:15.712: D/TouchEvent(2223): CustomRelativieLayout:dispatchTouchEvent
09-28 04:23:15.712: D/TouchEvent(2223): CustomRelativieLayout:onInterceptTouchEvent
09-28 04:23:15.712: D/TouchEvent(2223): CustomLinearLayout:dispatchTouchEvent
09-28 04:23:15.712: D/TouchEvent(2223): CustomLinearLayout:onInterceptTouchEvent
09-28 04:23:15.712: D/TouchEvent(2223): CustomButton:dispatchTouchEvent
09-28 04:23:15.722: D/TouchEvent(2223): MainActivity:onTouch
09-28 04:23:15.882: D/TouchEvent(2223): MainActivity:dispatchTouchEvent
09-28 04:23:15.882: D/TouchEvent(2223): CustomRelativieLayout:dispatchTouchEvent
09-28 04:23:15.882: D/TouchEvent(2223): CustomRelativieLayout:onInterceptTouchEvent
09-28 04:23:15.882: D/TouchEvent(2223): CustomLinearLayout:dispatchTouchEvent
09-28 04:23:15.882: D/TouchEvent(2223): CustomLinearLayout:onInterceptTouchEvent
09-28 04:23:15.882: D/TouchEvent(2223): CustomButton:dispatchTouchEvent
09-28 04:23:15.892: D/TouchEvent(2223): MainActivity:onTouch

总结6:

可以看到onTouch()设置为true,事件到最后也没传递到onClick()里面,而是由Button的onTouch()给消费了。所以onTouch()方法应该是先于onClick执行的,事件到了onTouch()(返回true)已经被消费了,那么整个方法都已经返回了,事件就不会进一步传递,自然没有onClcik的事。

到这里我们差不多可以结合源码来解析一下我们锁看到的现象了。

7.dispathTouchEvent()和onTouch()源码理解

找到View中dispathTouchEvent()的源码:

public boolean dispatchTouchEvent(MotionEvent event) {
        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }
            // We have focus and got the event, then use normal event dispatch.
            event.setTargetAccessibilityFocus(false);
        }
        boolean result = false;
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }
        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }
        if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            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;
            }
        }
        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }
        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
        // of the gesture.
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }
        return result;
    }

从上面的代码24行开始可以看出dispathToychEvent()若能返回true的话都是要在,第24行或是第38行的判断中返回true, dispathTouchEvent才会为true。在这里其实可以看到前面的第24行的判断正是onTouch()方法的响应。而我们onTouch()方法的返回值就是写在Acivity中的代码,所以当onTouch()返回true的时候,事件在这里就已经消费了,而 dispathToychEvent()也马上返回false。

而如果 onTouch()返回false则事件会去到View自己的OnTouchEvent()方法里,那么onClick方法是怎么才调用到的呢?接着我们再看源码。

找到View中dispathTouchEvent()的源码:

public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return (((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }
                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                       }
                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();
                            // 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();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }
                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }
                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }
                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;
                case MotionEvent.ACTION_DOWN:
                    mHasPerformedLongPress = false;
                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }
                    // Walk up the hierarchy to determine if we're inside a scrolling container.
                    boolean isInScrollingContainer = isInScrollingContainer();
                    // For views inside a scrolling container, delay the pressed feedback for
                    // a short period in case this is a scroll.
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true, x, y);
                        checkForLongClick(0);
                    }
                    break;
                case MotionEvent.ACTION_CANCEL:
                    setPressed(false);
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    break;
                case MotionEvent.ACTION_MOVE:
                    drawableHotspotChanged(x, y);
                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Outside button
                        removeTapCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            // Remove any future long press/tap checks
                            removeLongPressCallback();
                            setPressed(false);
                        }
                    }
                    break;
            }
            return true;
        }
        return false;
    }

代码很多,我们看到第24行,程序进过一些列的判断后会进入一个 switch()语句判断,而里面的case事件就是我们熟悉的ACTION_DOWN,ACTION_UP,ACTION_MOVE等事件的处理。

而我们的setOnClickListener在哪?Ctrl+F搜索一下,发现下面两段代码:

public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }

其中有一个变量mOnClickListener.于是再搜索:

public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        return result;
    }
到这里我们可以知道performClick()方法是在OnTouchEvent()里面的ACTION_UP调用的。所以Android的事件分发都是这样进过一层层的View,再通过每个View中的 dispatchTouchEvent()和onTouchEvent()里层层的判断,最终才会决定谁去消费这个事件。而这些层层的判断条件我已经写到图上去了,在这里不多做解析,想要继续深究的童鞋可以到源码中去找。

这次的浅谈就到此为止了,如果有问题的童靴欢迎一起探讨,互相学习,共同进步~

 
标签: 安卓开发
反对 0举报 0 评论 0
 

免责声明:本文仅代表作者个人观点,与乐学笔记(本网)无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
    本网站有部分内容均转载自其它媒体,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责,若因作品内容、知识产权、版权和其他问题,请及时提供相关证明等材料并与我们留言联系,本网站将在规定时间内给予删除等相关处理.

  • 安卓中通知功能的具体实现
    安卓中通知功能的具体实现
    通知[Notification]是Android中比较有特色的功能,当某个应用程序希望给用户发出一些提示信息,而该应用程序又不在前台运行时,就可以借助通知实现。使用通知的步骤1、需要一个NotificationManager来获得NotificationManager manager = (NotificationManager
    02-05 安卓开发
  • Android view系统分析-setContentView
    Android view系统分析-setContentView
    第一天上班,列了一下今年要学习的东西。主要就是深入学习Android相关的系统源代码,夯实基础。对于学习Android系统源代码,也没什么大概,就从我们平常使用最基础的东西学起,也就是从view这个切入点开始学习Android的源码,在没分析源码之前,我们有的时候
    02-05 安卓开发
  • 如何进行网络视频截图/获取视频的缩略图
    如何进行网络视频截图/获取视频的缩略图
    小编导读:获取视频的缩略图,截图正在播放的视频某一帧,是在音视频开发中,常遇到的问题。本文是主要用于点播中截图视频,同时还可以获取点播视频的缩略图进行显示,留下一个问题,如下图所示, 如果要获取直播中节目视频缩略图,该怎么做呢?(ps:直播是直
  • Android NDK 层发起 HTTP 请求的问题及解决
    Android NDK 层发起 HTTP 请求的问题及解决
    前言新的一年,大家新年快乐~~鸡年大吉!本次给大家带来何老师的最新文章~虽然何老师还在过节,但依然放心不下广大开发者,在此佳节还未结束之际,给大家带来最新的技术分享~ 事件的起因不说了,总之是需要实现一个 NDK 层的网络请求。为了多端适用,还是选择
  • Android插件化(六): OpenAtlasの改写aapt以防止资源ID冲突
    Android插件化(六): OpenAtlasの改写aapt以防
    引言Android应用程序的编译中,负责资源打包的是aapt,如果不对打包后的资源ID进行控制,就会导致插件中的资源ID冲突。所以,我们需要改写aapt的源码,以达到通过某种方式传递资源ID的Package ID,通过aapt打包时获取到这个Package ID并且应用才插件资源的命名
    02-05 安卓开发
  • Android架构(一)MVP架构在Android中的实践
    Android架构(一)MVP架构在Android中的实践
    为什么要重视程序的架构设计 对程序进行架构设计的原因,归根结底是为了 提高生产力 。通过设计是程序模块化,做到模块内部的 高聚合 和模块之间的 低耦合 (如依赖注入就是低耦合的集中体现)。 这样做的好处是使得程序开发过程中,开发人员主需要专注于一点,
    02-05 安卓开发
  • 安卓逆向系列教程 4.2 分析锁机软件
    安卓逆向系列教程 4.2 分析锁机软件
    安卓逆向系列教程 4.2 分析锁机软件 作者: 飞龙 这个教程中我们要分析一个锁机软件。像这种软件都比较简单,完全可以顺着入口看下去,但我这里还是用关键点来定位。首先这个软件的截图是这样,进入这个界面之后,除非退出模拟器,否则没办法回到桌面。上面那
    02-05 安卓开发
  • Android插件化(二):OpenAtlas插件安装过程分析
    Android插件化(二):OpenAtlas插件安装过程分析
    在前一篇博客 Android插件化(一):OpenAtlas架构以及实现原理概要 中,我们对应Android插件化存在的问题,实现原理,以及目前的实现方案进行了简单的叙述。从这篇开始,我们要深入到OpenAtlas的源码中进行插件安装过程的分析。 插件的安装分为3种:宿主启动时立
    02-05 安卓开发
  • [译] Android API 指南
    [译] Android API 指南
    众所周知,Android开发者有中文网站了,API 指南一眼看去最左侧的菜单都是中文,然而点进去内容还是很多是英文,并没有全部翻译,我这里整理了API 指南的目录,便于查看,如果之前还没有通读,现在可以好好看一遍。注意,如果标题带有英文,说明官方还没有翻
  • 使用FileProvider解决file:// URI引起的FileUriExposedException
    使用FileProvider解决file:// URI引起的FileUri
    问题以下是一段简单的代码,它调用系统的相机app来拍摄照片:void takePhoto(String cameraPhotoPath) {File cameraPhoto = new File(cameraPhotoPath);Intent takePhotoIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);takePhotoIntent.putExtra(Medi
    02-05 安卓开发
点击排行