Android进阶从零学习自定义View——概念基础

其实关于自定义View,网上实在是太多太多了,但是呢,还是最终决定开一个专栏来讲述自定义View。这个专栏的目的有两个:

1. 我自己对于自定义View的总结
2. 讲述我自己在学习自定义View过程中走过的弯路,旨在希望各位同学少走弯路。
3. 工作四年,从0回归学习自定义View

好了,其实自定义View知识点真的是很多,很复杂,很难理解,因此,本专栏会通过很多篇博客循序渐进的学习自定义View的相关内容。这个专栏的大纲如下:

1. 自定义View基础概念
2. View树的绘制流程
3. 自定义ViewGroup
4. 自定义ViewGroup项目中的常见问题(以ScrollView为例)
5. 自定义View

以上就是自定义View专栏的主要内容,PS:自定义ViewGroup也可以称为:自定义View。通过该专栏的学习,各位同学可掌握如下技能:

1. 能解决布局嵌套不显示问题
2. 能写出各式各样的自定义ViewGroup以及View
3. 能够明白问题的原理,以及源码的原理,轻松应对实际项目和面试中的相应问题

这个专栏我打算从基础,一层一层深入源码,并且,根据源码反推我们项目中遇到的实际问题的解决办法,这样的话,就达到了知其然而知其所以然

闲话少叙,开始今天的表演。。。

本篇博客是第一篇博客,学习一下自定义View的基础知识,其实关于基础知识,可能可能很多人不太在意,但是通过四年的工作磨炼,我发现其实万般框架,万般花里胡哨的东西,都是有基础知识构成的,所有本篇博客我们回归基础,重温学习一下。

自定义View的分类

关于自定义View我们分为两类

  1. 自定义ViewGroup: 一般只需要重写onMeasure方法和onLayout方法
  2. 自定义View:一般只需要重写onMeasure和onDraw方法。

我们知道一般无论是自定义View还是自定义ViewGroup都无非三个方法:onMeasure(), onLayout() 和 onDraw()方法。
其实在面试中我们经常被问到:了解自定义View吗?
面试者:了解,自定义View需要重写:onMeasure,onLayout, onDraw三个方法。

没然后了,其实这样说不错,但是呢,如果你是工作3-5年的开发者,很显然,这个回答是不合格的,因为你需要回答出这三个方法是怎么调用的?内部的绘制流程是什么? 其中必然会牵扯出:ViewRootImpl,WMS,DecorView等等,这个问题,专栏之后博客会讲到。

现在我们需要明白的是三个方法:onMeasure, onLayout,onDraw。

View类简介

我们先看下View的源码:

// 如果View是在Java代码里面new的,则调用第一个构造函数
   public CarsonView(Context context) {
          super(context);
      }
  
  // 如果View是在.xml里声明的,则调用第二个构造函数
  // 自定义属性是从AttributeSet参数传进来的
      public  CarsonView(Context context, AttributeSet attrs) {
          super(context, attrs);
      }
  
  // 不会自动调用
  // 一般是在第二个构造函数里主动调用
  // 如View有style属性时
      public  CarsonView(Context context, AttributeSet attrs, int defStyleAttr) {
          super(context, attrs, defStyleAttr);
      }
  
      //API21之后才使用
      // 不会自动调用
      // 一般是在第二个构造函数里主动调用
      // 如View有style属性时
      public  CarsonView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
          super(context, attrs, defStyleAttr, defStyleRes);
      }

我们在XML中调用的是第二个构造函数,将XML解析成了Java代码。

PS:这里面会延伸一个问题:XML是怎么转化成Java代码的呢? 这个问题之后专门博客讲述,感兴趣的小伙伴,可以看下:LayoutInflater这个类。

View的视图结构

在这里插入图片描述
其实关于View视图的结构,了解Activity启动模式的应该是清楚的,或者了解Dialog的也是清楚的,应为它们都是创建了PhoneWindow,在Activity创建的时候,创建了一个PhoneWindow并且创建了相应的DecorView,
其实这里的PhoneWindow是虚拟的概念,它并不是一个真正的View,而DecorView才是真正的View,而DecorView才是真正的View,而DecorView内部是一个线性布局(不同的API不同的布局)。不懂DecorView构造的看下:Android进阶3:Activity源码分析(3) —— setContentView分析(8.0)。

