安卓自定义view

view的视图架


每一个Activity都包含的一个window,这个window的实现类是Phone Window。后Phone Window是顶层的view,叫docor view。docor view中有一个叫content的FrameLayout,我们经常在Activity的onCreate中使用setContentView(R.layout.id)设置我们自定义的视图,就是添加到这个叫content的framelayout中。然后上面还有一个TitleActionBar,这个就是TitleBar。

在content这个view中,是我们在xml中自定义的view。从图中可以看出,我们自定义的view形成了一个树形结构。viewGroup中可以放置若干个view,而由于viewGroup本身也继承了view,所以ViewGroup的子view也可以是另一个Viewgroup,这样形成一个树形结构。

MeasureSpec

这是一个很重要的概念,一个view的MeasureSpec可以看作这个view暂定的大小。

MeasureSpec是一个32位的int值。前两位表示测量模式,后两位表示暂定的测量大小。

测量模式有三种,分别是:UNSPECIFIED,即父容器对子容器的大小不作任何要求;EXACTLY,父容器已经确定了自容器的精确大小;AT_MOST,父容器指定了一个最大的大小,view不能超过这个大小。

MeasureSpec由当前view的parentView调用getChildMeasureSpec生成,传入的三个参数分别是,parentView的MeasureSpec(父容器的暂定大小),parentView的左右上下内边距和当前view的LayoutParams(自容器的期望大小)。

/**

* Does the hard part of measureChildren: figuring out the MeasureSpec to

* pass to a particular child. This method figures out the right MeasureSpec

* for one dimension (height or width) of one child view.

*

* 这个方法将根据父容器的MeasureSpec和子View LayoutParams中的宽/高

* 为子View生成最合适的MeasureSpec

*

* @param spec 父容器的MeasureSpec

* @param padding 父容器的内间距(padding)加上子View的外间距(margin)

* @param childDimension 子View的LayoutParams中封装的width/height

* @return 子View的MeasureSpec

*/ 

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {

    // ① 对父容器的MeasureSpec进行解包

    int specMode = MeasureSpec.getMode(spec);

    int specSize = MeasureSpec.getSize(spec);

    // ② 减去间距,得到最大可用空间

    int size = Math.max(0, specSize - padding);

    // 记录子View最终的大小和测量模式

    int resultSize = 0;

    int resultMode = 0;

    switch (specMode) {

    // ③ 父容器是精准测量模式

    case MeasureSpec.EXACTLY:

        if (childDimension >= 0) {              //如果子view在LayoutParam中指定了大小,那么子view的resultSize 就是该大小,模式是EXACTLY

            resultSize = childDimension;

            resultMode = MeasureSpec.EXACTLY;

        } else if (childDimension == LayoutParams.MATCH_PARENT) {      //如果子view中LayoutParams是MATCH_PARENT,则view的大小等于最大可用大小,测量模式是EXACTLY

            resultSize = size;

            resultMode = MeasureSpec.EXACTLY;

        } else if (childDimension == LayoutParams.WRAP_CONTENT) {  //如果子view中LayoutParams是WRAP_CONTENT,则view的大小等于最大可用大小,测量模式是AT_MOST

            resultSize = size;

            resultMode = MeasureSpec.AT_MOST;

        }

        break;

    // ④ 父容器指定了一个最大可用的空间

    case MeasureSpec.AT_MOST:

        if (childDimension >= 0) {

            resultSize = childDimension;

            resultMode = MeasureSpec.EXACTLY;

        } else if (childDimension == LayoutParams.MATCH_PARENT) {

            resultSize = size;

            resultMode = MeasureSpec.AT_MOST;

        } else if (childDimension == LayoutParams.WRAP_CONTENT) {

            resultSize = size;

            resultMode = MeasureSpec.AT_MOST;

        }

        break;

    // ⑤ 父容器不对子View的大小作出限制

    case MeasureSpec.UNSPECIFIED:

        if (childDimension >= 0) {

            resultSize = childDimension;

            resultMode = MeasureSpec.EXACTLY;

        } else if (childDimension == LayoutParams.MATCH_PARENT) {

            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;

            resultMode = MeasureSpec.UNSPECIFIED;

        } else if (childDimension == LayoutParams.WRAP_CONTENT) {

            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;

            resultMode = MeasureSpec.UNSPECIFIED;

        }

        break;

    }

    // ⑥ 将最终的size和mode打包为子View需要的MeasureSpec

    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);

}

