通过直接继承View来实现自定义View的方法简介
在设计自定义View时,可以选择直接继承View、直接继承ViewGroup、继承特定View、继承特定ViewGroup等方式。本文通过实现一个在屏幕上绘制矩形的自定义View,介绍通过继承View来实现自定义View的方法。
Contents
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. 完整代码
完整代码可以在这里找到。