View的绘制与事件分发机制

1. Android视图构成

Android视图构成.png

2. View 的绘制流程

当 Activity 接收到焦点的时候,它会被请求绘制布局,该请求由 Android 的 framework 层处理。绘制是从根节点开始,从上到下开始遍历,对布局树递归地进行 measure、layout 和 draw。整个 View 树的绘图流程在 ViewRoot.java 类的 performTraversals() 函数展开,该函数所做 的工作可简单概况为是否需要重新计算视图大小(measure)、是否需要重新安置视图的位置(layout)、以及是否需要重绘(draw),流程图如下:

View 树的绘图流程.png

绘制顺序.png

3. 事件分发机制

(1) 为什么要使用事件分发
android中View是树形结构的,View可能会重叠在一起,当我们点击一个地方有多个View都可以响应,这个点击事件应该分配给谁呢?为了解决这个问题,就有了事件分发机制。

(2) 三个重要的事件分发事件

Android事件分发机制主要由“事件分发”—>“事件拦截”—>“事件响应”这三步来进行逻辑控制的。

  • 事件分发:dispatchTouchEvent(MotionEvent event)
  • 事件拦截:onInterceptTouchEvent()
  • 事件响应:onTouchEvent()

(3) 事件分发流程
问题:如下图所示,点击View1的位置时,由于View重叠,View1、GroupView 和 RootView都可响应事件,这个点击事件应该分配给谁呢?

View重叠及对应的视图结构.png

分析:点击View1位置时,如其他父控件都不拦截,仅View1对事件进行拦截,则事件分发流程如下

事件分发流程.png

相关结论

a. Activity 和 View 是没有拦截事件的,即无onInterceptTouchEvent()方法。原因是

  • Activity 作为事件的原始分发者,若拦截了事件,则整个屏幕都会无法响应事件。
  • View 作为事件传递的最末端,要么消费处理掉事件,要么不处理并回传事件给activity,因为向下没有子控件了,所有没必要再对事件拦截并向下分发。

b. 屏幕被点击后,事件传递过程为:Activity—>PhoneWindow—>DecorView—>GroupView—>...—>View,即点击事件发生后,事件先传到Activity、再传到ViewGroup、最终再传到View。

c. 点击事件的分发过程如下:dispatchTouchEvent—>onTouchListener的OnTouch方法—>onTouchEvent—>onClickListener的onClick方法。从而也可以看出onTouch优先于onClick执行,即事件传递的顺序是先经过onTouch,再传递到onClick。如:

为一个按钮同事注册点击事件和触摸事件。

package comi.example.liy.mytestdemo;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;

/**
 * Created by liy on 2019-12-18 8:55
 */
public class EventDispatchActivity extends AppCompatActivity {

    private Button button;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_event_dispatch);
        button = findViewById(R.id.btn_event);

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("liy", "onClick execute");
            }
        });

        button.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.d("liy", "onTouch execute, action " + event.getAction());
                /*return false;*/

                switch (event.getAction()) {// 获取当前触摸事件的Action
                    case MotionEvent.ACTION_DOWN://手指按下
                        Log.d("liy", "手指按下: " + event.getAction());
                        break;
                    case MotionEvent.ACTION_MOVE://手指滑动
                        //获取手指滑动到新的位置
                        Log.d("liy", "手指滑动: " + event.getAction());
                        break;
                    case MotionEvent.ACTION_UP://手指抬起
                        Log.d("liy", "手指抬起: " + event.getAction());
                        break;
                }
                //onTouch事件默认返回false;如果设置为true,那么这个触摸事件会被onTouch消费掉,不会再继续向下传递。
                return false;

            }
        });
    }

}

点击按钮打印结果为:


onTouch优先于onClick执行.png

onTouch基础:

  • onTouch方法里能做的事情比onClick要多一些,比如判断手指按下、抬起、移动等事件。
  • onTouch事件默认返回false;如果设置为true,那么这个触摸事件会被onTouch消费掉,不会再继续向下传递,即不会触发onClick事件。

(4) 事件分发—源码分析

源码分析.png

  • onTouch和onTouchEvent的区别:从源码中可以看出,这两个方法都是在View的dispatchTouchEvent中调用的,onTouch优先于onTouchEvent执行。如果在onTouch方法中通过返回true将事件消费掉,onTouchEvent将不会再执行。
  • 注意:如果控件是非enable的,那么给它注册onTouch事件将永远得不到执行。对于这一类控件(如 ImageView),如果我们想要监听它的touch事件:第一,在ImageView的onTouch方法里返回true,这样可以保证ACTION_DOWN之后的其它action都能得到执行;第二,在布局文件里面给ImageView增加一个android:clickable="true"的属性,这样ImageView变成可点击的之后,即使在onTouch里返回了false,ACTION_DOWN之后的其它action也是可以得到执行的;第三:重写该控件的onTouchEvent方法。
  • touch事件的层级传递:如果给一个控件注册了touch事件,每次点击它的时候都会触发一系列的ACTION_DOWN,ACTION_MOVE,ACTION_UP等事件。这里需要注意,如果在执行ACTION_DOWN的时候返回了false,后面一系列其它的action就不会再得到执行了。简单的说,就是当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发后一个action。

3. 事件分发示例

(1) android外接USB扫码枪

(2) ImageView的触摸事件

ivPicture.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                // 获取当前触摸事件的Action
                switch (event.getAction()) {
                    //手指按下
                    case MotionEvent.ACTION_DOWN:
                        //获取绘画开始的位置
                        startX = event.getX();
                        startY = event.getY();
                        break;
                    //手指滑动
                    case MotionEvent.ACTION_MOVE:
                        //获取手指滑动到新的位置
                        float newStartX = event.getX();
                        float newStartY = event.getY();
                        //开始绘画
                        cacheCanvas.drawLine(startX, startY, newStartX, newStartY, paint);
                        //手指滑动过程要不断初始化开始位置,不然开始位置不变
                        startX = newStartX;
                        startY = newStartY;
                        //重新设置iv显示副本
                        ivPicture.setImageBitmap(cacheBitmap);
                        break;
                    //手指抬起
                    case MotionEvent.ACTION_UP:
                        cacheCanvas.drawPath(path,paint);
                        path.reset();
                        break;
                }
                //如果设置为true,那么这个触摸事件由该组件控制
                return true;
            }
        });

你可能感兴趣的:(View的绘制与事件分发机制)