在view中每个子view的MeasureSpec都是由父容器的MeasureSpec、间距和自容器的LayoutParam来生成,逐级往上,最顶层的view,也就是docor view的MeasureSpec是怎么生成的呢?docor view的MeasureSpec由其上层的window的大小和docor view的自身的LayoutParam来生成。

自定义View 6步

1.继承View

2.实现画笔paint类

3.覆写onMeasure(…)方法

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    int w = MeasureSpec.getSize(widthMeasureSpec);

    int h = MeasureSpec.getSize(heightMeasureSpec);

    setMeasuredDimension(w, h);

}

4.实现onSizeChanged(…)方法

这个方法是你获取View现在的宽和高. 这里我们计算的是中心和半径。

@Override

protected void onSizeChanged(int w, int h, int oldw, int oldh) {

    mCenterX = w / 2f;

    mCenterY = h / 2f;

    mRadius = Math.min(w, h) / 2f;

}

5.实现onDraw(…)方法

这个方法提供了如何绘制view,它提供的Canvas类可以进行绘制。

@Override

protected void onDraw(Canvas canvas) {

    // draw face

    canvas.drawCircle(mCenterX, mCenterY, mRadius, mCirclePaint);

    // draw eyes

    float eyeRadius = mRadius / 5f;

    float eyeOffsetX = mRadius / 3f;

    float eyeOffsetY = mRadius / 3f; canvas.drawCircle(mCenterX - eyeOffsetX, mCenterY - eyeOffsetY, eyeRadius, mEyeAndMouthPaint); canvas.drawCircle(mCenterX + eyeOffsetX, mCenterY - eyeOffsetY, eyeRadius, mEyeAndMouthPaint);

    // draw mouth

    float mouthInset = mRadius /3f;

    mArcBounds.set(mouthInset, mouthInset, mRadius * 2 - mouthInset, mRadius * 2 - mouthInset);

    canvas.drawArc(mArcBounds, 45f, 90f, false, mEyeAndMouthPaint);

}

6.添加你的View到xml使用

7.自定义View的触摸反馈

重写onTouchEvent(),在方法内部定制触摸反馈算法

是否取消事件取决于ACTION_DOWN事件或PONITER_DOWN事件是否返回true

MotionEvent

getActionMasked()和getAction()

 POINTER_DOWN / POINTER_UP 和 getActionIndex()

onTouchEvent()

当用户按下(ACTION_DOWN)

  如果不在滑动控件中,切换至按下状态,并注册长按计时器

   如果在滑动控件中,切换至按下状态,并注册按下计时器

    当进入按下状态并移动(ACTION_MOVE)

    重绘Ripple Effect

    如果移动出自己的范围,自我标记本次事件失效,忽略后续事件

    当用户抬起(ACTION_UP)

    如果是按下状态并且未触发长按,切换至抬起状态并触发点击事件,并清除一切状态

    如果已经触发长按,切换至抬起状态并清除一切状态

    当事件意外结束(ACTION_CANCEL)

     切换至抬起状态,并清除一切状态

自定义ViewGroup 的触摸反馈

除了重写onTouchEvent(),还需要重写onInterceptEvent()

 onInterceptEvent() 不用在第一时间返回true,而是在任意事件,需要拦截的时候返回true就行

触摸反馈的流程

Activity.dispatchEvent()

        递归:ViewGroup(View).dispatchTouchEvent()

            ViewGroup.onInterceptTouchEvent()

            ViewGroup.onIntercepTouchEvent()

            Child.dispatchTouchEvent()

            Super.dispatchEvent()

                View.onTouchEvent()

        Activity.onTouchEvent()

View.dispatchTouchEvent()

如果设置了OnTouchListener,调用OnTouchListener.onTouch()

            如果OnTouchListener消费了事件,返回true

            如果onTouchListener没有了消费事件,继续调用自己的onTouchEvent(),并返回和onTouchEvent()相同的结果

        如果没有设置onTouchEvent,同上

ViewGroup.dispatchEvent()

如果是用户初次按下(ACTION_DOWN),清空TouchTargets和DISALLOW_INTERCEPT标记

        拦截处理

        如果不拦截并且不是CANCEL事件,并且是DOWN或者POINTER_DOWN,尝试把pointer(手指)通过TouchTarget分配给子View;并且如果分配给了新的子View,调用child.dispathTouchEvent()把事件传递给子View

        看有没有TouchTarget

            如果没有TouchTarget,调用自己的super.dispatchTouchEvent()

            如果有,调用child.dispatchEvent()把事件传给对应的子View(如果有的话)

            如果是POINTER_UP,从TouchTargets中清除POINTER信息,如果是UP或CANCEL,重置状态

