自定义 View - 基础

作者: Arnold_J | 来源:发表于2017-12-11 08:47 被阅读54次

    虽然安卓系统自带了许多功能丰富,种类多样的控件,但是要想完全实现各个场景中,UI 给出的炫酷效果,这些控件却是远远不够的。为此,自定义 View 成了每个工程师的必备技能,今天先主要看基础内容。


    一、坐标系

    1.屏幕坐标系

    下图,画得不是很好看。。

    • 数学坐标系中,一般以水平方向为 X 轴,且向右为正方向,以垂直方向为 Y 轴,且向上为正方向,中心是原点。
    • 但是在手机屏幕上, Y 轴的正方向向下,且原点在左上角。
      注意:由于整个坐标系是相对于 X 轴做了对称的,所以变化的不只是 y 轴的正方向,还有象限和角度。
    效果图.png

    2.子 View 坐标系

    获取子 View 位置的方法的返回值都是相对于父容器的,相关方法如下。

    getTop()        //获取 view 上边缘到父容器上端的距离
    getBottom()     //获取 view 下边缘到父容器上端的距离
    getLeft()       //获取 view 左边缘到父容器左端的距离
    getRight()      //获取 view 右边缘到父容器左端的距离
    

    下面以最常见的例子做图解,其中白色部分为父容器,蓝色部分为子 View。需要注意的是,由于 View 的测量方法在 onCreate 之前是不会调用的,所以上面的方法,如果在 onCreate 中调用,返回的都是

    效果图-g.png

    二、自定义 View 的分类

    在我看来,只要对于原生的控件部分功能进行了改动(不管是测量方式的改变,暴露出数据接口,亦或是整体的自我绘制等等),就是工程师有意识的在做适合当前工程的自定义。
    总体来说,自定义 View 主要可以分成这样几个种类:

      1. 继承原生控件,并对部分方法重写改造或者增加新功能。
      1. 继承 ViewGroup,并对原生控件进行组合以达到复用的目的,通常这样的自定义 View 会有自己的功能。例如验证码按钮
      1. 继承 View,自己绘制和测量当前的视图。例如图表等特殊的视图。当然,也有控制触摸事件的,例如手势键盘,涂鸦板等。

    以上三种,一二比较容易,可以很快上手,而第三种,在图形绘制测量和事件分发上,需要一定的理解。之后应该都会做 demo 进行练习。

    三、View 的绘制流程

    简单地利用 Android Studio 画了一个图。


    Studio Test.png

    不过上面的图,并不十分准确。在视图变化的时候,有两种可能,上面的箭头指向重新调用 onDraw() 方法,而另一种情况并非如此。
    我们一共有两种方法来通知 View 树刷新视图。

    • invalidate() -----> onDraw()
    • requestLayout() -----> onMeasure()

    其中调用 invalidate 会使 View 重新绘制,而调用 requestLayout 会使 View 重新测量。

    四、自定义 View 构造器

    如果现在开始着手创建一个 View 的子类,我们会发现它一共有四个构造方法:


    效果图-g.png

    这里最常用的是第一个和第二个构造方法,以下为两个构造器的参数和基本说明。

    //一个参数的构造器在代码中动态构建的时候会被调用
    public TView(Context context) {
        super(context);
    }
    
    //两个参数的构造器在初始化 xml 中的控件时会被调用
    public TView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
    

    我们以 ImageView 的构造器为例来看看自定义 View 的构造器需要做哪些事情:

    public ImageView(Context context) {
        super(context);
        initImageView();
    }
    
    public ImageView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }
    
    public ImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }
    
    public ImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
            int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    
        initImageView();
        
        //...初始化布局属性
    }
    

    从上面我们可以看到,无论调用哪个构造器,都会先调用 initImageView() 方法,通常版本适配和默认属性赋值会在这里进行。后面两个参数、三个参数的构造器实际上调用的都是四个参数的构造器,这里除了会调用 initImageView() 方法外,还会利用 attrs 获取到 xml 中的属性,方便之后图形绘制和测量时更准确地表达我们的意图。

    五、测量自定义 View 的大小

    上面第三点的绘制流程中,我们可以看到,在调用构造器后,就到了测量方法 --- onMeasure()

    onMeasure 方法给我们提供了两个参数:widthMeasureSpec heightMeasureSpec 。在了解这两个参数之前,我们需要先了解 MeasureSpec

    MeasureSpec 是 View 的内部类,它把 32 位的整型数据,分为两部分,前两位用于存储 View 的填充类型(match_parent | wrap_content),后面 30 位用于存储数值大小。存储的类型和数值可以利用掩码和移位拆分,这里我们通常用 MeasureSpec.getMode 和 MeasureSpec.getSize 方法拆分获取。

    文字说的总是有限,为了表示得更清晰一点,我画了一个图表,其中 ModeSize 应该是连在一起的一个数值,为方便观察才分在两个格子中。

    Mode Size 备注
    10 000000000000000000000000000001 val=2^31+1 ,[mode]-[wrap_content]-[MeasureSpec.AT_MOST]
    01 000000000000000000000000000001 val=2^30+1 ,[mode]-[match_parent]-[MeasureSpec.EXACTLY]

    对于 onMeasure 中的两个参数,我们可以通过下面的方法分别获取 ModeSize

    int specWidthMode = MeasureSpec.getMode(widthMeasureSpec);
    int specWidthSize = MeasureSpec.getSize(widthMeasureSpec);
    int specHeightMode = MeasureSpec.getMode(heightMeasureSpec);
    int specHeightSize = MeasureSpec.getSize(heightMeasureSpec);
    

    注意:

    • 事实上,不管 ModeMeasureSpec.AT_MOST 还是 MeasureSpec.EXACTLY 得到的 Size 都是充满根布局的,因此在测量的时候,如果 ModeMeasureSpec.AT_MOST 我们需要自己测量对应的尺寸。
    • 对于测量好的尺寸,需要调用 setMeasuredDimension( resultWidth,resultHeight ) 使测量值生效

    感谢 :GcsSloop 自定义 View 系列

    谢谢观赏

    相关文章

      网友评论

        本文标题:自定义 View - 基础

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