美文网首页
安卓自定义view

安卓自定义view

作者: 静默的小猫 | 来源:发表于2021-01-05 07:43 被阅读0次

    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

          本文链接:https://www.haomeiwen.com/subject/agrboktx.html