TouchTarget

作用:记录每个子View是被哪些pointer(手指)按下的

        结构:单向链表

拦截处理

如果不是初次按下,并且没有TouchTarget,直接拦截

        如果不是初次按下,或者有TouchTarget

            如果设置了disallow intercept,不拦截


否则,调用onInterceptTouchEvent(),如果返回true则拦截,返回false则不拦截

事件分发机制涉及两个知识点:1、事件分发传递流程;2、滑动冲突问题


仔细看的话,图分为3层,从上往下依次是 Activity、ViewGroup、View

事件从左上角那个白色箭头开始,由 Activity 的 dispatchTouchEvent 做分发

箭头的上面字代表方法返回值,(return true、return false、return super.xxxxx(),super 的意思是调用父类实现。

dispatchTouchEvent 和 onTouchEvent 的框里有个【true---->消费】的字,表示的意思是如果方法返回 true,那么代表事件就此消费,不会继续往别的地方传了,事件终止。

目前所有的图的事件是针对 ACTION_DOWN 的,对于 ACTION_MOVE 和 ACTION_UP我们最后做分析。

只有 return super.dispatchTouchEvent(ev) 才是往下走,返回true 或者 false 事件就被消费了(终止传递)。

1、如果事件不被中断,整个事件流向是一个类U型图,我们来看下这张图,可能更能理解U型图的意思。


所以如果我们没有对控件里面的方法进行重写或更改返回值,而直接用super调用父类的默认实现,那么整个事件流向应该是从 Activity---->ViewGroup--->View 从上往下调用 dispatchTouchEvent方法,一直到叶子节点(View)的时候,再由 View--->ViewGroup--->Activity 从下往上调用 onTouchEvent 方法。

2、dispatchTouchEvent 和 onTouchEvent 一旦 return true, 事件就停止传递了(到达终点)(没有谁能再收到这个事件)。看下图中只要 return true 事件就没再继续传下去了,对于 return true 我们经常说事件被消费了,消费了的意思就是事件走到这里就是终点,不会往下传,没有谁能再收到这个事件了。

3、dispatchTouchEvent 和 onTouchEvent return false 的时候事件都回传给父控件的onTouchEvent 处理。

看上图深蓝色的线,对于返回 false 的情况,事件都是传给父控件 onTouchEvent 处理。

对于 dispatchTouchEvent 返回 false 的含义应该是:事件停止往子 View 传递和分发同时开始往父控件回溯(父控件的 onTouchEvent 开始从下往上回传直到某个 onTouchEvent return true),事件分发机制就像递归,return false 的意义就是递归停止然后开始回溯。

对于onTouchEvent return false 就比较简单了,它就是不消费事件,并让事件继续往父控件的方向从下往上流动。

4、dispatchTouchEvent、onTouchEvent、onInterceptTouchEvent ViewGroup 和 View 的这些方法的默认实现就是会让整个事件安装 U 型完整走完,所以 return super.xxxxxx() 就会让事件依照U型的方向的完整走完整个事件流动路径),中间不做任何改动,不回溯、不终止,每个环节都走到。



所以如果看到方法 return super.xxxxx() 那么事件的下一个流向就是走 U 型下一个目标,稍微记住上面这张图,你就能很快判断出下一个走向是哪个控件的哪个函数。

5、onInterceptTouchEvent 的作用


Intercept 的意思就拦截,每个 ViewGroup 每次在做分发的时候,问一问拦截器要不要拦截(也就是问问自己这个事件要不要自己来处理)如果要自己处理那就在 onInterceptTouchEvent 方法中 return true 就会交给自己的 onTouchEvent 的处理,如果不拦截就是继续往子控件往下传。默认是不会去拦截的,因为子 View 也需要这个事件,所以 onInterceptTouchEvent 拦截器 return super.onInterceptTouchEvent() 和r eturn false 是一样的,是不会拦截的,事件会继续往子 View 的 dispatchTouchEvent 传递。

6、ViewGroup 和View 的dispatchTouchEvent方法返回super.dispatchTouchEvent()的时候事件流走向。


首先看下 ViewGroup 的 dispatchTouchEvent,之前说的 return true 是终结传递。return false 是回溯到父 View 的 onTouchEvent,然后 ViewGroup 怎样通过 dispatchTouchEvent 方法能把事件分发到自己的 onTouchEvent 处理呢,return true 和 false 都不行,那么只能通过 Interceptor 把事件拦截下来给自己的 onTouchEvent,所以 ViewGroup dispatchTouchEvent 方法的 super 默认实现就是去调用 onInterceptTouchEvent,记住这一点。

