概述:在日常开发者自定义绘图是很常见的一个技能,有时候我们需要自己实现一个不是特别复杂的视图。则需要通过Android提供的API进行计算绘制等。本篇的目的就是通过简单的例子对Android绘图进行简单的认识。注意,本例中没有对其做任何封装,知识简单示范。

对于一般自定义View,我们都是新建一个类,然后继承自View。例如本例中ProgressView.java

public class ProgressView extends View {

public ProgressView(Context context) {
super(context);
}

public ProgressView(Context context, AttributeSet attrs) {
super(context, attrs);
}

public ProgressView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

@Override
protected void onDraw(Canvas canvas) {

}
}

上述代码是平时开发中再熟悉不过的了,然后我们在xml布局中使用它。这里我添加了一个背景色方便绘制的时候进行调试

<com.jinlin.progressview.ProgressView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#ed7612"
android:layout_centerInParent="true"/>

那么我们接下来就在onDraw方法中进行绘制操作。首先要知道,绘制任何东西都是需要画笔对象的也就是我们的Paint,初始化工作代码如下

public class ProgressView extends View {

/**
* 圆形画笔宽度
*/
private float mProgressStrokeWidth = 5;
/**
* 文字画笔欢度
*/
private float mTextStrokeWidth = 3;
/**
* 最大进度
*/
private int mMaxProgress;
/**
* 当前进度
*/
private int mCurrentProgress;
/**
* 绘制区域
*/
private RectF mRectF;
/**
* 画笔对象
*/
private Paint mPaint;
/**
* 上下文对象
*/
private Context mContext;
/**
* 提示信息1
*/
private String mTxtHint1;
/**
* 提示信息2
*/
private String mTxtHint2;

public ProgressView(Context context) {
super(context);
init(context);
}

public ProgressView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}

public ProgressView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}

private void init(Context context) {
mContext = context;
mPaint = new Paint();
mRectF = new RectF();
}

@Override
protected void onDraw(Canvas canvas) {
int width = this.getWidth();
int height = this.getHeight();
if (width != height) {
int min = Math.min(width, height);
width = min;
height = min;
}
// 画最底层
mPaint.setAntiAlias(true); // 设置抗锯齿
mPaint.setColor(Color.rgb(0xe9, 0xe9, 0xe9)); // 设置全圆弧颜色
mPaint.setStrokeWidth(mProgressStrokeWidth);
mPaint.setStyle(Style.STROKE);
canvas.drawColor(Color.TRANSPARENT);
mRectF.left = mRectF.top = mProgressStrokeWidth / 2;
mRectF.right = width - mProgressStrokeWidth / 2;
mRectF.bottom = height - mProgressStrokeWidth / 2;
canvas.drawArc(mRectF, 0, 360, false, mPaint);

// 画第二层
mPaint.setColor(getResources().getColor(R.color.colorPrimary));
canvas.drawArc(mRectF, 0 , 112, false, mPaint);
}
}

此时我们可以看到效果为

截图

我们看到此时起点是有右边开始,此时查看代码得知我们的起点是由0开始绘制的

canvas.drawArc(mRectF, 0 , 112, false, mPaint);

那么如果我们需要有顶部开始怎么做呢?我们可以知道顺时针为正,那么顶部为右边逆时针90°,如果换种思维,顶点也是顺时针270°。那我们试一试,代码说话

canvas.drawArc(mRectF, -90 , 112, false, mPaint);
canvas.drawArc(mRectF, 270 , 112, false, mPaint);

上面两端代码实现效果其实是一致的

截图

接下来就是绘制文字的部分,我们需要将进度文字绘制到视图的正中央,那么我们可以先把辅助中心线绘制出来。后期进入生产阶段可以去除调试代码部分或者将代码放入到if (isInEditMode())代码块里

if (isInEditMode()) {
// 绘制辅助线
mPaint.setStrokeWidth(1);
mPaint.setColor(Color.LTGRAY);
canvas.drawLine(0, width / 2, height , width /2 , mPaint);
canvas.drawLine(width / 2, 0, width / 2, height, mPaint);
}

由于Android中对文字的绘制时一个复杂的处理过程,所以这里不做详细说明,将单独哪一篇博文来说明文字应该如何绘制。

