View的事件体系

View的基础知识

通常来说用户界面都是由Activity组成,Activity中关联了一个PhoneWindow创建,在这个窗口下管理了一个视图树。这颗视图树的顶级视图就是一个GroupView类型的DecorView,DecorView下就是各个ViewGroup,ViewGroup下是各个View。

View的位置由四个顶点决定,分别对应四个属性:top,left,right,bottom,通过这几个单词就很容易知道所代表的意思。但是,这些坐标都是相对于View的父容器来说的,是一种相对坐标

MotionEvent

当你手指接触屏幕后会产生一些列事件,主要有ACTION_DOWN,ACTION_MOVE,ACTION_UP,所以我们平时所知道的点击事件、滑动事件分别是DOWN->UP,DOWN->MOVE->MOVE….->UP。说到这里就必须要提到一个概念TouchSlop。TouchSlop是系统所能识别出的被认为是滑动的最小距离,当两次滑动之间的距离小于这个常量,系统就不认为你是在进行滑动。关于Android系统中的滑动事件我们另外再讲,这里主要讲自定义View控件事件分发机制

自定义控件

虽然Android提供了很多强大的UI控件,但是依旧不能满足开发人员的需求。我们只能通过自定义View实现。自定义View也有几种实现类型,分别为继承View,继承现有控件(如ImageView),继承自ViewGroup实现布局类。比较主要的知识点就是测量与布局,View的绘制,触摸事件,动画等。

自定义View————最为自由的一种实现

public class SimpleImageView extends View {
    private Paint mPaint;
    private Drawable drawable;
    private int mWidth;
    private int mHeight;

    public SimpleImageView(Context context) {
        super(context);
    }

    public SimpleImageView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initAttributeSet(attrs);
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
    }

    public SimpleImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public SimpleImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    private void initAttributeSet(AttributeSet attrs) {
        if (attrs != null) {
            TypedArray array = null;
            try {
                array = getContext().obtainStyledAttributes(attrs, R.styleable.SimpleImageView);
                drawable = array.getDrawable(R.styleable.SimpleImageView_src);
                measureDrawable();
            } finally {
                if (array != null) {
                    array.recycle();
                }
            }
        }
    }

    private void measureDrawable() {
        if (drawable == null) {
            throw new RuntimeException("drawable非空");
        }
        mWidth = drawable.getIntrinsicWidth();
        mHeight = drawable.getIntrinsicHeight();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(mWidth, mHeight);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (drawable == null){
            return;
        }
        canvas.drawBitmap(ImageUtils.drawableToBitmap(drawable),getLeft(),getTop(),mPaint);
    }
}

我们首先创建了一个继承自View的SimpleImageView类,在含有构造函数我们会获取该控件的属性,并且进行初始化画笔和绘制图片。在vaule/attr.xml中我们定义了这个View的属性,便于后续使用,attr.xml文件如下:

<declare-styleable name="SimpleImageView">
     <attr name="src" format="integer"/>
declare-styleable>

当我们在XML文件中使用该控件时,需要指定它的图片资源。当应用启动时会从这个xml布局中解析SimpleImageView的属性,例如宽、高。进入SimpleImageView的构造函数后会调用initAttrs函数进行初始化。在initAttrs函数中,我们会首先读取SimpleImageView的属性集TypedArray;再从该对象中读取SimpleImageView_src属性值,该属性是一个drawable的资源id,然后我们根据这个id从该TypedArray对象中获取到该id对应的Drawable,最后调用measureDrawable函数测量该图片的大小。

关于该View控件的宽高是这样的。我们在SimpleImageView中设置了两个字段mWidth,mHeight,分别表示该视图的宽高。在measureDrawable函数中,我们通过在xml文件中指定资源id对应的drawable得到图片的宽高,并且把它们作为SimpleImageView的宽高。然后在SimpleImageView被加载的时候,首先会调用onMeasure函数测量SimpleImageView的大小,然后再把图片绘制出来。

