이 글에서는 Android Touch 이벤트 배포 과정을 예시 형식으로 설명하고 있는데, 이는 Android 프로그래밍을 심층적으로 이해하고 익히는 데 매우 도움이 됩니다. 구체적인 분석은 다음과 같습니다.
먼저 간단한 예부터 시작하세요.
먼저 아래 예를 살펴보세요.
레이아웃 파일:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center" tools:context="com.example.touch_event.MainActivity" tools:ignore="MergeRootFrame" > <Button android:id="@+id/my_button" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hello_world" /> </FrameLayout>
MainActivity 파일:
public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button mBtn = (Button) findViewById(R.id.my_button); mBtn.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { Log.d("", "### onTouch : " + event.getAction()); return false; } }); mBtn.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Log.d("", "### onClick : " + v); } }); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { Log.d("", "### activity dispatchTouchEvent"); return super.dispatchTouchEvent(ev); } }
사용자가 버튼을 클릭하면 다음 로그가 출력됩니다.
08-31 03:03:56.116: D/(1560): ### activity dispatchTouchEvent 08-31 03:03:56.116: D/(1560): ### onTouch : 0 08-31 03:03:56.196: D/(1560): ### activity dispatchTouchEvent 08-31 03:03:56.196: D/(1560): ### onTouch : 1 08-31 03:03:56.196: D/(1560): ### onClick : android.widget.Button{52860d98 VFED..C. ...PH... 0,0-1080,144 #7f05003d app:id/my_button}
dispatchTouchEvent 메소드가 Activity가 먼저 실행된 후 onTouch 메서드가 실행되고, 다음으로 dispatchTouchEvent --> onTouch가 실행되고, 마지막으로 버튼의 클릭 이벤트가 실행됩니다. 여기서 질문이 있을 수 있습니다. 왜 dispatchTouchEvent와 onTouch는 두 번 실행되지만 onClick은 한 번만 실행됩니까? 두 터치 이벤트의 동작이 다른 이유는 무엇입니까? 동작 0과 동작 1은 무엇을 나타냅니까?
onTouchEvent를 재정의한 친구는 일반적으로 ACTION_DOWN, ACTION_MOVE, ACTION_UP 등을 포함하여 이 메소드 본문에서 중앙 집중식 터치 유형 이벤트를 처리한다는 것을 알고 있습니다. 그러나 위의 예에서는 움직임이 없습니다. 간단하게 누르고 들어 올리세요. 따라서 우리의 터치 이벤트는 누르기와 떼기뿐이므로 2개의 터치 이벤트가 있고 동작은 각각 0과 1입니다. MotionEvent의 몇 가지 변수 정의를 살펴보겠습니다.
public final class MotionEvent extends InputEvent implements Parcelable { // 代码省略 public static final int ACTION_DOWN = 0; // 按下事件 public static final int ACTION_UP = 1; // 抬起事件 public static final int ACTION_MOVE = 2; // 手势移动事件 public static final int ACTION_CANCEL = 3; // 取消 // 代码省略 }
눌려진 이벤트가 0이고 들어 올려진 이벤트가 1이라는 것을 알 수 있는데, 이는 위에서 말한 내용도 확인시켜 줍니다.
다른 두 장면을 보면:
1. 버튼 바깥쪽 영역을 클릭하면 출력 Log는 다음과 같습니다.
08-31 03:04:45.408: D/(1560): ### activity dispatchTouchEvent08-31 03:04:45.512: D/(1560): ### activity dispatchTouchEvent
2. onTouch 함수를 실행하고 출력하면 로그는 다음과 같습니다.
08-31 03:06:04.764: D/(1612): ### activity dispatchTouchEvent 08-31 03:06:04.764: D/(1612): ### onTouch : 0 08-31 03:06:04.868: D/(1612): ### activity dispatchTouchEvent 08-31 03:06:04.868: D/(1612): ### onTouch : 1
위 두 장면은 왜 이럴까요? 계속해서 읽어보자.
Android Touch 이벤트 배포
그럼 전체 이벤트 배포 프로세스는 어떻게 되나요?
간단히 말하면 사용자가 휴대폰 화면을 터치하면 터치 메시지가 생성됩니다. 마지막으로 이 터치 메시지가 ViewRoot의 InputHandler로 전송됩니다(4.2의 소스 코드를 보면 이 ViewRootImpl로 변경됨) ViewRoot는 GUI 관리 시스템으로 ViewRoot의 정의에 따르면 View 유형이 아닌 Handler임을 알 수 있습니다. InputHandler는 KeyEvent 및 TouchEvent 유형 이벤트를 처리하는 데 사용되는 인터페이스 유형입니다. 소스 코드를 살펴보겠습니다.
public final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Callbacks { // 代码省略 private final InputHandler mInputHandler = new InputHandler() { public void handleKey(KeyEvent event, Runnable finishedCallback) { startInputEvent(finishedCallback); dispatchKey(event, true); } public void handleMotion(MotionEvent event, Runnable finishedCallback) { startInputEvent(finishedCallback); dispatchMotion(event, true); // 1、handle 触摸消息 } }; // 代码省略 // 2、分发触摸消息 private void dispatchMotion(MotionEvent event, boolean sendDone) { int source = event.getSource(); if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) { dispatchPointer(event, sendDone); // 分发触摸消息 } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) { dispatchTrackball(event, sendDone); } else { // TODO Log.v(TAG, "Dropping unsupported motion event (unimplemented): " + event); if (sendDone) { finishInputEvent(); } } } // 3、通过Handler投递消息 private void dispatchPointer(MotionEvent event, boolean sendDone) { Message msg = obtainMessage(DISPATCH_POINTER); msg.obj = event; msg.arg1 = sendDone ? 1 : 0; sendMessageAtTime(msg, event.getEventTime()); } @Override public void handleMessage(Message msg) { // ViewRoot覆写handlerMessage来处理各种消息 switch (msg.what) { // 代码省略 case DO_TRAVERSAL: if (mProfile) { Debug.startMethodTracing("ViewRoot"); } performTraversals(); if (mProfile) { Debug.stopMethodTracing(); mProfile = false; } break; case DISPATCH_POINTER: { // 4、处理DISPATCH_POINTER类型的消息,即触摸屏幕的消息 MotionEvent event = (MotionEvent) msg.obj; try { deliverPointerEvent(event); // 5、处理触摸消息 } finally { event.recycle(); if (msg.arg1 != 0) { finishInputEvent(); } if (LOCAL_LOGV || WATCH_POINTER) Log.i(TAG, "Done dispatching!"); } } break; // 代码省略 } // 6、真正的处理事件 private void deliverPointerEvent(MotionEvent event) { if (mTranslator != null) { mTranslator.translateEventInScreenToAppWindow(event); } boolean handled; if (mView != null && mAdded) { // enter touch mode on the down boolean isDown = event.getAction() == MotionEvent.ACTION_DOWN; if (isDown) { ensureTouchMode(true); // 如果是ACTION_DOWN事件则进入触摸模式,否则为按键模式。 } if(Config.LOGV) { captureMotionLog("captureDispatchPointer", event); } if (mCurScrollY != 0) { event.offsetLocation(0, mCurScrollY); // 物理坐标向逻辑坐标的转换 } if (MEASURE_LATENCY) { lt.sample("A Dispatching TouchEvents", System.nanoTime() - event.getEventTimeNano()); } // 7、分发事件,如果是窗口类型,则这里的mView对应的就是PhonwWindow中的DecorView,否则为根视图的ViewGroup。 handled = mView.dispatchTouchEvent(event); // 代码省略 } } // 代码省略 }
코드 7의 mView가 DecorView인지 루트 뷰인지에 관계없이 안개 레이어 이후입니다. 비창문 인터페이스, 본질은 ViewGroup입니다. 즉, 터치 이벤트는 결국 루트 뷰 ViewGroup에 의해 배포됩니다! ! !
이 프로세스를 분석하기 위해 Activity를 예로 들어 보겠습니다. 표시된 Activity에는 이 창의 구현 클래스가 PhoneWindow라는 것을 알 수 있습니다. 휴대폰에서 볼 수 있듯이 이 DecorView는 Activity의 dispatchTouchEvent가 실제로 PhoneWindow의 dispatchTouchEvent를 호출하는 하위 클래스입니다. 소스 코드를 살펴보고 Activity의 dispatchTouchEvent 함수(
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { // 1、调用的是PhoneWindow的superDispatchTouchEvent(ev) return true; } return onTouchEvent(ev); } public void onUserInteraction() { }
)를 입력하여 이벤트가 발생하는지 살펴보겠습니다. 프레스 이벤트인 경우 onUserInteraction() 함수가 시작됩니다. 이 함수는 비어 있으므로 지금은 무시하겠습니다. 계속해서 읽으면 터치 이벤트 배포가 getWindow().superDispatchTouchEvent(ev) 함수를 호출하는 것을 알 수 있습니다. getWindow()에서 얻은 인스턴스 유형은 Activity 클래스에서 다음 메서드를 사용하여 getWindow가 무엇인지 확인할 수 있습니다. ()를 얻습니다. 유형:
Log.d("", "### Activiti中getWindow()获取的类型是 : " + this.getWindow()) ;
출력:
08-31 03:40:17.036: D/(1688): ### Activiti中getWindow()获取的类型是 : com.android.internal.policy.impl.PhoneWindow@5287fe38
좋습니다. 더 이상 고민하지 말고 PhoneWindow의 superDispatchTouchEvent 함수를 계속 살펴보겠습니다.
@Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); }
음, mDecor의 superDispatchTouchEvent(event) 함수가 호출됩니다. 이 mDecor는 위에서 언급한 DecorView 유형으로, 우리가 보는 활동의 모든 콘텐츠에 대한 최상위 ViewGroup입니다. 전체 ViewTree 루트 노드. 그 진술을 살펴보십시오.
// This is the top-level view of the window, containing the window decor. private DecorView mDecor;
DecorView
그럼 DecorView가 무엇인지 계속해서 살펴보겠습니다.
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker { /* package */int mDefaultOpacity = PixelFormat.OPAQUE; /** The feature ID of the panel, or -1 if this is the application's DecorView */ private final int mFeatureId; private final Rect mDrawingBounds = new Rect(); private final Rect mBackgroundPadding = new Rect(); private final Rect mFramePadding = new Rect(); private final Rect mFrameOffsets = new Rect(); private boolean mChanging; private Drawable mMenuBackground; private boolean mWatchingForMenu; private int mDownY; public DecorView(Context context, int featureId) { super(context); mFeatureId = featureId; } @Override public boolean dispatchKeyEvent(KeyEvent event) { final int keyCode = event.getKeyCode(); // 代码省略 return isDown ? PhoneWindow.this.onKeyDown(mFeatureId, event.getKeyCode(), event) : PhoneWindow.this.onKeyUp(mFeatureId, event.getKeyCode(), event); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { final Callback cb = getCallback(); return cb != null && mFeatureId < 0 ? cb.dispatchTouchEvent(ev) : super .dispatchTouchEvent(ev); } @Override public boolean dispatchTrackballEvent(MotionEvent ev) { final Callback cb = getCallback(); return cb != null && mFeatureId < 0 ? cb.dispatchTrackballEvent(ev) : super .dispatchTrackballEvent(ev); } public boolean superDispatchKeyEvent(KeyEvent event) { return super.dispatchKeyEvent(event); } public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); } public boolean superDispatchTrackballEvent(MotionEvent event) { return super.dispatchTrackballEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { return onInterceptTouchEvent(event); } // 代码省略 }
보시다시피 DecorView는 FrameLayout에서 상속됩니다. 터치 이벤트(dispatchTouchEvent)의 배포 및 처리는 FrameLayout에서 처리되는 슈퍼 클래스로 전달됩니다. 구현 후 FrameLayout의 상위 클래스인 ViewGroup을 계속 추적합니다. DispatchTouchEvent의 구현을 살펴보았습니다. 그런 다음 먼저 ViewGroup(Android 2.3 소스 코드)이 이벤트를 배포하는 방법을 살펴보겠습니다.
ViewGroup의 터치 이벤트 배포
rreee
这个函数代码比较长,我们只看上文中标注的几个关键点。首先在代码1处可以看到一个条件判断,如果disallowIntercept和!onInterceptTouchEvent(ev)两者有一个为true,就会进入到这个条件判断中。disallowIntercept是指是否禁用掉事件拦截的功能,默认是false,也可以通过调用requestDisallowInterceptTouchEvent方法对这个值进行修改。那么当第一个值为false的时候就会完全依赖第二个值来决定是否可以进入到条件判断的内部,第二个值是什么呢?onInterceptTouchEvent就是ViewGroup对事件进行拦截的一个函数,返回该函数返回false则表示不拦截事件,反之则表示拦截。第二个条件是是对onInterceptTouchEvent方法的返回值取反,也就是说如果我们在onInterceptTouchEvent方法中返回false,就会让第二个值为true,从而进入到条件判断的内部,如果我们在onInterceptTouchEvent方法中返回true,就会让第二个值的整体变为false,从而跳出了这个条件判断。例如我们需要实现ListView滑动删除某一项的功能,那么可以通过在onInterceptTouchEvent返回true,并且在onTouchEvent中实现相关的判断逻辑,从而实现该功能。
进入代码1内部的if后,有一个for循环,遍历了当前ViewGroup下的所有子child view,如果触摸该事件的坐标在某个child view的坐标范围内,那么该child view来处理这个触摸事件,即调用该child view的dispatchTouchEvent。如果该child view是ViewGroup类型,那么继续执行上面的判断,并且遍历子view;如果该child view不是ViewGroup类型,那么直接调用的是View中的dispatchTouchEvent方法,除非这个child view的类型覆写了该方法。我们看看View中的dispatchTouchEvent函数:
View的Touch事件分发
/** * 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 (!onFilterTouchEventForSecurity(event)) { return false; } if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) { return true; } return onTouchEvent(event); }
该函数中,首先判断该事件是否符合安全策略,然后判断该view是否是enable的 ,以及是否设置了Touch Listener,mOnTouchListener即我们通过setOnTouchListener设置的。
/** * Register a callback to be invoked when a touch event is sent to this view. * @param l the touch listener to attach to this view */ public void setOnTouchListener(OnTouchListener l) { mOnTouchListener = l; }
如果mOnTouchListener.onTouch(this, event)返回false则继续执行onTouchEvent(event);如果mOnTouchListener.onTouch(this, event)返回true,则表示该事件被消费了,不再传递,因此也不会执行onTouchEvent(event)。这也验证了我们上文中留下的场景2,当onTouch函数返回true时,点击按钮,但我们的点击事件没有执行。那么我们还是先来看看onTouchEvent(event)函数到底做了什么吧。
/** * Implement this method to handle touch screen motion events. * * @param event The motion event. * @return True if the event was handled, false otherwise. */ public boolean onTouchEvent(MotionEvent event) { final int viewFlags = mViewFlags; if ((viewFlags & ENABLED_MASK) == DISABLED) // 1、判断该view是否enable // 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)); } if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) // 2、是否是clickable或者long clickable switch (event.getAction()) { case MotionEvent.ACTION_UP: // 抬起事件 boolean prepressed = (mPrivateFlags & PREPRESSED) != 0; if ((mPrivateFlags & 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 (!mHasPerformedLongPress) { // 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)) // post performClick(); // 3、点击事件处理 } } } if (mUnsetPressedState == null) { mUnsetPressedState = new UnsetPressedState(); } if (prepressed) { mPrivateFlags |= PRESSED; refreshDrawableState(); postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); } else if (!post(mUnsetPressedState)) { // If the post failed, unpress right now mUnsetPressedState.run(); } removeTapCallback(); } break; case MotionEvent.ACTION_DOWN: if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPrivateFlags |= PREPRESSED; mHasPerformedLongPress = false; postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); break; case MotionEvent.ACTION_CANCEL: mPrivateFlags &= ~PRESSED; refreshDrawableState(); removeTapCallback(); break; case MotionEvent.ACTION_MOVE: final int x = (int) event.getX(); final int y = (int) event.getY(); // Be lenient about moving outside of buttons int slop = mTouchSlop; if ((x < 0 - slop) || (x >= getWidth() + slop) || (y < 0 - slop) || (y >= getHeight() + slop)) { // Outside button removeTapCallback(); if ((mPrivateFlags & PRESSED) != 0) { // Remove any future long press/tap checks removeLongPressCallback(); // Need to switch from pressed to not pressed mPrivateFlags &= ~PRESSED; refreshDrawableState(); } } break; } return true; } return false; }
我们看到,在onTouchEvent函数中就是对ACTION_UP、ACTION_DOWN、ACTION_MOVE等几个事件进行处理,而最重要的就是UP事件了,因为这个里面包含了对用户点击事件的处理,或者是说对于用户而言相对重要一点,因此放在了第一个case中。在ACTION_UP事件中会判断该view是否enable、是否clickable、是否获取到了焦点,然后我们看到会通过post方法将一个PerformClick对象投递给UI线程,如果投递失败则直接调用performClick函数执行点击事件。
/** * Causes the Runnable to be added to the message queue. * The runnable will be run on the user interface thread. * * @param action The Runnable that will be executed. * * @return Returns true if the Runnable was successfully placed in to the * message queue. Returns false on failure, usually because the * looper processing the message queue is exiting. */ public boolean post(Runnable action) { Handler handler; if (mAttachInfo != null) { handler = mAttachInfo.mHandler; } else { // Assume that post will succeed later ViewRoot.getRunQueue().post(action); return true; } return handler.post(action); }
我们看看PerformClick类吧。
private final class PerformClick implements Runnable { public void run() { performClick(); } }
可以看到,其内部就是包装了View类中的performClick()方法。再看performClick()方法:
/** * Register a callback to be invoked when this view is clicked. If this view is not * clickable, it becomes clickable. * * @param l The callback that will run * * @see #setClickable(boolean) */ public void setOnClickListener(OnClickListener l) { if (!isClickable()) { setClickable(true); } mOnClickListener = l; } /** * Call this view's OnClickListener, if it is defined. * * @return True there was an assigned OnClickListener that was called, false * otherwise is returned. */ public boolean performClick() { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); if (mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); mOnClickListener.onClick(this); return true; } return false; }
代码很简单,主要就是调用了mOnClickListener.onClick(this);方法,即执行用户通过setOnClickListener设置进来的点击事件处理Listener。
总结
用户触摸屏幕产生一个触摸消息,系统底层将该消息转发给ViewRoot ( ViewRootImpl ),ViewRoot产生一个DISPATCHE_POINTER的消息,并且在handleMessage中处理该消息,最终会通过deliverPointerEvent(MotionEvent event)来处理该消息。在该函数中会调用mView.dispatchTouchEvent(event)来分发消息,该mView是一个ViewGroup类型,因此是ViewGroup的dispatchTouchEvent(event),在该函数中会遍历所有的child view,找到该事件的触发的左边与每个child view的坐标进行对比,如果触摸的坐标在该child view的范围内,则由该child view进行处理。如果该child view是ViewGroup类型,则继续上一步的查找过程;否则执行View中的dispatchTouchEvent(event)函数。在View的dispatchTouchEvent(event)中首先判断该控件是否enale以及mOnTouchListent是否为空,如果mOnTouchListener不为空则执行mOnTouchListener.onTouch(event)方法,如果该方法返回false则再执行View中的onTouchEvent(event)方法,并且在该方法中执行mOnClickListener.onClick(this, event) ;方法; 如果mOnTouchListener.onTouch(event)返回true则不会执行onTouchEvent方法,因此点击事件也不会被执行。
相信本文所述对大家进一步深入掌握Android程序设计有一定的借鉴价值。
更多Android Touch事件分发过程详解相关文章请关注PHP中文网!