zoukankan      html  css  js  c++  java
  • 第三章 View的事件体系

    View的坐标体系:
        

    MotionEvent:
    1.     -ACTION_DOWN 手指刚接触屏幕
    2.     -ACTION_MOVE 手指在屏幕上移动
    3.     -ACTION_UP 手指从屏幕上松开的一瞬
    1. getX/getY 获取的是当前View左上角的xy坐标
    2. getRawX/Y是现对于手机屏幕的

    TouchSlop:
        一个常量,系统所能识别出的被认为是滑动的最小距离,每个设备都不同
        
    1. ViewConfiguration.get(getContext()).getScaledTouchSlop() 可以获取

    VelocityTracker:
        速度追踪
    1. //初始化
    2. VelocityTracker mVelocityTracker = VelocityTracker.obtain();
    3. //在onTouchEvent方法中
    4. mVelocityTracker.addMovement(event);
    5. //获取速度
    6. mVelocityTracker.computeCurrentVelocity(1000);
    7. float xVelocity = mVelocityTracker.getXVelocity();
    8. //重置和回收
    9. mVelocityTracker.clear(); //一般在MotionEvent.ACTION_UP的时候调用
    10. mVelocityTracker.recycle(); //一般在onDetachedFromWindow中调用
    原理:
       

                速度的计算公式:

               速度 = (终点位置 - 起点位置) / 时间段

              速度可能为负值,例如当手指从屏幕右边往左边滑动的时候。此外,速度是单位时间内移动的像素数,单位时间不一定是1秒钟,可以使用方法               computeCurrentVelocity(xxx)指定单位时间是多少,单位是ms。例如通过computeCurrentVelocity(1000)来获取速度,手指在1s中滑动了100           个像素,那么速度是100,即100(像素/1000ms)。如果computeCurrentVelocity(100)来获取速度,在100ms内手指只是滑动了10个像素,那么           速度是10,即10(像素/100ms)。


     

    GestureDetector:
        手势检测

    使用方法如下
    1. package com.example.testgesture;
    2. import android.app.Activity;
    3. import android.os.Bundle;
    4. import android.util.Log;
    5. import android.view.GestureDetector;
    6. import android.view.GestureDetector.SimpleOnGestureListener;
    7. import android.view.MotionEvent;
    8. import android.view.View;
    9. import android.view.View.OnTouchListener;
    10. import android.widget.Button;
    11. public class MainActivity extends Activity {
    12. private Button mButton;
    13. private GestureDetector mGestureDetector;
    14. @Override
    15. protected void onCreate(Bundle savedInstanceState) {
    16. super.onCreate(savedInstanceState);
    17. setContentView(R.layout.activity_main);
    18. mGestureDetector = new GestureDetector(this, new MyOnGestureListener());
    19. mButton = (Button) findViewById(R.id.btn_textgesture);
    20. mButton.setOnTouchListener(new OnTouchListener() {
    21. @Override
    22. public boolean onTouch(View v, MotionEvent event) {
    23. Log.i(getClass().getName(), "onTouch-----" + getActionName(event.getAction()));
    24. mGestureDetector.onTouchEvent(event);
    25. // 一定要返回true,不然获取不到完整的事件
    26. return true;
    27. }
    28. });
    29. }
    30. private String getActionName(int action) {
    31. String name = "";
    32. switch (action) {
    33. case MotionEvent.ACTION_DOWN: {
    34. name = "ACTION_DOWN";
    35. break;
    36. }
    37. case MotionEvent.ACTION_MOVE: {
    38. name = "ACTION_MOVE";
    39. break;
    40. }
    41. case MotionEvent.ACTION_UP: {
    42. name = "ACTION_UP";
    43. break;
    44. }
    45. default:
    46. break;
    47. }
    48. return name;
    49. }
    50. class MyOnGestureListener extends SimpleOnGestureListener {
    51. @Override
    52. public boolean onSingleTapUp(MotionEvent e) {
    53. Log.i(getClass().getName(), "onSingleTapUp-----" + getActionName(e.getAction()));
    54. return false;
    55. }
    56. @Override
    57. public void onLongPress(MotionEvent e) {
    58. Log.i(getClass().getName(), "onLongPress-----" + getActionName(e.getAction()));
    59. }
    60. @Override
    61. public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
    62. Log.i(getClass().getName(),
    63. "onScroll-----" + getActionName(e2.getAction()) + ",(" + e1.getX() + "," + e1.getY() + ") ,("
    64. + e2.getX() + "," + e2.getY() + ")");
    65. return false;
    66. }
    67. @Override
    68. public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
    69. Log.i(getClass().getName(),
    70. "onFling-----" + getActionName(e2.getAction()) + ",(" + e1.getX() + "," + e1.getY() + ") ,("
    71. + e2.getX() + "," + e2.getY() + ")");
    72. return false;
    73. }
    74. @Override
    75. public void onShowPress(MotionEvent e) {
    76. Log.i(getClass().getName(), "onShowPress-----" + getActionName(e.getAction()));
    77. }
    78. @Override
    79. public boolean onDown(MotionEvent e) {
    80. Log.i(getClass().getName(), "onDown-----" + getActionName(e.getAction()));
    81. return false;
    82. }
    83. @Override
    84. public boolean onDoubleTap(MotionEvent e) {
    85. Log.i(getClass().getName(), "onDoubleTap-----" + getActionName(e.getAction()));
    86. return false;
    87. }
    88. @Override
    89. public boolean onDoubleTapEvent(MotionEvent e) {
    90. Log.i(getClass().getName(), "onDoubleTapEvent-----" + getActionName(e.getAction()));
    91. return false;
    92. }
    93. @Override
    94. public boolean onSingleTapConfirmed(MotionEvent e) {
    95. Log.i(getClass().getName(), "onSingleTapConfirmed-----" + getActionName(e.getAction()));
    96. return false;
    97. }
    98. }
    99. }
    1. 首先是声明一个GestureDetector,然后重写ButtononTouch函数,将触屏事件交给GestureDetector处理。

        

    Scroller:
        待补..



    View的滑动。
        绝大部分是这三种方法:
            1、使用scrollTo/scrollBy ,只能滑动view的内容,不能滑动view
            2、使用动画,但是只能改变视图位置,实际的逻辑位置未改变。
            3、使用LayoutParams,无敌,但是比较麻烦




    1、使用scrollTo/scrollBy 

    其中scrollBy其实调用的是scrollTo
    1. public void scrollBy(int x,int y){
    2. scrollTo(mScrollx+x,mScrolly + y);
    3. }
    其中mScrollx/y的取值如下图所示。


     
        

    2、使用动画
        略,放在动画章节再细写

    3、LayoutParams
       略



    弹性滑动:
        弹性滑动也就是渐进式滑动,实现弹性滑动的方法有很多,但是他们都有一个共同的思想:将一次大的滑动分成若干次小的滑动并在一段时间内完成。本文主要介绍三种弹性滑动方式,Scroller、动画和Handler。
        
        Scroller:
            只能滑动View的内容
            使用范例:
    1. private void smoothScrollTo(int destX, int destY) {
    2. int scrollX = getScrollX();
    3. int detlaX = destX - scrollX;
    4. int scrollY = getScrollY();
    5. int detlaY = destY - scrollY;
    6. Log.d(TAG, "smoothScrollTo:scrollY, detlaY= " + scrollY+" " + detlaY);
    7. mScroller.startScroll(scrollX, scrollY, detlaX, detlaY, 1000);
    8. invalidate();
    9. }
    10. @Override
    11. public void computeScroll() {
    12. if (mScroller.computeScrollOffset()) {
    13. scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
    14. postInvalidate();
    15. }
    16. }

    1. 实现原理为:Scroller本身无法让View弹性滑动,它需要和ViewcomputeScroll方法配合使用才能共同完成这个功能。在startScroll()方法下调用了invalidate(),这使得View重绘,View重绘的时候会在draw方法中调用computeScroll(),在此方法中调用scrollTo滑向指定位置。之后再通过postInvalidate()进行二次重绘,如此重复直到滑动结束。
    下面是computeScrollOffset()这个函数的源码。从中可以看出判断滑动是否结束是以timePassed为标准的。

    1. /**
    2. * Call this when you want to know the new location. If it returns true,
    3. * the animation is not yet finished.
    4. */
    5. public boolean computeScrollOffset() {
    6. if (mFinished) {
    7. return false;
    8. }
    9. int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
    10. if (timePassed < mDuration) {
    11. switch (mMode) {
    12. case SCROLL_MODE:
    13. final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
    14. mCurrX = mStartX + Math.round(x * mDeltaX);
    15. mCurrY = mStartY + Math.round(x * mDeltaY);
    16. break;
    17. case FLING_MODE:
    18. ...
    19. ...
    20. break;
    21. }
    22. }
    23. else {
    24. mCurrX = mFinalX;
    25. mCurrY = mFinalY;
    26. mFinished = true;
    27. }
    28. return true;
    29. }
    也就是根据消耗的时间的百分比,确定改滚动到何处,直到滚动结束。

    使用动画:
        
    1. private static final int FRAME_COUNT = 30;
    2. final int startX = 0;
    3. final int deltaX = 100;
    4. ValueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(1000);
    5. animator.addUpdateListener(new AnimatorUpdateListener() {
    6. @Override
    7. public void onAnimationUpdate(ValueAnimator animator) {
    8. float fraction = animator.getAnimatedFraction();
    9. mButton1.scrollTo(startX + (int) (deltaX * fraction), 0);
    10. }
    11. });
    12. animator.start();
    通过一个百分比配合scrollTo方法来完成View的滑动。

    Hanlder:
        使用PostDelay不断的发送消息进行滑动。


    View的事件分发机制:
             在Android中,事件主要包括点按、长按、拖拽、滑动等,点按又包括单击和双击,另外还包括单指操作和多指操作。所有这些都构成了Android中的事件响应。总的来说,所有的事件都由如下三个部分作为基础:
    • 按下(ACTION_DOWN)
    • 移动(ACTION_MOVE)
    • 抬起(ACTION_UP)

    所有的操作事件首先必须执行的是按下操作(ACTIONDOWN),之后所有的操作都是以按下操作作为前提,当按下操作完成后,接下来可能是一段移动(ACTIONMOVE)然后抬起(ACTION_UP),或者是按下操作执行完成后没有移动就直接抬起。这一系列的动作在Android中都可以进行控制。

    我们知道,所有的事件操作都发生在触摸屏上,而在屏幕上与我们交互的就是各种各样的视图组件(View),在Android中,所有的视图都继承于 View,另外通过各种布局组件(ViewGroup)来对View进行布局,ViewGroup也继承于View。所有的UI控件例如Button、 TextView都是继承于View,而所有的布局控件例如RelativeLayout、容器控件例如ListView都是继承于ViewGroup。 所以,我们的事件操作主要就是发生在View和ViewGroup之间,那么View和ViewGroup中主要有哪些方法来对这些事件进行响应呢?记住 如下3个方法,我们通过查看View和ViewGroup的源码可以看到:

           View.java
        
    1. public boolean dispatchTouchEvent(MotionEvent event)
    2. public boolean onTouchEvent(MotionEvent event)
           ViewGroup.java
    1. public boolean dispatchTouchEvent(MotionEvent event)
    2. public boolean onTouchEvent(MotionEvent event)
    3. public boolean onInterceptTouchEvent(MotionEvent event)
        在View和ViewGroup中都存在dispatchTouchEvent和onTouchEvent方法,但是在ViewGroup中还有一 个onInterceptTouchEvent方法,那这些方法都是干嘛的呢?别急,我们先看看他们的返回值。这些方法的返回值全部都是boolean型, 为什么是boolean型呢,看看本文的标题,“事件传递”,传递的过程就是一个接一个,那到了某一个点后是否要继续往下传递呢?你发现了吗,“是否”二 字就决定了这些方法应该用boolean来作为返回值。没错,这些方法都返回true或者是false。在Android中,所有的事件都是从开始经过传 递到完成事件的消费,这些方法的返回值就决定了某一事件是否是继续往下传,还是被拦截了,或是被消费了。

    接下来就是这些方法的参数,都接受了一个MotionEvent类型的参数,MotionEvent继承于InputEvent,用于标记各种动作事件。之前提到的ACTIONDOWN、ACTIONMOVE、ACTION_UP都是MotinEvent中定义的常量。我们通过MotionEvent传进来的事件类型来判断接收的是哪一种类型的事件。到现在,这三个方法的返回值和参数你应该都明白了,接下来就解释一下这三个方法分别在什么时候处理事件。

    • dispatchTouchEvent方法用于事件的分发,Android中所有的事件都必须经过这个方法的 分发,然后决定是自身消费当前事件还是继续往下分发给子控件处理。返回true表示不继续分发,事件被消费。返回false则继续往下分发,如果是 ViewGroup则分发给onInterceptTouchEvent进行判断是否拦截该事件。
    • onTouchEvent方法用于事件的处理,返回true表示消费处理当前事件,返回false则不处理,交给子控件进行继续分发。
    • onInterceptTouchEvent是ViewGroup中才有的方法,View中没有,它的作用是 负责事件的拦截,返回true的时候表示拦截当前事件,不继续往下分发,交给自身的onTouchEvent进行处理。返回false则不拦截,继续往下 传。这是ViewGroup特有的方法,因为ViewGroup中可能还有子View,而在Android中View中是不能再包含子View的(iOS 可以)。

    到目前为止,Android中事件的构成以及事件处理方法的作用你应该比较清楚了,接下来我们就通过一个Demo来实际体验实验一下。

    1. 新建一个工程,并新建一个类RTButton继承Button,用来实现我们对按钮事件的跟踪。
    1. public class RTButton extends Button {
    2. public RTButton(Context context, AttributeSet attrs) {
    3. super(context, attrs);
    4. }
    5. @Override
    6. public boolean dispatchTouchEvent(MotionEvent event) {
    7. switch (event.getAction()) {
    8. case MotionEvent.ACTION_DOWN:
    9. System.out.println("RTButton---dispatchTouchEvent---DOWN");
    10. break;
    11. case MotionEvent.ACTION_MOVE:
    12. System.out.println("RTButton---dispatchTouchEvent---MOVE");
    13. break;
    14. case MotionEvent.ACTION_UP:
    15. System.out.println("RTButton---dispatchTouchEvent---UP");
    16. break;
    17. default:
    18. break;
    19. }
    20. return super.dispatchTouchEvent(event);
    21. }
    22. @Override
    23. public boolean onTouchEvent(MotionEvent event) {
    24. switch (event.getAction()) {
    25. case MotionEvent.ACTION_DOWN:
    26. System.out.println("RTButton---onTouchEvent---DOWN");
    27. break;
    28. case MotionEvent.ACTION_MOVE:
    29. System.out.println("RTButton---onTouchEvent---MOVE");
    30. break;
    31. case MotionEvent.ACTION_UP:
    32. System.out.println("RTButton---onTouchEvent---UP");
    33. break;
    34. default:
    35. break;
    36. }
    37. return super.onTouchEvent(event);
    38. }
    39. }
    1. RTButton中我重写了dispatchTouchEventonTouchEvent方法,并获取了MotionEvent各个事件状 态,打印输出了每一个状态下的信息。然后在activity_main.xml中直接在根布局下放入自定义的按钮RTButton
    1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    2. xmlns:tools="http://schemas.android.com/tools"
    3. android:id="@+id/myLayout"
    4. android:layout_width="match_parent"
    5. android:layout_height="match_parent" >
    6. <com.ryantang.eventdispatchdemo.RTButton
    7. android:id="@+id/btn"
    8. android:layout_width="match_parent"
    9. android:layout_height="wrap_content"
    10. android:text="Button"/>
    11. </LinearLayout>
    1. 接下来在Activity中为RTButton设置onTouchonClick的监听器来跟踪事件传递的过程,另外,Activity中也有一个dispatchTouchEvent方法和一个onTouchEvent方法,我们也重写他们并输出打印信息。
    1. public class MainActivity extends Activity {
    2. private RTButton button;
    3. @Override
    4. protected void onCreate(Bundle savedInstanceState) {
    5. super.onCreate(savedInstanceState);
    6. setContentView(R.layout.activity_main);
    7. button = (RTButton)this.findViewById(R.id.btn);
    8. button.setOnTouchListener(new OnTouchListener() {
    9. @Override
    10. public boolean onTouch(View v, MotionEvent event) {
    11. switch (event.getAction()) {
    12. case MotionEvent.ACTION_DOWN:
    13. System.out.println("RTButton---onTouch---DOWN");
    14. break;
    15. case MotionEvent.ACTION_MOVE:
    16. System.out.println("RTButton---onTouch---MOVE");
    17. break;
    18. case MotionEvent.ACTION_UP:
    19. System.out.println("RTButton---onTouch---UP");
    20. break;
    21. default:
    22. break;
    23. }
    24. return false;
    25. }
    26. });
    27. button.setOnClickListener(new OnClickListener() {
    28. @Override
    29. public void onClick(View v) {
    30. System.out.println("RTButton clicked!");
    31. }
    32. });
    33. }
    34. @Override
    35. public boolean dispatchTouchEvent(MotionEvent event) {
    36. switch (event.getAction()) {
    37. case MotionEvent.ACTION_DOWN:
    38. System.out.println("Activity---dispatchTouchEvent---DOWN");
    39. break;
    40. case MotionEvent.ACTION_MOVE:
    41. System.out.println("Activity---dispatchTouchEvent---MOVE");
    42. break;
    43. case MotionEvent.ACTION_UP:
    44. System.out.println("Activity---dispatchTouchEvent---UP");
    45. break;
    46. default:
    47. break;
    48. }
    49. return super.dispatchTouchEvent(event);
    50. }
    51. @Override
    52. public boolean onTouchEvent(MotionEvent event) {
    53. switch (event.getAction()) {
    54. case MotionEvent.ACTION_DOWN:
    55. System.out.println("Activity---onTouchEvent---DOWN");
    56. break;
    57. case MotionEvent.ACTION_MOVE:
    58. System.out.println("Activity---onTouchEvent---MOVE");
    59. break;
    60. case MotionEvent.ACTION_UP:
    61. System.out.println("Activity---onTouchEvent---UP");
    62. break;
    63. default:
    64. break;
    65. }
    66. return super.onTouchEvent(event);
    67. }
    68. }
    1. 代码部分已经完成了,接下来运行工程,并点击按钮,查看日志输出信息,我们可以看到如下结果:
          
          
    1. 通过日志输出可以看到,首先执行了ActivitydispatchTouchEvent方法进行事件分发,在MainActivity.java代码第55行,dispatchTouchEvent方法的返回值是super.dispatchTouchEvent(event),因此调用了父类方法,我们进入Activity.java的源码中看看具体实现。
    1. /**
    2. * Called to process touch screen events. You can override this to
    3. * intercept all touch screen events before they are dispatched to the
    4. * window. Be sure to call this implementation for touch screen events
    5. * that should be handled normally.
    6. *
    7. * @param ev The touch screen event.
    8. *
    9. * @return boolean Return true if this event was consumed.
    10. */
    11. public boolean dispatchTouchEvent(MotionEvent ev) {
    12. if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    13. onUserInteraction();
    14. }
    15. if (getWindow().superDispatchTouchEvent(ev)) {
    16. return true;
    17. }
    18. return onTouchEvent(ev);
    19. }
    1. 从源码中可以看到,dispatchTouchEvent方法只处理了ACTIONDOWN事件,前面提到过,所有的事件都是以按下为起点的,所以,Android认为当ACTIONDOWN 事件没有执行时,后面的事件都是没有意义的,所以这里首先判断ACTION_DOWN事件。如果事件成立,则调用了onUserInteraction 法,该方法可以在Activity中被重写,在事件被分发前会调用该方法。该方法的返回值是void型,不会对事件传递结果造成影响,接着会判断 getWindow().superDispatchTouchEvent(ev)的执行结果,看看它的源码:
    1. /**
    2. * Used by custom windows, such as Dialog, to pass the touch screen event
    3. * further down the view hierarchy. Application developers should
    4. * not need to implement or call this.
    5. *
    6. */
    7. public abstract boolean superDispatchTouchEvent(MotionEvent event);
    1. 通过源码注释我们可以了解到这是个抽象方法,用于自定义的Window,例如自定义Dialog传递触屏事件,并且提到开发者不需要去实现或调用该 方法,系统会完成,如果我们在MainActivity中将dispatchTouchEvent方法的返回值设为true,那么这里的执行结果就为 true,从而不会返回执行onTouchEvent(ev),如果这里返回false,那么最终会返回执行onTouchEvent方法,由此可知,接 下来要调用的就是onTouchEvent方法了。别急,通过日志输出信息可以看到,ACTION_DOWN事件从Activity被分发到了 RTButton,接着执行了onTouchonTouchEvent方法,为什么先执行onTouch方法呢?我们到RTButton中的 dispatchTouchEvent看看View中的源码是如何处理的。
    1. /**
    2. * Pass the touch screen motion event down to the target view, or this
    3. * view if it is the target.
    4. *
    5. * @param event The motion event to be dispatched.
    6. * @return True if the event was handled by the view, false otherwise.
    7. */
    8. public boolean dispatchTouchEvent(MotionEvent event) {
    9. if (mInputEventConsistencyVerifier != null) {
    10. mInputEventConsistencyVerifier.onTouchEvent(event, 0);
    11. }
    12. if (onFilterTouchEventForSecurity(event)) {
    13. //noinspection SimplifiableIfStatement
    14. ListenerInfo li = mListenerInfo;
    15. if (li != null && li.mOnTouchListener != null && (mViewFlags &
    16. ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) {
    17. return true;
    18. }
    19. if (onTouchEvent(event)) {
    20. return true;
    21. }
    22. }
    23. if (mInputEventConsistencyVerifier != null) {
    24. mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    25. }
    26. return false;
    27. }
    1. 挑选关键代码进行分析,可以看代码第16行,这里有几个条件,当几个条件都满足时该方法就返回true,当条件li.mOnTouchListener不为空时,通过在源码中查找,发现mOnTouchListener是在以下方法中进行设置的。
    1. /**
    2. * Register a callback to be invoked when a touch event is sent to this view.
    3. * @param l the touch listener to attach to this view
    4. */
    5. public void setOnTouchListener(OnTouchListener l) {
    6. getListenerInfo().mOnTouchListener = l;
    7. }
    1. 这个方法就已经很熟悉了,就是我们在MainActivity.java中为RTButton设置的 onTouchListener,条件(mViewFlags & ENABLED_MASK) == ENABLED判断的是当前View是否是ENABLE的,默认都是ENABLE状态的。接着就是 li.mOnTouchListener.onTouch(this, event)条件,这里调用了onTouch方法,该方法的调用就是我们在MainActivity.java中为RTButton设置的监听回调,如果该方法返回true,则整个条件都满足,dispatchTouchEvent就返回true,表示该事件就不继续向下分发了,因为已经被onTouch消费了。
    2. 如果onTouch返回的是false,则这个判断条件不成立,接着执行onTouchEvent(event)方法进行判断,如果该方法返回 true,表示事件被onTouchEvent处理了,则整个dispatchTouchEvent就返回true。到这里,我们就可以回答之前提出的 “为什么先执行onTouch方法”的问题了。到目前为止,ACTIONDOWN的事件经过了从Activity到RTButton的分发,然后经过onTouch和onTouchEvent的处理,最终,ACTIONDOWN事件交给了RTButton得onTouchEvent进行处理。
    1. 当我们的手(我这里用的Genymotion然后用鼠标进行的操作,用手的话可能会执行一些ACTIONMOVE操作)从屏幕抬起时,会发生ACTIONUP事件。从之前输出的日志信心中可以看到,ACTIONUP事件同样从Activity开始到RTButton进行分发和处理,最后,由于我们注册了onClick事件,当onTouchEvent执行完毕后,就调用了onClick事件,那么onClick是在哪里被调用的呢?继续回到View.java的源代码中寻找。由于onTouchEvent在View.java中的源码比较长,这里就不贴出来了,感兴趣的可以自己去研究一下,通过源码阅读,我们在ACTIONUP的处理分支中可以看到一个performClick()方法,从这个方法的源码中可以看到执行了哪些操作。
    1. /**
    2. * Call this view's OnClickListener, if it is defined. Performs all normal
    3. * actions associated with clicking: reporting accessibility event, playing
    4. * a sound, etc.
    5. *
    6. * @return True there was an assigned OnClickListener that was called, false
    7. * otherwise is returned.
    8. */
    9. public boolean performClick() {
    10. sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
    11. ListenerInfo li = mListenerInfo;
    12. if (li != null && li.mOnClickListener != null) {
    13. playSoundEffect(SoundEffectConstants.CLICK);
    14. li.mOnClickListener.onClick(this);
    15. return true;
    16. }
    17. return false;
    18. }
    1. 在if分支里可以看到执行了li.mOnClickListener.onClick(this);这句代码,这里就执行了我们为RTButton 实现的onClick方法,所以,到目前为止,可以回答前一个“onClick是在哪里被调用的呢?”的问题了,onClick是在 onTouchEvent中被执行的,并且,onClick要后于onTouch的执行。
    2. 到此,点击按钮的事件传递就结束了,我们结合源代码窥探了其中的执行细节,如果我们修改各个事件控制方法的返回值又会发生什么情况呢,带着这个问题,进入下一节的讨论。
    1. 从上一节分析中,我们知道了在Android中存在哪些事件类型,事件的传递过程以及在源码中对应哪些处理方法。我们可以知道在Android中,事件是通过层级传递的,一次事件传递对应一个完整的层级关系,例如上节中分析的ACTIONDOWN事件从Activity传递到RTButton,ACTIONUP事件也同样。结合源码分析各个事件处理的方法,也可以明确看到事件的处理流程。
    2. 之前提过,所有事件处理方法的返回值都是boolean类型的,现在我们来修改这个返回值,首先从Activity开始,根据之前的日志输出结果, 首先执行的是Activity的dispatchTouchEvent方法,现在将之前的返回值 super.dispatchTouchEvent(event)修改为true,然后重新编译运行并点击按钮,看到如下的日志输出结果。

    1. 可以看到,事件执行到dispatchTouchEvent方法就没有再继续往下分发了,这也验证了之前的说法,返回true时,不再继续往下分 发,从之前分析过的Activity的dispatchTouchEvent源码中也可知,当返回true时,就没有去执行onTouchEvent方法 了。
    2. 接着,将上述修改还原,让事件在Activity这继续往下分发,接着就分发到了RTButton,将RTButton的dispatchTouchEvent方法的返回值修改为true,重新编译运行并查看输出日志结果。
    1. 从结果可以看到,事件在RTButton的dispatchTouchEvent方法中就没有再继续往下分发了。接着将上述修改还原,将 RTButton的onTouchEvent方法返回值修改为true,让其消费事件,根据之前的分析,onClick方法是在onTouchEvent 方法中被调用的,事件在这被消费后将不会调用onClick方法了,编译运行,得到如下日志输出结果。


    则流程图如下:
     
    上面示例中不调用Activity的onTouchEvent的原因是,他在button中的onTouchEvent中消耗了。


    Android嵌套布局事件传递
    1. 首先,新建一个类RTLayout继承于LinearLayout,同样重写dispatchTouchEvent和onTouchEvent方 法,另外,还需要重写onInterceptTouchEvent方法,在文章开头介绍过,这个方法只有在ViewGroup和其子类中才存在,作用是控 制是否需要拦截事件。这里不要和dispatchTouchEvent弄混淆了,后者是控制对事件的分发,并且后者要先执行。
    2. 那么,事件是先传递到View呢,还是先传递到ViewGroup的?通过下面的分析我们可以得出结论。首先,我们需要对工程代码进行一些修改。
    1. public class RTLayout extends LinearLayout {
    2. public RTLayout(Context context, AttributeSet attrs) {
    3. super(context, attrs);
    4. }
    5. @Override
    6. public boolean dispatchTouchEvent(MotionEvent event) {
    7. switch (event.getAction()) {
    8. case MotionEvent.ACTION_DOWN:
    9. System.out.println("RTLayout---dispatchTouchEvent---DOWN");
    10. break;
    11. case MotionEvent.ACTION_MOVE:
    12. System.out.println("RTLayout---dispatchTouchEvent---MOVE");
    13. break;
    14. case MotionEvent.ACTION_UP:
    15. System.out.println("RTLayout---dispatchTouchEvent---UP");
    16. break;
    17. default:
    18. break;
    19. }
    20. return super.dispatchTouchEvent(event);
    21. }
    22. @Override
    23. public boolean onInterceptTouchEvent(MotionEvent event) {
    24. switch (event.getAction()) {
    25. case MotionEvent.ACTION_DOWN:
    26. System.out.println("RTLayout---onInterceptTouchEvent---DOWN");
    27. break;
    28. case MotionEvent.ACTION_MOVE:
    29. System.out.println("RTLayout---onInterceptTouchEvent---MOVE");
    30. break;
    31. case MotionEvent.ACTION_UP:
    32. System.out.println("RTLayout---onInterceptTouchEvent---UP");
    33. break;
    34. default:
    35. break;
    36. }
    37. return super.onInterceptTouchEvent(event);
    38. }
    39. @Override
    40. public boolean onTouchEvent(MotionEvent event) {
    41. switch (event.getAction()) {
    42. case MotionEvent.ACTION_DOWN:
    43. System.out.println("RTLayout---onTouchEvent---DOWN");
    44. break;
    45. case MotionEvent.ACTION_MOVE:
    46. System.out.println("RTLayout---onTouchEvent---MOVE");
    47. break;
    48. case MotionEvent.ACTION_UP:
    49. System.out.println("RTLayout---onTouchEvent---UP");
    50. break;
    51. default:
    52. break;
    53. }
    54. return super.onTouchEvent(event);
    55. }
    56. }
    1. 同时,在布局文件中为RTButton添加一个父布局,指明为自定义的RTLayout,修改后的布局文件如下。
    2. activity_main.xml
    1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    2. xmlns:tools="http://schemas.android.com/tools"
    3. android:layout_width="match_parent"
    4. android:layout_height="match_parent" >
    5. <com.ryantang.eventdispatchdemo.RTLayout
    6. android:id="@+id/myLayout"
    7. android:layout_width="match_parent"
    8. android:layout_height="match_parent" >
    9. <com.ryantang.eventdispatchdemo.RTButton
    10. android:id="@+id/btn"
    11. android:layout_width="match_parent"
    12. android:layout_height="wrap_content"
    13. android:text="Button" />
    14. </com.ryantang.eventdispatchdemo.RTLayout>
    15. </LinearLayout>
    1. 最后,我们在Activity中也为RTLayout设置onTouchonClick事件,在MainActivity中添加如下代码。
    2. MainActivity.java
    1. rtLayout.setOnTouchListener(new OnTouchListener() {
    2. @Override
    3. public boolean onTouch(View v, MotionEvent event) {
    4. switch (event.getAction()) {
    5. case MotionEvent.ACTION_DOWN:
    6. System.out.println("RTLayout---onTouch---DOWN");
    7. break;
    8. case MotionEvent.ACTION_MOVE:
    9. System.out.println("RTLayout---onTouch---MOVE");
    10. break;
    11. case MotionEvent.ACTION_UP:
    12. System.out.println("RTLayout---onTouch---UP");
    13. break;
    14. default:
    15. break;
    16. }
    17. return false;
    18. }
    19. });
    20. rtLayout.setOnClickListener(new OnClickListener() {
    21. @Override
    22. public void onClick(View v) {
    23. System.out.println("RTLayout clicked!");
    24. }
    25. });
    1. 代码修改完毕后,编译运行工程,同样,点击按钮,查看日志输出结果如下:




    1. 从日志输出结果我们可以看到,嵌套了RTLayout以后,事件传递的顺序变成了 Activity->RTLayout->RTButton,这也就回答了前面提出的问题,Android中事件传递是从ViewGroup 传递到View的,而不是反过来传递的。
    2. 从输出结果第三行可以看到,执行了RTLayoutonInterceptTouchEvent方法,该方法的作用就是判断是否需要拦截事件,我们到ViewGroup的源码中看看该方法的实现。
    ViewGroup.java
    1. public boolean onInterceptTouchEvent(MotionEvent ev) {
    2. return false;
    3. }
    1. 该方法的实现很简单,只返回了一个false。那么这个方法是在哪被调用的呢,通过日志输出分析可知它是在RTLayout dispatchTouchEvent执行后执行的,那我们就进到dispatchTouchEvent源码里面去看看。由于源码比较长,我将其中的关键 部分截取出来做解释说明。
    1. // Check for interception.
    2. final boolean intercepted;
    3. if (actionMasked == MotionEvent.ACTION_DOWN
    4. || mFirstTouchTarget != null) {
    5. final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    6. if (!disallowIntercept) {
    7. intercepted = onInterceptTouchEvent(ev);
    8. ev.setAction(action); // restore action in case it was changed
    9. } else {
    10. intercepted = false;
    11. }
    12. } else {
    13. // There are no touch targets and this action is not an initial down
    14. // so this view group continues to intercept touches.
    15. intercepted = true;
    16. }
    1. 从这部分代码中可以看到onInterceptTouchEvent调用后返回值被赋值给intercepted,该变量控制了事件是否要向其子控 件分发,所以它起到拦截的作用,如果onInterceptTouchEvent返回false则不拦截,如果返回true则拦截当前事件。我们现在将 RTLayout中的该方法返回值修改为true,并重新编译运行,然后点击按钮,查看输出结果如下。



    1. 可以看到,我们明明点击的按钮,但输出结果显示RTLayout点击事件被执行了,再通过输出结果分析,对比上次的输出结果,发现本次的输出结果完 全没有RTButton的信息,没错,由于onInterceptTouchEvent方法我们返回了true,在这里就将事件拦截了,所以他不会继续分 发给RTButton了,反而交给自身的onTouchEvent方法执行了,理所当然,最后执行的就是RTLayout的点击事件了。
    1. 再看一下整个ViewGroup事件分发过程的流程图吧,相信可以帮助大家更好地去理解:
    1. 详解onInterceptTouchEvent()
    2. down事件首先会传递到onInterceptTouchEvent()方法
    3. 如果该ViewGroup的onInterceptTouchEvent()在接收到down事件处理完成之后return false,那么后续的move, up等事件将继续会先传递给该ViewGroup,之后才和down事件一样传递给最终的目标view的onTouchEvent()处理。
    4. 如果该ViewGroup的onInterceptTouchEvent()在接收到down事件处理完成之后return true,那么后续的move, up等事件将不再传递给onInterceptTouchEvent(),而是和down事件一样传递给该ViewGroup的 onTouchEvent()处理,注意,目标view将接收不到任何事件。
    1. 总结
    2. 以上我们对Android事件传递机制进行了分析,期间结合系统源码对事件传递过程中的处理情况进行了探究。通过单布局情况和嵌套布局情况下的事件传递和处理进行了分析,现总结如下:
    3. Android中事件传递按照从上到下再从下到上进行层级传递,事件处理从Activity开始到ViewGroup再到View,如果View没有消费事件会再次从View到ViewGroup再到Activity最后事件被抛出消费掉。
    4. 事件传递方法包括dispatchTouchEvent、onInterceptTouchEven、onTouchEvent,这三个方法的作用分别是负责事件分发、事件拦截、事件处理。其中事件分发和事件处理是View和ViewGroup都有的,最后一个事件拦截只有ViewGroup才有的方法。
    5. 事件从被点击的控件开始,先开始从下到上的通过getParent()找到根布局rootView,然后开始从上向下的开始事件分发dispatchTouchEvent(),事件拦截onInterceptTouchEvent()和事件处理onTouchEvent()。如果事件被拦截了被处理但事件没有被消费的话,事件就会从被拦截处理的控件开始自下到上的开始往上传递,在传递过程中会调用父类的事 件拦截onInterceptTouchEvent()和事件处理onTouchEvent()方法;如果事件一直没被消费,将会被rootView抛出 结束事件传递。在事件被触发开始从下到上的传递后开始自上到下的事件分发,等事件传到最底部的控件后如果没有被消费的话,事件又开始自下到上的传递直到被 根部布局抛出,一旦事件被消费,事件传递机制就会停止结束。
    6. 在ViewGroup中可以通过onInterceptTouchEvent方法对事件传递进行拦截,onInterceptTouchEvent方法返回true代表不允许事件继续向子View传递,返回false代表不对事件进行拦截,默认返回false。
    7. 在事件从上到下分发到底后开始自下到上的传递过程中,子View可以通过调用getParent().requestDisallowInterceptTouchEvent(true); 让其父类ViewGroup布局不拦截事件(requestDisallowInterceptTouchEvent(true)此方法参数为true 表示不拦截事件,为false 表示拦截事件)




            
    1. 正常情况下,一个事件序列只能被一个VIew拦截且消耗,因为一旦一个元素拦截了某个事件,那么同一个事件序列内的所有事件都会直接交给它处理,因此同一个事件序列中的事件不能分别有两个View同时处理,但是通过特殊手段可以做到,比如一个View将本该自己处理的事件通过onTouchEvent强行传递给其他View处理。
    2. 某个View一旦决定拦截,那么这一个事件序列都只能由它处理,并且它的onInterceptTouchEvent不会再被调用。


    3. 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一事件序列中的其他事件都不会再交给它处理,并且事件将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。意思就是事件一旦交给一个View处理,那么它就必须消耗掉,否则同一事件序列中剩下的事件就不再交给它来处理了。


    4. 如果View不消耗出ACTION_DOWN以外的其他事件,那么这个点击事件会消失,最终这些消失的点击事件会传递给Activity处理。

    5. ViewGroup默认不拦截任何事件。
    6.     
    7. View没有onInterceptTouchEvent方法,一旦有事件传递给它,那么它的onTouchEvent方法就会被调用。
    8.     
    9. View的OnTouchEvent方法默认都会消耗事件(返回true)。
    10.     
    11. View的enable属性不影响onTouchEvent的默认返回值。
    12.     
    13. onClick会发生的前提是当前View是可点击的,并且它收到了down和up的事件。
    14.     
    15. 事件的传递过程是由外向内的,即事件总是项传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。(requestDisallowInterceptTouchEvent方法主要设置父元素中的FLAG_DISALLOW_INTERCEPT标记位,一旦设置后,ViewGroup将无法拦截除了ACTION_DOWN以外的其他点击事件)

            



    View的滑动冲突:
        常见的滑动冲突场景:
        

    场景一:        
               比如ViewPager里面嵌入Fragment,Fragment里面又有ListView所导致。
               但是ViewPager为我们处理了冲突。
           
     
    1. 滑动冲突处理规则
    2. 可以根据滑动距离和水平方向形成的夹角;或者根绝水平和竖直方向滑动的距离差;或者两个方向上的速度差等
    1. 解决方式
    2. 外部拦截法:点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要就不拦截。该方法需要重写父容器的onInterceptTouchEvent方法,在内部做相应的拦截即可,其他均不需要做修改。伪代码如下:
    1. public boolean onInterceptTouchEvent(MotionEvent event) {
    2. boolean intercepted = false;
    3. int x = (int) event.getX();
    4. int y = (int) event.getY();
    5. switch (event.getAction()) {
    6. case MotionEvent.ACTION_DOWN: {
    7. intercepted = false;
    8. break;
    9. }
    10. case MotionEvent.ACTION_MOVE: {
    11. int deltaX = x - mLastXIntercept;
    12. int deltaY = y - mLastYIntercept;
    13. if (父容器需要拦截当前点击事件的条件,例如:Math.abs(deltaX) > Math.abs(deltaY)) {
    14. intercepted = true;
    15. } else {
    16. intercepted = false;
    17. }
    18. break;
    19. }
    20. case MotionEvent.ACTION_UP: {
    21. intercepted = false;
    22. break;
    23. }
    24. default:
    25. break;
    26. }
    27. mLastXIntercept = x;
    28. mLastYIntercept = y;
    29. return intercepted;
    30. }
    1. 内部拦截法:父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器来处理。这种方法和Android中的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作。
    1. public boolean dispatchTouchEvent(MotionEvent event) {
    2. int x = (int) event.getX();
    3. int y = (int) event.getY();
    4. switch (event.getAction()) {
    5. case MotionEvent.ACTION_DOWN: {
    6. getParent().requestDisallowInterceptTouchEvent(true);
    7. break;
    8. }
    9. case MotionEvent.ACTION_MOVE: {
    10. int deltaX = x - mLastX;
    11. int deltaY = y - mLastY;
    12. if (当前view需要拦截当前点击事件的条件,例如:Math.abs(deltaX) > Math.abs(deltaY)) {
    13. getParent().requestDisallowInterceptTouchEvent(false);
    14. }
    15. break;
    16. }
    17. case MotionEvent.ACTION_UP: {
    18. break;
    19. }
    20. default:
    21. break;
    22. }
    23. mLastX = x;
    24. mLastY = y;
    25. return super.dispatchTouchEvent(event);
    26. }
    父View的onInterceptTouchEvent(...)伪代码
    1. public boolean onInterceptTouchEvent(MotionEvent ev){
    2. if(ev.getAction() == MotionEvent.ACTION_DOWN){
    3. retuen false;
    4. }else{
    5. retuen true;
    6. }
    7. }

     




     




  • 相关阅读:
    【nodejs】使用Node.js实现REST Client调用REST API
    [Groovy] Groovy && JNI
    一些很实用的Maven Plugins
    秋天到了
    今天头好疼
    想起那个人的时候
    不知道标题
    生活让我懂得
    显示实现接口和实现接口
    Foreach原理
  • 原文地址:https://www.cnblogs.com/You0/p/5984465.html
Copyright © 2011-2022 走看看