// 画进度文字
mPaint.setStrokeWidth(mTextStrokeWidth);
String text = 88 + "%";
int textHeight = height / 4;
mPaint.setTextSize(textHeight);
FontMetrics fontMetrics = mPaint.getFontMetrics();
float baseline = (mRectF.bottom + mRectF.top - fontMetrics.bottom - fontMetrics.top) / 2;
int textWidth = (int) mPaint.measureText(text, 0, text.length());
mPaint.setStyle(Style.FILL);
canvas.drawText(text, mRectF.centerX() - textWidth / 2, baseline, mPaint);

由效果图我们可以看见,文字已经居中了,接下来分别在视图的上三分之一和下三分之一处绘制提示信息,原理同样与上述代码类似

截图

if (!TextUtils.isEmpty(mTxtHint1)) {
mPaint.setStrokeWidth(mTextStrokeWidth);
text = mTxtHint1;
textHeight = height / 8;
mPaint.setTextSize(textHeight);
fontMetrics = mPaint.getFontMetrics();
mPaint.setColor(Color.rgb(0x99, 0x99, 0x99));
textWidth = (int) mPaint.measureText(text, 0, text.length());
mPaint.setStyle(Style.FILL);
baseline = height / 4 - (fontMetrics.bottom + fontMetrics.top) / 2;
canvas.drawText(text, width / 2 - textWidth / 2, baseline, mPaint);
}

if (!TextUtils.isEmpty(mTxtHint2)) {
mPaint.setStrokeWidth(mTextStrokeWidth);
text = mTxtHint2;
textHeight = height / 8;
mPaint.setTextSize(textHeight);
fontMetrics = mPaint.getFontMetrics();
textWidth = (int) mPaint.measureText(text, 0, text.length());
mPaint.setStyle(Style.FILL);
baseline = 3 * height / 4 - (fontMetrics.bottom + fontMetrics.top) / 2;
canvas.drawText(text, width / 2 - textWidth / 2, baseline, mPaint);
}

最后我们来看效果图

截图

然后我们对部分属性设置访问方法,或者再新增一些自定义属性就可以完成一个简单的自定义View。例如

<declare-styleable name="ProgressView">
<attr name="progress" format="integer"/>
<attr name="textHint" format="string"/>
<attr name="hint" format="string"/>
<attr name="strokeColor" format="color" />
<attr name="progressColor" format="color" />
</declare-styleable>

然后在View中获取自定义属性,进行配置,完整代码如下

public class ProgressView extends View {

/**
* 圆形画笔宽度
*/
private float mProgressStrokeWidth = 5;
/**
* 文字画笔欢度
*/
private float mTextStrokeWidth = 3;
/**
* 最大进度
*/
private int mMaxProgress = 100;
/**
* 当前进度
*/
private int mCurrentProgress;
/**
* 绘制区域
*/
private RectF mRectF;
/**
* 画笔对象
*/
private Paint mPaint;
/**
* 进度条画笔
*/
private Paint mProgressPaint;
private TextPaint mTextPaint;
/**
* 提示信息1
*/
private String mHint;
/**
* 提示信息2
*/
private String mTextHint;
/**
* 默认线条颜色
*/
private int mStrokeColor;
/**
* 进度线条颜色
*/
private int mProgressColor;

public ProgressView(Context context) {
this(context, null);
}

public ProgressView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public ProgressView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}

private void init(Context context, AttributeSet attrs) {
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ProgressView);
try {
mCurrentProgress = ta.getInt(R.styleable.ProgressView_progress, 0);
if (mCurrentProgress > mMaxProgress) {
throw new RuntimeException("progress only define less than 100");
}
mHint = ta.getString(R.styleable.ProgressView_hint);
mTextHint = ta.getString(R.styleable.ProgressView_textHint);
mStrokeColor = ta.getColor(R.styleable.ProgressView_strokeColor, Color.rgb(0xe9, 0xe9, 0xe9));
mProgressColor = ta.getColor(R.styleable.ProgressView_progressColor, getResources().getColor(R.color.colorPrimary));
} finally {
ta.recycle();
}
mPaint = new Paint();
mRectF = new RectF();
}

