Android 自定义 View 基本规范

本人只是Android小菜一个,写技术文档只是为了总结本身在最近学习到的知识,历来不敢为人师,若是里面有些不正确的地方请你们尽情指出,谢谢!java

1. 概述

在进行Android应用开发时,能够选择系统提供的各式各样的控件,但有时原生控件在功能和效果上并不能知足需求,这时就要求必须根据实际需求来定义新的控件,能够经过继承View,也能够继承某些已经存在的原生控件,来实现自定义控件。本文将选择直接继承View来实现一个最简单的控件。android

自定义控件包含了Android中和View相关的不少知识,学习自定义控件也能帮组学习和理解相关知识。canvas

要想自定义出功能强大效果酷炫的控件,要求必须对View体系有深刻的理解,在这点我还差的不少,因此本文并不能教你们怎样去实现这样的控件。本文只是从自定义View的基本规范方面,跟你们探讨下在自定义一个控件的过程当中,有哪些方面须要注意的,或者说有哪些功能是须要实现的,主要包括:控件属性控件测量控件绘制控件交互app

2. 控件属性

当咱们在xml中定义控件的时候,确定须要对控件具备的某些属性进行设置,例如宽高背景颜色文本等等,下面是在使用 TextView的一个示例:ide

<TextView android:id="@+id/main_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="#FF0000" android:text="Hello World!" />
复制代码

在自定义控件的时候,为了可以让用户灵活定义控件的某些特性,也须要经过属性的方法把用户指定的值传入控件,而不是在控件内部使用预约义的值,这也就要求在自定义控件的时候使用到自定义属性。函数

2.1 定义属性

自定义属性须要在res/values/attrs.xml里面定义,若是这个文件不存就本身建立一个。结合一个例子进行介绍:学习

<declare-styleable name="custom_view">
        <attr name="default_color" format="color"></attr>
    </declare-styleable>
复制代码

declare-styleable name="custom_view"指定了自定义属性集合的name信息,这个值能够是任意值,但通常为了方面使用都是直接使用自定义控件的名字。测试

<attr name="default_color" format="color"></attr>指定了自定义属性集合里的具体属性和该属性对应的类型,本例中使用的是color类型,代表这个属性须要的是一个颜色值,可以支持的format类型以下表:this

类型 含义 取值
boolean 布尔类型 只能是truefalse
string 字符串类型 任意字符串值
integer 整数类型 只能是整数
float 浮点数类型 只能是浮点和整型
fraction 百分比类型 只能以%结尾
color 颜色类型 能够是颜色值或者指向color的资源
dimension 尺寸类型 能够是具体尺寸值或指向尺寸的资源
reference 引用类型 只能是指向某一资源的ID
enum 枚举类型 只能是定义的枚举值
flag 位标志类型 只能是定义的位值

在这里只定义了一个简单的color类型的属性,其余类型的属性你们可自行定义,方法是相似的。spa

2.2 使用属性

在定义了属性后,能够直接在xml使用这些属性,使用方法和原生控件属性同样,只需根据不一样类型设置值便可。在上面定义一个属性default_color,如今就能够在xml里使用了:

<com.test.androidtest.CustomView android:id="@+id/custom_view" android:layout_width="wrap_content" android:layout_height="wrap_content" app:default_color="#ffff00"/>
复制代码

须要注意的是,在这里使用了新的命名空间app,其声明是xmlns:app="http://schemas.android.com/apk/res-auto",若是你们使用的Android Studio,这个命名空间是自动添加的,无须自行处理。

xml使用了自定义属性后,在建立这个控件的时候,就会把这些属性传入控件,在控件内部就能够获取并使用到该属性值了。

// 在代码里经过 new 方式建立控件实例时使用
    public CustomView(Context context) {
        super(context);
    }

    // 在 xml 定义控件时使用,会获取到定义的属性
    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 获取定义的属性集合
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.custom_view);
        // 获取特定的属性值
        if (array != null) {
            default_color = array.getColor(R.styleable.custom_view_default_color, -1);
        }
    }
复制代码

上述代码演示了如何在控件内部获取自定义属性,在成功获取到属性值后就能够利用该值进行后续的控件绘制工做了。

须要注意的是在自定义控件是须要实现两个不一样的构造函数,分别对应于在javaxml的使用场景。

2.3 修改属性

在前面已经讲了如何定义和在控件内部获取属性,可是咱们知道有时控件属性是须要根据不一样的场景进行修改的,而在xml只能指定属性的初始值,没法进行不断的修改。这就要求必须针对有些属性提供取值器设值器,也就是常说的gettersetter,这里之因此说是“有些属性”,是由于并非全部属性都须要支持动态修改的。

仍是针对前面定义的default_color属性,如今对其设置取值器设值器:

public int getColor() {
        return default_color;
    }

    public void setColor(int color) {
        default_color = color;
        // 调用 onDraw,从新刷新控件.
        invalidate();
    }
复制代码

取值器比较简单,只要返回当前属性值就能够了。设值器除了要更新当前属性值外,更重要的是,在更新完当前属性值外,要对当前的控件进行第二次的绘制,以更新控件状态,这里直接调用invalidate(),它会把当前view标志为DIRTY,在下一帧绘制时调用控件的onDraw()方法完成对控件的更新。设置了属性的gettersetter后,就能够在使用控件的时候,动态获取和修改属性值了。

3. 控件测量

测量的目的是要肯定控件在显示的时候具体的显示尺寸,你们可能会奇怪:不是在xml已经指定了控件大小了吗?为何还要再测量一次呢?这是由于在xml指定控件大小的时候有不一样的方式,每种方式最终致使分配给控件的尺寸也不同。

