Android 自定义 View 基本规范

本文详细介绍自定义Android控件的全过程,包括属性定义、控件测量、绘制及交互,适合初学者入门。

本人只是Android小菜一个,写技术文档只是为了总结自己在最近学习到的知识,从来不敢为人师,如果里面有些不正确的地方请大家尽情指出,谢谢!

1. 概述

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

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

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

2. 控件属性

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

    <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类型如下表:

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

在这里只定义了一个简单的color类型的属性,其他类型的属性大家可自行定义,方法是类似的。

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_MOSTwrap_content父控件提供一个最大值,子控件不要超过父控件提供的尺寸大小。
EXACTLYmatch_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的基本规范,其中包括属性、测量、绘制、交互,大家可以把它当做自定义控件的入门知识,但相信在了解了这些基本规范后,再加上勤奋的练习,以后也能定义出功能复杂效果炫酷的控件,一起加油!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值