@Override
protected void onDraw(Canvas canvas) {
int width = this.getWidth();
int height = this.getHeight();
Log.d("ProgressView", "width = " + width + " height = " + height);
if (width != height) {
int min = Math.min(width, height);
width = min;
height = min;
}
// 画最底层
mPaint.setAntiAlias(true); // 设置抗锯齿
mPaint.setColor(mStrokeColor); // 设置全圆弧颜色
mPaint.setStrokeWidth(mProgressStrokeWidth);
mPaint.setStyle(Style.STROKE);
mRectF.left = mRectF.top = mProgressStrokeWidth / 2;
mRectF.right = width - mProgressStrokeWidth / 2;
mRectF.bottom = height - mProgressStrokeWidth / 2;
canvas.drawArc(mRectF, 0, 360, false, mPaint);

// 画第二层
mPaint.setColor(mProgressColor);
canvas.drawArc(mRectF, 270, ((float) mCurrentProgress / 100) * 360, false, mPaint);

// 画进度文字
mPaint.setStrokeWidth(mTextStrokeWidth);
String text = mCurrentProgress + "%";
int textHeight = height / 5;
mPaint.setTextSize(textHeight);
FontMetrics fontMetrics = mPaint.getFontMetrics();
float baseline = (mRectF.bottom + mRectF.top - fontMetrics.bottom - fontMetrics.top) / 2;
int textWidth = (int) mPaint.measureText(text, 0, text.length());
mPaint.setStyle(Style.FILL);
canvas.drawText(text, mRectF.centerX() - textWidth / 2, baseline, mPaint);

if (!TextUtils.isEmpty(mHint)) {
mPaint.setStrokeWidth(mTextStrokeWidth);
textHeight = height / 8;
mPaint.setTextSize(textHeight);
fontMetrics = mPaint.getFontMetrics();
mPaint.setColor(Color.rgb(0x99, 0x99, 0x99));
textWidth = (int) mPaint.measureText(mHint, 0, mHint.length());
mPaint.setStyle(Style.FILL);
baseline = height / 4 - (fontMetrics.bottom + fontMetrics.top) / 2;
canvas.drawText(mHint, width / 2 - textWidth / 2, baseline, mPaint);
}

if (!TextUtils.isEmpty(mTextHint)) {
mPaint.setStrokeWidth(mTextStrokeWidth);
textHeight = height / 8;
mPaint.setTextSize(textHeight);
fontMetrics = mPaint.getFontMetrics();
textWidth = (int) mPaint.measureText(mTextHint, 0, mTextHint.length());
mPaint.setStyle(Style.FILL);
baseline = 3 * height / 4 - (fontMetrics.bottom + fontMetrics.top) / 2;
canvas.drawText(mTextHint, width / 2 - textWidth / 2, baseline, mPaint);
}

if (isInEditMode()) {
// 绘制辅助线
mPaint.setStrokeWidth(1);
mPaint.setColor(Color.LTGRAY);
canvas.drawLine(0, width / 2, width, width / 2, mPaint); // 横线 1/2
canvas.drawLine(0, height / 4, width, height / 4, mPaint); // 横线 1/4
canvas.drawLine(0, height / 4 * 3, width, height / 4 * 3, mPaint); // 横线 3/4
canvas.drawLine(width / 2, 0, width / 2, height, mPaint); // 竖线
}
}

public int getMaxProgress() {
return mMaxProgress;
}

public void setMaxProgress(int maxProgress) {
this.mMaxProgress = maxProgress;
}

public void setProgress(int progress) {
this.mCurrentProgress = progress;
this.invalidate();
}

public void setProgressNotInUIThread(int progress) {
this.mCurrentProgress = progress;
this.postInvalidate();
}

public int getProgress() {
return mCurrentProgress;
}

public String getHint() {
return mHint;
}

public void setHint(String hint) {
mHint = hint;
}

public String getTextHint() {
return mTextHint;
}

public void setTextHint(String textHint) {
mTextHint = textHint;
}

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

int desiredWidth = 250;
int desiredHeight = 250;

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

int width;
int height;

//Measure Width
if (widthMode == MeasureSpec.EXACTLY) {
//Must be this size
width = widthSize;
} else if (widthMode == MeasureSpec.AT_MOST) {
//Can't be bigger than...
width = Math.min(desiredWidth, widthSize);
} else {
//Be whatever you want
width = desiredWidth;
}

//Measure Height
if (heightMode == MeasureSpec.EXACTLY) {
//Must be this size
height = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
//Can't be bigger than...
height = Math.min(desiredHeight, heightSize);
} else {
//Be whatever you want
height = desiredHeight;
}

//MUST CALL THIS
setMeasuredDimension(width, height);
}
}

看看我们最终的实现的效果图

截图