通过直接继承View来实现自定义View的方法简介
Author: nex3z
2016-05-07
在设计自定义View时,可以选择直接继承View、直接继承ViewGroup、继承特定View、继承特定ViewGroup等方式。本文通过实现一个在屏幕上绘制矩形的自定义View,介绍通过继承View来实现自定义View的方法。
1. 创建继承View的自定义View
首先创建BlockView直接继承自View:
public class BlockView extends View {
public class BlockView extends View {
}
public class BlockView extends View {
}
2. 配置自定义属性
添加自定义可以让自定义View更加便于使用。首先在/res/values/下新建attrs.xml文件,内容如下:
<declare-styleable name="BlockView">
<attr name="blockColor" format="color" />
<resources>
<declare-styleable name="BlockView">
<attr name="blockColor" format="color" />
</declare-styleable>
</resources>
<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(
mColor = a.getColor(R.styleable.BlockView_blockColor, DEFAULT_COLOR);
Log.v(LOG_TAG, "BlockView(): mColor = " + mColor);
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();
}
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;
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);
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);
}
}
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);
protected void onDraw(Canvas 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);
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);
}
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() {
public int getBlockColor() {
return mColor;
}
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);
public void setBlockColor(int color) {
Log.v(LOG_TAG, "setBlockColor(): color = " + color);
mColor = color;
mPaint.setColor(mColor);
invalidate();
}
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 的处理即可。
public boolean onTouchEvent(MotionEvent event) {
final int MAX_ALPHA = 255;
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);
} else if (newAlpha > MAX_ALPHA) {
Log.v(LOG_TAG, "onTouchEvent(): ACTION_MOVE newAlpha = " + newAlpha);
Color.red(mColor), Color.green(mColor), Color.blue(mColor));
@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;
}
@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:layout_gravity="center_horizontal"
android:layout_width="200dp"
android:layout_height="100dp"
android:paddingBottom="16dp"
app:blockColor="@android:color/darker_gray"/>
<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>
<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"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:app="http://schemas.android.com/apk/res-auto"
9. 完整代码
完整代码可以在这里找到。