关于Canvas 和Paint的函数较多,但理解起来比较简单,我们不多讲。另外关于Scroller的使用我们单独讲。

View的事件分发机制

我们上边讲了View的触摸事件,那么就会遇到View的另一大难题滑动冲突,解决办法的理论基础就是事件分发机制。

在介绍点击事件的传递规则之前,我们要明白这里分析的对象就是MotionEvent,即点击事件。当一个MotionEvent生成以后,系统需要把这个事件传递给一个具体的View,这个传递过程就是分发过程。点击事件的分发过程由三个很重要的方法来完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent。

public boolean dispatchTouchEvent(MotionEvent event)

用来进行事件分发。如果事件能够传递给当前View,那么此方法一定会调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent的方法影响,表示是否消耗当前事件。

public boolean onInterceptTouchEvent(MotionEvent event)

在上述方法内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列中,此方法不会被再次调用,返回结果表示是否拦截当前事件。

public boolean onTouchEvent(MotionEvent event)

在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个时间序列中,当前view无法再次接收到事件。其实,三个函数的关系可以用如下代码表示:

public boolean dispatchTouchEvent(MotionEvent event){
        boolean consume = false;
        if (onInterceptTouchEvent(event)){
            consume = onTouchEvent(event);
        }else {
            consume = child.dispatchTouchEvent(event);
        }
    }

对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,这时它的dispatchTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true,就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理,即它的onTouchEvent方法就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回false,就表示它不拦截这个事件,这时当前事件就会传递给它的子元素,如此传递下去,知道事件最终被处理。

当一个View需要处理事件时,如果它设置了OnTouchListener,那么OnTouchListener中的onTouch方法会被调用。这时事件如何处理还要看onTouch的返回值,如果返回false,则当前View的onTouchEvent方法会被调用;如果返回true,onTouchEvent方法将不调用。由此可见,给View设置的OnTouchListener,它的优先级比onTouchEvent要高在onTouchEvent方法中,如果当前设置的有OnClickListener,那么它的onClick方法会被调用。可见,我们平时常用的OnClickListener,其优先级最低,即处于事件传递的最末端。

这里我们要提一点,事件的传递过程遵循如下顺序:Activity->Window-View.当View的onTouchEvent方法返回false时怎么办呢?这时,它的父容器的onTouchEvent方法被启用,以此类推。直至传递给Activity处理。

这里总结了一些结论,帮助大家理解事件的分发机制。

  • 同一个事件序列是指从手指接触屏幕的那一刻算起,到手指离开屏幕的那一刻结束。也就是这个事件序列以down事件开始,中间含有不定数量的move事件,最终以up结束。

  • 正常情况下,一个事件序列只能被一个View拦截且消耗

  • 一旦一个元素拦截了某此事件,那么同一个事件序列内的所有事件都会直接交给它处理,因此同一个事件序列中的事件不能分别由两个View同时处理。但是通过特殊手段可以做到,比如一个View将本该自己处理的事件通过onTouchEvent强行传递给其他View处理。
  • 某个View一旦决定拦截,那么只一个事件序列只能由它处理,并且它的onInterceptTouchEvent不会再被调用。
  • 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent方法返回false),那么同一事件序列中的其他事件将不会再交给它处理,并且事件将重新交由它的父元素处理,即父元素的onTouchEvent会被调用。
  • 如果View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件将会消失,此时父元素的onTouchEvent并不会调用。
  • ViewGroup默认不拦截任何事件
  • View没有onInterceptTouvhEvent方法,一旦事件传递给它,那么它的onTouchEvent方法就会被调用。
  • View的onTouchEvent默认都会消耗事件,除非它是不可点击(clickable和longClickable同时为false)。View的longClickable属性默认都是false,clickable属性要分情况,比如Button的clickable属性为true,TextView默认为false。
  • View的enable属性不影响onTouchEvent的默认返回值。
  • onClick会发生的前提是View是可点击的,并接收到了down和up事件。
  • 事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View

你可能感兴趣的:(Android小工到专家,view,自定义view,android,事件分发机制)