那么对于 View 的 dispatchTouchEvent return super.dispatchTouchEvent() 的时候呢事件会传到哪里呢,很遗憾 View 没有拦截器。但是同样的道理 return true 是终结。return false 是回溯会父类的 onTouchEvent,怎样把事件分发给自己的 onTouchEvent 处理呢,那只能 return super.dispatchTouchEvent,View 类的 dispatchTouchEvent() 方法默认实现就是能帮你调用 View 自己的 onTouchEvent 方法的。

说了这么多,不知道有说清楚没有,我这边最后总结一下:

对于 dispatchTouchEvent,onTouchEvent,return true是终结事件传递。return false 是回溯到父 View 的 onTouchEvent 方法。

ViewGroup 想把自己分发给自己的 onTouchEvent,需要拦截器 onInterceptTouchEvent 方法 return true 把事件拦截下来。

ViewGroup 的拦截器 onInterceptTouchEvent 默认是不拦截的,所以 return super.onInterceptTouchEvent()=return false;

View 没有拦截器,为了让View可以把事件分发给自己的 onTouchEvent,View的 dispatchTouchEvent 默认实现(super)就是把事件分发给自己的 onTouchEvent。

ViewGroup 和 View 的 dispatchTouchEvent 是做事件分发,那么这个事件可能分发出去的四个目标

注:------> 后面代表事件目标需要怎么做。

1、 自己消费,终结传递。------->return true ;

2、 给自己的 onTouchEvent 处理-------> 调用 super.dispatchTouchEvent() 系统默认会去调用 onInterceptTouchEvent,在onInterceptTouchEvent return true就会去把事件分给自己的 onTouchEvent 处理。

3、 传给子 View ------>调用 super.dispatchTouchEvent() 默认实现会去调用 onInterceptTouchEvent 在 onInterceptTouchEvent return false,就会把事件传给子类。

4、 不传给子 View,事件终止往下传递,事件开始回溯,从父 View 的 onTouchEvent 开始事件从下到上回归执行每个控件的 onTouchEvent -------> return false;

注: 由于 View 没有子 View 所以不需要 onInterceptTouchEvent 来控件是否把事件传递给子 View 还是拦截,所以 View 的事件分发调用 super.dispatchTouchEvent() 的时候默认把事件传给自己的 onTouchEvent 处理(相当于拦截),对比 ViewGroup 的 dispatchTouchEvent 事件分发,View 的事件分发没有上面提到的4个目标的第3点

ViewGroup 和 View 的 onTouchEvent 方法是做事件处理的,那么这个事件只能有两个处理方式:

1、自己消费掉,事件终结,不再传给谁----->return true;

2、继续从下往上传,不消费事件,让父View也能收到到这个事件----->return false;View的默认实现是不消费的。所以super==false。

ViewGroup的onInterceptTouchEvent方法对于事件有两种情况:

1、拦截下来,给自己的onTouchEvent处理--->return true;

2、不拦截,把事件往下传给子View---->return false,ViewGroup默认是不拦截的,所以super==false;

关于ACTION_MOVE 和 ACTION_UP

上面讲解的都是针对ACTION_DOWN的事件传递,ACTION_MOVE和ACTION_UP在传递的过程中并不是和ACTION_DOWN 一样,你在执行ACTION_DOWN的时候返回了false,后面一系列其它的action就不会再得到执行了。简单的说,就是当dispatchTouchEvent在进行事件分发的时候,只有前一个事件(如ACTION_DOWN)返回true,才会收到ACTION_MOVE和ACTION_UP的事件。具体这句话很多博客都说了,但是具体含义是什么呢?我们来看一下下面的具体分析。

上面提到过了,事件如果不被打断的话是会不断往下传到叶子层(View),然后又不断回传到Activity,dispatchTouchEvent 和 onTouchEvent 可以通过return true 消费事件,终结事件传递,而onInterceptTouchEvent 并不能消费事件,它相当于是一个分叉口起到分流导流的作用,ACTION_MOVE和ACTION_UP 会在哪些函数被调用,之前说了并不是哪个函数收到了ACTION_DOWN,就会收到 ACTION_MOVE 等后续的事件的。

下面通过几张图看看不同场景下,ACTION_MOVE事件和ACTION_UP事件的具体走向并总结一下规律。

//打印日志Activity|dispatchTouchEvent-->ACTION_DOWN ViewGroup1|dispatchTouchEvent-->ACTION_DOWN---->消费


在这种场景下ACTION_MOVE和ACTION_UP 将如何呢,看下面的打出来的日志