其实上面一坨东西,就是想说我们自己的布局就添加在DecorView中,DecorView就是顶级的View。PhoneWindow并不是一个真正的View,是个虚拟概念。记住这些,就## 标题完了。

  1. PhoneWindow是Android系统中最基本的窗口系统,继承自Windows类,负责管理界面显示以及事件响应。它是Activity与View系统交互的接口
  2. DecorView是PhoneWindow中的起始节点View,继承于View类,作为整个视图容器来使用。用于设置窗口属性。它本质上是一个FrameLayout
  3. ViewRoot在Activtiy启动时创建,负责管理、布局、渲染窗口UI等等

Android中的坐标系

其实Android中的坐标系,和我们正常中数学的坐标是不一样的:
在这里插入图片描述

屏幕的左上角是屏幕的中心,X轴从左到右,是逐渐增大的,Y轴从上到下是逐渐增大的。角度顺时针是逐渐增大的。

View位置(坐标)描述

View的位置是有四个顶点决定的,而都是相对于父布局的。

  • Top:子View上边界到父view上边界的距离
  • Left:子View左边界到父view左边界的距离
  • Bottom:子View下边距到父View上边界的距离
  • Right:子View右边界到父view左边界的距离

MotionEvent中,getX和getRawX的区别

getX是相对于自己的,getRawX是相对于的父布局。如下图:

MeasureSpec

其实这东西,网上很多博客,关于概念我就说一下:是一个32位的int值,高两位代表测量模式,低30位代表测量大小。

MeasureSpec有三种测量模式:

  • UNSPECIFIED
    父控件不对你有任何限制,你想要多大给你多大,想上天就上天。这种情况一般用于系统内部,表示一种测量状态。(这个模式主要用于系统内部多次Measure的情形,并不是真的说你想要多大最后就真有多大)
  • EXACTLY
    父控件已经知道你所需的精确大小,你的最终大小应该就是这么大。
  • AT_MOST
    你的大小不能大于父控件给你指定的size,但具体是多少,得看你自己的实现。

我重点说一下它的用处,在项目中,我们可能会发现一个问题:有时候我们设置View为Match_parent,但是不起作用。 我想这个问题肯定是都遇见过的吧,今天我们掰扯一下这个东西。

既然不起作用,那么肯定是还有一个因素影响了测量的大小。 没错,就是MeasureSpec这东西,先说结果吧,一个View的最终大小是有:父布局的MeasureSpec和自己的LayoutParam决定的。
在这里插入图片描述

那么是怎么影响的呢? 看下ViewGroup的getChildMeasureSpec方法:

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    	//获取父布局的测量模式以及大小
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
		//这里的计算父布局的Padding以及自己的Margin
        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // 当父布局给的测量模式是:精确模式时,
        case MeasureSpec.EXACTLY:
        	//这时候根据子View在XML设置的大小而确定最终的大小
            if (childDimension >= 0) { // XML中设置的是大于0 的值:比如:100dp
            	//此时子View大小就是:100dp
                resultSize = childDimension;
                //测量模式时精准模式
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                //此时是父布局有多大,子布局你可以这么大,但是不能超过父布局
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        //父布局的测量模式是:AT_MOST
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                //此时父布局都不知道自己有多大,所以给了子View的测量模式也是AT_MOST,但是子view再大也不能大过父布局,
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

上述代码的各种情况,希望各位仔细阅读,至于为什么分析这个方法,等说到自定义ViewGroup布局的时候会说到,这里先提一下。 通过上述代码我们可以得到如下图:
在这里插入图片描述
上图的纵列是子View在XML中定义的大小,横列是父布局的测量模式。 因此我们的在XML中定义的大小,并不是最终的大小,而是自己想要的大小,真正的大小,还得依赖父布局的测量模式。通过测量之后,才能知道自己的确切大小,而测量的标准就是:上图。

以上就是我觉得自定义View需要掌握的很重要的基本知识点,下篇博客分享:View树的测量绘制

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值