通过直接继承View来实现自定义View的方法简介

  在设计自定义View时,可以选择直接继承View、直接继承ViewGroup、继承特定View、继承特定ViewGroup等方式。本文通过实现一个在屏幕上绘制矩形的自定义View,介绍通过继承View来实现自定义View的方法。

1. 创建继承View的自定义View

  首先创建BlockView直接继承自View:

public class BlockView extends View {
}

2. 配置自定义属性

  添加自定义可以让自定义View更加便于使用。首先在/res/values/下新建attrs.xml文件,内容如下:

<resources>
    <declare-styleable name="BlockView">
        <attr name="blockColor" format="color" />
    </declare-styleable>
</resources>

【完整代码】

其中BlockView 是自定义View的名称,为它定义了一个名为blockColor 属性,格式为color 。

3. 自定义View的构造器

  自定义属性会通过构造器以AttributeSet 的形式传递给自定义View,需要在自定义View的构造器中过去自定义属性,并对View进行配置。

private int mColor = DEFAULT_COLOR;

public BlockView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    TypedArray a = context.getTheme().obtainStyledAttributes(
            attrs,
            R.styleable.BlockView,
            0, 0);

    try {
        mColor = a.getColor(R.styleable.BlockView_blockColor, DEFAULT_COLOR);
        Log.v(LOG_TAG, "BlockView(): mColor = " + mColor);
    } finally {
        a.recycle();
    }

    init();
}

这里通过obtainStyledAttributes() 从构造器参数attrs 中取出R.styleable.BlockView 的相关属性,然后读取R.styleable.BlockView_blockColor 属性并保存到本地。R.styleable.BlockView_blockColor 对应了在前面定义的blockColor 属性。

4. 自定义View的测量

  在View本身实现的测量流程中,并没有对当其宽或高设置为wrap_content的情况做处理,wrap_content与match_parent相同。对于直接继承View的自定义View,需要根据自身情况,在测量的时候加入对wrap_content的处理。

private static final int DEFAULT_HEIGHT = 200;
private static final int DEFAULT_WIDTH = 200;

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(DEFAULT_WIDTH, DEFAULT_HEIGHT);
    } else if (widthMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(DEFAULT_WIDTH, heightSize);
    } else if (heightMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(widthSize, DEFAULT_HEIGHT);
    }
}

这里对宽和高MeasureSpec的Mode进行判断:对于宽和高都是MeasureSpec.AT_MOST ,即wrap_content的情况,就把宽度和高度都设置为默认值;如果宽或高其中之一为MeasureSpec.AT_MOST ,就把另外一个设置为默认值。这样就可以在宽或高设置为wrap_content时避免占用多余的空间。

5. 自定义View的绘制

  对于直接继承自View的自定义View,默认实现下padding属性也是无法生效的,需要在绘制时考虑各个padding的值。

private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    final int paddingLeft = getPaddingLeft();
    final int paddingRight = getPaddingRight();
    final int paddingTop = getPaddingTop();
    final int paddingBottom = getPaddingBottom();

    int width = getWidth() - paddingLeft - paddingRight;
    int height = getHeight() - paddingTop - paddingBottom;

    Log.v(LOG_TAG, "onDraw(): mPaint.getColor() = " + mPaint.getColor());
    canvas.drawRect(paddingLeft, paddingTop, paddingLeft + width, paddingTop + height, mPaint);
}

这里先获取了四个方向的padding,然后在drawRect() 时,设置所画矩形的左上角坐标为(paddingLeft, paddingTop),右下角坐标为(paddingLeft + width, paddingTop + height)。

6. 为属性添加getter和setter

  为属性添加getter和setter可以动态地对自定义属性进行设置,对于blockColor 这个自定义属性,getter比较简单,直接返回即可:

public int getBlockColor() {
    return mColor;
}

但setter的实现需要额外留意,通常我们希望调用setter之后,set的效果可以立即反应在界面上,比如TextView的setText() 可以立即将所配置的字符串显示到TextView。这里所要实现的setBlockColor() 也应能够立即改变BlockView中矩形的颜色。
public void setBlockColor(int color) {
    Log.v(LOG_TAG, "setBlockColor(): color = " + color);
    mColor = color;
    mPaint.setColor(mColor);
    invalidate();
}

这里额外调用了invalidate() 来把整个自定义View无效化,这样之后自定义View的onDraw() 会被再次调用,使用新的参数重新绘制,使得setBlockColor() 可以立即改变当前BlockView的颜色。

【完整文件】

7. 添加触摸事件处理

  可以通过重写onTouchEvent() 来实现自定义View对触摸事件的处理。这里为BlockView添加一个水平滑动的手势操作,可以通过水平滑动改变lockView的透明度。实现方法很简单,只要在onTouchEvent() 中添加对MotionEvent.ACTION_MOVE 的处理即可。

@Override
public boolean onTouchEvent(MotionEvent event) {
    final int MAX_ALPHA = 255;
    float x = event.getX();
    float y = event.getY();

    switch (event.getAction()) {
        case MotionEvent.ACTION_MOVE: {
            float dx = x - mPreviousX;
            float dy = y - mPreviousY;
            if (Math.abs(dx) >= Math.abs(dy)) {
                int newAlpha = Color.alpha(mColor) + (int) (dx / getWidth() * MAX_ALPHA);
                if (newAlpha < 0) {
                    newAlpha = 0;
                } else if (newAlpha > MAX_ALPHA) {
                    newAlpha = MAX_ALPHA;
                }
                Log.v(LOG_TAG, "onTouchEvent(): ACTION_MOVE newAlpha = " + newAlpha);
                mColor = Color.argb(
                        newAlpha,
                        Color.red(mColor), Color.green(mColor), Color.blue(mColor));
                setBlockColor(mColor);
            }
        }
        default: {
            break;
        }
    }

    mPreviousX = x;
    mPreviousY = y;

    return true;
}

【完整代码】

当发生MotionEvent.ACTION_MOVE 时,获取当次事件的水平和垂直位移量dx 和dy 。若Math.abs(dx) >= Math.abs(dy) ,即当前为水平位移,则根据水平位移量修改颜色的alpha通道,改变BlockView的透明度。

8. 自定义View的使用

  自定义View的使用和一般的View相同,在布局文件中直接加入即可:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <com.nex3z.examples.simplecustomview.BlockView
        android:id="@+id/block"
        android:layout_gravity="center_horizontal"
        android:layout_width="200dp"
        android:layout_height="100dp"
        android:paddingBottom="16dp"
        app:blockColor="@android:color/darker_gray"/>

</LinearLayout>

【完整文件】

这里自定义属性blockColor 使用了前缀app ,这需要添加schemas声明:

xmlns:app="http://schemas.android.com/apk/res-auto"

9. 完整代码

  完整代码可以在这里找到。