Activity|dispatchTouchEvent-->ACTION_MOVE ViewGroup1|dispatchTouchEvent-->ACTION_MOVE----Activity|dispatchTouchEvent-->ACTION_UP ViewGroup1|dispatchTouchEvent-->ACTION_UP----

下图中

红色的箭头代表ACTION_DOWN 事件的流向

蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向

2、我们在ViewGroup2 的dispatchTouchEvent 返回true消费这次事件

Activity|dispatchTouchEvent-->ACTION_DOWN ViewGroup1|dispatchTouchEvent-->ACTION_DOWNViewGroup1|onInterceptTouchEvent-->ACTION_DOWNViewGroup2|dispatchTouchEvent-->ACTION_DOWN---->消费Activity|dispatchTouchEvent-->ACTION_MOVE ViewGroup1|dispatchTouchEvent-->ACTION_MOVEViewGroup1|onInterceptTouchEvent-->ACTION_MOVEViewGroup2|dispatchTouchEvent-->ACTION_MOVE----TouchEventActivity|dispatchTouchEvent-->ACTION_UP ViewGroup1|dispatchTouchEvent-->ACTION_UPViewGroup1|onInterceptTouchEvent-->ACTION_UPViewGroup2|dispatchTouchEvent-->ACTION_UP----

红色的箭头代表ACTION_DOWN 事件的流向

蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向


3、我们在View 的 dispatchTouchEvent 返回 true 消费这次事件

这个我不就画图了,效果和在ViewGroup2 的dispatchTouchEvent return true的差不多,同样的收到ACTION_DOWN 的dispatchTouchEvent函数都能收到 ACTION_MOVE和ACTION_UP。

所以我们就基本可以得出结论如果在某个控件的dispatchTouchEvent 返回true消费终结事件,那么收到ACTION_DOWN 的函数也能收到 ACTION_MOVE和ACTION_UP。

4、我们在View 的onTouchEvent 返回true消费这次事件

红色的箭头代表ACTION_DOWN 事件的流向

蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向

5、我们在ViewGroup 2 的onTouchEvent 返回true消费这次事件

红色的箭头代表ACTION_DOWN 事件的流向

蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向

6、我们在ViewGroup 1 的onTouchEvent 返回true消费这次事件

红色的箭头代表ACTION_DOWN 事件的流向

蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向

7、我们在Activity 的onTouchEvent 返回true消费这次事件

红色的箭头代表ACTION_DOWN 事件的流向

蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向

8、我们在View的dispatchTouchEvent 返回false并且Activity 的onTouchEvent 返回true消费这次事件

红色的箭头代表ACTION_DOWN 事件的流向

蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向


9、我们在View的dispatchTouchEvent 返回false并且ViewGroup 1 的onTouchEvent 返回true消费这次事件

红色的箭头代表ACTION_DOWN 事件的流向

蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向


10、我们在View的dispatchTouchEvent 返回false并且在ViewGroup 2 的onTouchEvent 返回true消费这次事件

红色的箭头代表ACTION_DOWN 事件的流向

蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向


11、我们在ViewGroup2的dispatchTouchEvent 返回false并且在ViewGroup1 的onTouchEvent返回true消费这次事件

红色的箭头代表ACTION_DOWN 事件的流向

蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向



12、我们在 ViewGroup2 的 onInterceptTouchEvent 返回 true 拦截此次事件并且在ViewGroup 1 的onTouchEvent返回true消费这次事件。

红色的箭头代表ACTION_DOWN 事件的流向

蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向


对于在onTouchEvent消费事件的情况:在哪个View的onTouchEvent 返回true,那么ACTION_MOVE和ACTION_UP的事件从上往下传到这个View后就不再往下传递了,而直接传给自己的onTouchEvent 并结束本次事件传递过程。

对于ACTION_MOVE、ACTION_UP总结:ACTION_DOWN事件在哪个控件消费了(return true), 那么ACTION_MOVE和ACTION_UP就会从上往下(通过dispatchTouchEvent)做事件分发往下传,就只会传到这个控件,不会继续往下传,如果ACTION_DOWN事件是在 dispatchTouchEvent 消费,那么事件到此为止停止传递(MOVE 和 UP也会传到这里),如果ACTION_DOWN事件是在 onTouchEvent 消费的,那么会把ACTION_MOVE或ACTION_UP事件传给该控件的 onTouchEvent 处理并结束传递。

作者:阿瑞921

链接:https://www.jianshu.com/p/11b37cfebb21

来源:

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

你可能感兴趣的:(安卓自定义view)