指定尺寸方式 含义
wrap_content 根据控件具体内容分配尺寸
match_parent 根据父控件剩余大小给控件分配尺寸
具体数值 根据给定的数值进行分配控件尺寸

为了可以测了控件,须要实现onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法,先看下View中该方法声明:

/** * Measure the view and its content to determine the measured width and the * measured height. This method is invoked by {@link #measure(int, int)} and * should be overridden by subclasses to provide accurate and efficient * measurement of their contents. */
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
复制代码

这里提到:onMeasure是用来决定控件的宽高信息的,为了可以提供更准确和高效的控件测量,子类最好要重写这个方法,因此自定义控件最好也要实现这个方法。

这里的参数widthMeasureSpecheightMeasureSpec表明是什么意思?是否是就是控件的宽高呢?固然不是,若是它们表示的就是控件宽高就不须要咱们继续测量了。widthMeasureSpecheightMeasureSpec里面都包含了两个信息:sizemode,其中size表示的是父控件告诉子控件的建议宽高,mode表示当前的测量模式,具体有AT_MOST,EXACTLYUNSPECIFIED,其含义以下:

测量模式 尺寸模式 含义
AT_MOST wrap_content 父控件提供一个最大值,子控件不要超过父控件提供的尺寸大小。
EXACTLY match_parent或者具体值 父控件提供一个确切值,子控件能够直接使用这个尺寸来设置大小。
UNSPECIFIED 暂无 父控件不提供,子控件能够任意设置大小。

从上面的表格能够看到:UNSPECIFIED通常是遇不到的,而AT_MOSTEXACTLY都会提供一个建议值,能够根据这个值和测试模式来肯定子控件大小。

本文中的自定义控件的onMeasure以下:

@Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 使用宽高中的最小值把宽高设置为等值,由于控件的最终目的是画一个圆。
        int dimension = Math.min(getSize(widthMeasureSpec), getSize(heightMeasureSpec));
        // 设置最终的宽高信息,若是少了这步,获得的宽高将没法应用到控件中。
        setMeasuredDimension(dimension, dimension);
    }

    private int getSize(int measureSpec) {
        int mode = MeasureSpec.getMode(measureSpec);
        int size = MeasureSpec.getSize(measureSpec);
        // EXACTLY 和 AT_MOST 直接使用父控件提供的宽高信息。
        switch (mode) {
            case MeasureSpec.EXACTLY:
            case MeasureSpec.AT_MOST:
                return size;
            default:
                // UNSPECIFIED 返回预约义的宽高信息,通常不会遇到。
                return mMeasureWidthHeight;
        }
    }
复制代码

在本文的自定义控件中,最终的目的是要显示一个圆形,在onMeasure里设置了等值宽高,而在获取宽高时针对AT_MOSTEXACTLY两种状况都直接使用了父控制传递过来的尺寸。固然这只是一种最简单的状况,当要自定义高能复杂的控件时,宽高的肯定须要结合的因素会更多,计算也会更复杂。

4. 控件绘制

测量控件后就能够知道控件的最终宽高信息,这时须要作的就是进行实际的绘制,只有经过绘制,控件才能真正地显示出来。绘制控件须要实现onDraw(Canvas canvas)方法,和onMeasure同样,先看下在View中的声明:

/** * Implement this to do your drawing. * * @param canvas the canvas on which the background will be drawn */
    protected void onDraw(Canvas canvas) {
    }
复制代码

能够发现:View并无实现onDraw,这是由于View 是全部控件的父类,但其自己并非一个能够直接显示的控件,这就要求全部须要显示的控件都必须实现这个方法,它的参数是Canvas类,就是常说的画布。为了显示控件,咱们须要作的就是用PaintCanvas上把须要显示的图像画出来,正如咱们在电脑上常常在画图软件上画图同样。

如今看下本例中自定义控件的onDraw(Canvas canvas)的实现:

@Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 初始化画笔,这个对象须要在控件初始化时初始化,这里正常不会走到。
        if (mPaint == null) {
            mPaint = new Paint();
        }
        // 设置画笔的颜色。
        mPaint.setColor(default_color);

        // 在画布上画出一个圆形。
        int radius = getMeasuredWidth() / 2;
        canvas.drawCircle(getLeft() + radius, getTop() +radius, radius, mPaint);
    }
复制代码

上面的示例代码只是实现一个根据用户传入的颜色来进行画圆功能,其效果以下:

示例效果

Canvas除了画圆,还能够画出更多更复杂的图形,Paint也能够有更多的控制,其你们自行查阅相关API

5. 控件交互

经过上面的几个过程,已经能在界面上显示自定义控件了,但显示不是最终的目的,真正的目的仍是但愿能与控件进行交互,最重要的是可以响应touch事件,接下来就经过实现一个简单的随手指移动功能:

private int mLastX;
    private int mLastY;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastX = x;
                mLastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                int offsetX = x - mLastX;
                int offsetY = y - mLastY;
                //从新放置新的位置
                layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
                break;
            default:
                break;
        }
        return true;
    }
复制代码

此次对onTouchEvent的重写能够实现让控件随着手指会移动,固然这里只是一个简单演示,还存在一些问题,好比控件会被移出屏幕以外,这是由于在移动时并无判断当前控件的位置,把这个条件加上就能够保证控件只在界面以内移动。

6. 总结

本文经过一个简单的自定义圆形的例子,大体讲解了自定义View的基本规范,其中包括属性、测量、绘制、交互,你们能够把它当作自定义控件的入门知识,但相信在了解了这些基本规范后,再加上勤奋的练习,之后也能定义出功能复杂效果炫酷的控件,一块儿加油!

相关文章
相关标签/搜索