一、背景
很多业务都具有特定性,做出来的设计稿也各有千秋,而且很多时候三方类库并没有很好的解决方案,这时候就需要自己自定义一个出来了,下面简单复习下自定义view。
二、自定义view
记得很久以前在郭霖老师博客中看到过,实现自定义view主要有3种方法:自绘控件、组合控件、继承控件。
android的所有控件、所有布局都直接或间接继承view。
三、自绘控件
上上周,公司商户组有个能够展示店铺各项数据的数据中心的新需求,如下图,需求的主视图是一个雷达图view,该view的背景是围绕一个圆心的5条逐步加大的圆,像一块pizza一样哈哈,pizza被平均切成6块,落刀的每个边缘显示各项数据信息,pizza的正中间放着个大块黑椒鸡肉用来显示"综合得分"数据,最后将各项数据两两相连成一个撒有碎香肠蔬菜芝士的蓝色区域。
一开始想用MPAndroidChart库去实现,但是该库的雷达图仅适用于蜘蛛网背景的雷达图,与设计稿的不同,只好自己写一个。如此美味的pizza,等不及了哈哈。
首先来自定义一个5条圆形的背景,代码如下:
public class PizzaHahaView extends View {
private Paint mPaintPolygon; //绘制圆形轮廓的画笔
private static final int NUM_OF_SIDES = 6; //雷达的边数
private float mAngel = (float) (2 * Math.PI / NUM_OF_SIDES); //角度
private Path mPathPolygon; //多边形的绘制路径
private float mStartRadius = 42F; //多边形的起始半径
private int mCenterX; //中心点 x 轴坐标
private int mCenterY; //中心点 y 轴坐标
public PizzaHahaView(Context context) {
super(context);
}
public PizzaHahaView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaintPolygon = new Paint(Paint.ANTI_ALIAS_FLAG);
mPathPolygon = new Path();
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
mCenterX = w / 2;
mCenterY = h / 2;
}
@Override
protected void onDraw(Canvas canvas) {
mPaintPolygon.setStyle(Paint.Style.STROKE);
mPaintPolygon.setColor(Color.GRAY);
mPaintPolygon.setStrokeWidth(1F);
for (int count = 1; count <= 5; count++) {
float newRadius = mStartRadius * count;
mPathPolygon.moveTo(mCenterX + newRadius, mCenterY);
for (int i = 1; i < NUM_OF_SIDES; i++) {
float x = (float) (mCenterX + Math.cos(mAngel * i) * newRadius);
float y = (float) (mCenterY + Math.sin(mAngel * i) * newRadius);
mPathPolygon.lineTo(x, y);
if ( i == NUM_OF_SIDES)
mPaintPolygon.setColor(Color.BLACK);
}
mPathPolygon.close();
canvas.drawCircle(mCenterX, mCenterY, newRadius, mPaintPolygon);
mPathPolygon.reset();
}
}
}
在以上代码中,首先在PizzaHahaView的构造函数中初始化了一些数据,当view中所有的子控件均被映射成xml后触发onFinishInflate(),当view的大小发生变化则调用onSizeChanged(),接下来是最重要的onDraw(),几乎所有逻辑都在这了,首先设置了画笔为空心画,并且设置了颜色和粗细度,接着循环遍历5条线,中间涉及到常用的初衷三角函数计算,从moveTo()的坐标开始移动到lineTo()的坐标,最后用canvas.drawCirclw()绘制出圆,效果图如下:
其余部分的绘制方法基本类似,设置好画笔paint和路径path,用canvas的drawPath()绘制路径、drawText()绘制文本等绘制出来,完整代码和最终效果如下,必要的注释写在代码中了:
public class PizzaHahaView extends View {
private Paint mPaintPolygon; //绘制圆形轮廓的画笔
private Paint mPaintLine; //绘制直线的画笔
private Paint mPaintRegion; //绘制分值区的画笔
private Paint mPaintRegionOutline; //分值区的轮廓,蓝色加粗
private Paint mTextPaint; //文本画笔
private Paint mDataPaint; //数据的画笔
/**
* 雷达的边数
*/
private static final int NUM_OF_SIDES = 6;
/**
* 角度
*/
private float mAngel = (float) (2 * Math.PI / NUM_OF_SIDES);
/**
* 多边形的绘制路径
*/
private Path mPathPolygon;
/**
* 多边形的起始半径
*/
private float mStartRadius = 42F;
/**
* 直线的绘制路径
*/
private Path mPathLine;
/**
* 分值区的绘制路径
*/
private Path mPathRegion;
/**
* 中心点 x 轴坐标
*/
private int mCenterX;
/**
* 中心点 y 轴坐标
*/
private int mCenterY;
private String[] titles = {"进货额", "新用户数", "营业时长", "订单量", "销售总额", "代销金额"};
private String[] number = {"0", "0", "0", "0", "0", "0"};
public String[] getNumber() {
return number;
}
public void setNumber(String[] number) {
this.number = number;
init();
}
private double avgScore = 0.00; //综合得分
public double getAvgScore() {
return avgScore;
}
public void setAvgScore(double avgScore) {
this.avgScore = avgScore;
init();
}
/**
* 分值区域数值
*/
private float[] percents = {0.0F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F};
public float[] getPercents() {
return percents;
}
public void setPercents(float[] percents) {
this.percents = percents;
init();
}
public DataCenterView(Context context) {
super(context);
}
public DataCenterView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mPaintPolygon = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintPolygon.setStyle(Paint.Style.STROKE);
mPaintPolygon.setColor(Color.GRAY);
mPaintPolygon.setStrokeWidth(1F);
mPaintLine = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintLine.setStyle(Paint.Style.STROKE);
mPaintLine.setColor(Color.parseColor("#E7E7E7"));
mPaintLine.setStrokeWidth(1F);
mPaintRegion = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintRegion.setStyle(Paint.Style.FILL_AND_STROKE);
mPaintRegion.setColor(Color.parseColor("#5032A2F8"));
mPaintRegion.setStrokeWidth(1F);
mPaintRegionOutline = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintRegionOutline.setStyle(Paint.Style.STROKE);
mPaintRegionOutline.setColor(Color.parseColor("#32A2F8"));
mPaintRegionOutline.setStrokeWidth(1F);
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setStyle(Paint.Style.FILL);
mTextPaint.setColor(Color.BLACK);
mTextPaint.setStrokeWidth(1F);
mDataPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mDataPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mDataPaint.setColor(Color.BLACK);
mDataPaint.setStrokeWidth(1F);
mPathPolygon = new Path();
mPathLine = new Path();
mPathRegion = new Path();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
mCenterX = w / 2;
mCenterY = h / 2;
}
@Override
protected void onDraw(Canvas canvas) {
drawCircle(canvas);
drawLine(canvas);
drawRegion(canvas);
drawText(canvas);
}
/**
* 绘制分值区
*
* @param canvas
*/
private void drawRegion(Canvas canvas) {
float radius = mStartRadius * 5;
for (int i = 0; i < NUM_OF_SIDES; i++) {
float x = (float) (Math.cos(mAngel * i + Math.PI / 6) * radius * percents[i]);
float y = (float) (Math.sin(mAngel * i + Math.PI / 6) * radius * percents[i]);
if (i == 0) {
mPathRegion.moveTo(mCenterX + x, mCenterY + y);
} else {
mPathRegion.lineTo(mCenterX + x, mCenterY + y);
}
}
mPathRegion.close();
canvas.drawPath(mPathRegion, mPaintRegion);
}
/**
* 绘制直线
*
* @param canvas
*/
private void drawLine(Canvas canvas) {
float radius = mStartRadius * 5;
float angel = (float) (2 * Math.PI / 6);
for (int i = 0; i < 6; i++) {
mPathLine.moveTo(mCenterX, mCenterY);
float x = (float) (mCenterX + Math.cos(angel * i + Math.PI / 6) * radius);
float y = (float) (mCenterY + Math.sin(angel * i + Math.PI / 6) * radius);
mPathLine.lineTo(x, y);
canvas.drawPath(mPathLine, mPaintLine);
}
}
/**
* 绘制圆形
*
* @param canvas
*/
private void drawCircle(Canvas canvas) {
for (int count = 1; count <= 5; count++) {
float newRadius = mStartRadius * count;
mPathPolygon.moveTo(mCenterX + newRadius, mCenterY);
for (int i = 1; i < NUM_OF_SIDES; i++) {
float x = (float) (mCenterX + Math.cos(mAngel * i) * newRadius);
float y = (float) (mCenterY + Math.sin(mAngel * i) * newRadius);
mPathPolygon.lineTo(x, y);
if ( i == NUM_OF_SIDES) {
mPaintPolygon.setColor(Color.BLACK);
}
}
mPathPolygon.close();
canvas.drawCircle(mCenterX, mCenterY, newRadius, mPaintPolygon);
mPathPolygon.reset();
}
}
/**
* 绘制数据
*
* @param canvas
*/
private void drawText(Canvas canvas) {
Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
Log.d("Aige", "ascent:" + fontMetrics.ascent);
Log.d("Aige", "top:" + fontMetrics.top);
Log.d("Aige", "leading:" + fontMetrics.leading);
Log.d("Aige", "descent:" + fontMetrics.descent);
Log.d("Aige", "bottom:" + fontMetrics.bottom);
float fontHeight = fontMetrics.descent - fontMetrics.ascent;
mTextPaint.setTextSize(28);
mTextPaint.setColor(Color.parseColor("#32A2F8"));
float avgScoreTitleDis = mTextPaint.measureText("综合得分");//文本长度
float avgScoreDis = mTextPaint.measureText(Double.toString(avgScore));
canvas.drawText("综合得分", mCenterX - avgScoreTitleDis / 2, mCenterY, mTextPaint);
canvas.drawText(avgScore+"", mCenterX - avgScoreDis / 2, mCenterY + 30, mTextPaint);
for (int i = 0; i < 6; i++) {
float x,y;
mTextPaint.setColor(Color.parseColor("#000000"));
if (mAngel * i + Math.PI == Math.PI / 2) {
x = (float) (mCenterX + (mStartRadius + fontHeight * 10) * Math.cos(mAngel * i + Math.PI / 6));
y = (float) (mCenterY + (mStartRadius + fontHeight * 10) * Math.sin(mAngel * i + Math.PI / 6));
} else if (mAngel * i + Math.PI == Math.PI / 2 * 3) {
x = (float) (mCenterX + (mStartRadius + fontHeight * 10) * Math.cos(mAngel * i + Math.PI / 6));
y = (float) (mCenterY + (mStartRadius + fontHeight * 10) * Math.sin(mAngel * i + Math.PI / 6));
} else {
x = (float) (mCenterX + (mStartRadius + fontHeight * 10) * Math.cos(mAngel * i + Math.PI / 6));
y = (float) (mCenterY + (mStartRadius + fontHeight * 10) * Math.sin(mAngel * i + Math.PI / 6));
}
if (mAngel * i >= 0 && mAngel * i <= Math.PI / 2) {//第4象限
float titlesDis = mTextPaint.measureText(titles[i]);//文本长度
float numberDis = mTextPaint.measureText(number[i]);//数字长度
if ("进货额".equals(titles[i])) {
canvas.drawText(titles[i], x + mStartRadius * 5 / 2 - titlesDis / 2, mCenterY + mStartRadius * 5 / 2, mTextPaint); //进货额
canvas.drawText(number[i], x + mStartRadius * 5 / 2, mCenterY + mStartRadius * 5 / 2 + titlesDis / 3, mTextPaint);// x - numberDis + 70, y + 100
} else {
canvas.drawText(titles[i], x - titlesDis / 2, y + titlesDis / 4 * 2, mTextPaint); //新用户数
}
} else if (mAngel * i >= 3 * Math.PI / 2 && mAngel * i <= Math.PI * 2) {//第3象限
float titlesDis = mTextPaint.measureText(titles[i]);//文本长度
float numberDis = mTextPaint.measureText(number[i]);//数字长度
canvas.drawText(titles[i], x + mStartRadius * 5 / 2 - titlesDis / 2, mCenterY - mStartRadius * 5 / 2 - titlesDis / 4, mTextPaint); //代销金额
canvas.drawText(number[i], x + mStartRadius * 5 / 2, mCenterY - mStartRadius * 5 / 2, mTextPaint);
} else if (mAngel * i > Math.PI / 2 && mAngel * i <= Math.PI) {//第2象限
float titlesDis = mTextPaint.measureText(titles[i]);//文本长度
float numberDis = mTextPaint.measureText(number[i]);//数字长度
canvas.drawText(titles[i], x - mStartRadius * 5 / 2 - titlesDis / 2, mCenterY + mStartRadius * 5 / 2, mTextPaint); //营业时长
canvas.drawText(number[i], x - mStartRadius * 5 / 2, mCenterY + mStartRadius * 5 / 2 + titlesDis / 4, mTextPaint);
} else if (mAngel * i >= Math.PI && mAngel * i < 3 * Math.PI / 2) {//第1象限
float titlesDis = mTextPaint.measureText(titles[i]);//文本长度
float numberDis = mTextPaint.measureText(number[i]);//数字长度
if ("订单量".equals(titles[i])) {
canvas.drawText(titles[i], x - mStartRadius * 5 / 2 - titlesDis / 2, mCenterY - mStartRadius * 5 / 2 - titlesDis / 4, mTextPaint); //订单量
canvas.drawText(number[i], x - mStartRadius * 5 / 2, mCenterY - mStartRadius * 5 / 2, mTextPaint);
} else {
canvas.drawText(titles[i], x - titlesDis / 2, y - titlesDis / 4 * 2, mTextPaint); //销售总额
canvas.drawText(number[i], x - numberDis / 2, y - titlesDis / 4, mTextPaint);
}
}
Log.i("radar", x + " " + y + " " + titles[i]);
}
}
}
一盘可口美味色泽上佳的pizza出炉了哈哈,自绘控件大致上就这个思路。
四、组合控件
组合控件,将多个原生控件组合在一起使用的控件。
首先,将原生控件组合在一个XML里面;
接着,自定义一个class,让他继承FrameLayout,在它的构造方法里用LayoutInflater找到该XML文件并将其实例化;
eg. LayoutInflater.from(this).inflate(R.layout.***, this);
最后,即可初始化那些添加进去的原生组件,然后进行相关业务处理。
五、继承控件
即是继承原有的控件,来增加一些新的功能。
参考郭林老师的PowerImageView,它是继承ImageView的。