在github上搜了一堆堆评分控件都没有理想中的样子所以在自己的开源项目上造了了轮子出来效果图如下:
先说明下理想中需求
支持任意大于等于3的评分
支持具有变色效果
支持分数以及图形分平均值描边
支持设置描边宽度大小的设置
支持显示对应的分数view添加
首先任意评分项生成图形的具体算法如下:
for (int position = 0; position < angleCount; position++) {
nextAngle = offsetAngle + (position * averageAngle);
nextRadians = (float) Math.toRadians(nextAngle);
nextPointX = (float) (centerX + Math.sin(nextRadians) * currentRadius);
nextPointY = (float) (centerY - Math.cos(nextRadians) * currentRadius);
if (position == 0) path.moveTo(nextPointX, nextPointY);
else path.lineTo(nextPointX, nextPointY);
}
上述效果图都是圆内接多边形实现,循环计算评分项角度通过计算得出弧度来计算XY坐标然后计算出path路径,其中offsetAngle代表复数评分项的角度偏移量,可以通过设置是否开启角度偏移量来动态生成角度偏移量,拿圆内四边形来描述,不设置角度偏移的时候,平分圆心角为直角,利用角分线原理将其偏移直角的二分一。
attr属性的定义:
<attr name="maxScore" format="float" />
<attr name="hierarchyCount" format="integer" />
<attr name="angleCount" format="integer" />
<attr name="spiderStrokeWidth" format="dimension" />
<attr name="scoreStrokeWidth" format="dimension" />
<attr name="spiderStokeBg" format="color" />
<attr name="spiderBg" format="color" />
<attr name="scoreBg" format="color" />
<attr name="scoreStokeBg" format="color" />
<attr name="isSpiderBg" format="boolean" />
<attr name="isScoreBg" format="boolean" />
<attr name="isSpiderStroke" format="boolean" />
<attr name="isScoreStroke" format="boolean" />
<attr name="isComplexOffset" format="boolean" />
<attr name="isGradientSpider" format="boolean" />
<attr name="spiderEndBg" format="color" />
<declare-styleable name="RxEthanSpiderWeb">
<attr name="maxScore" />
<attr name="hierarchyCount" />
<attr name="angleCount" />
<attr name="spiderStrokeWidth" />
<attr name="scoreStrokeWidth" />
<attr name="spiderStokeBg" />
<attr name="spiderBg" />
<attr name="spiderEndBg" />
<attr name="scoreBg" />
<attr name="scoreStokeBg" />
<attr name="isSpiderBg" />
<attr name="isScoreBg" />
<attr name="isSpiderStroke" />
<attr name="isScoreStroke" />
<attr name="isComplexOffset" />
<attr name="isGradientSpider" />
</declare-styleable>
<declare-styleable name="RxSpiderWebLayout">
<attr name="isBuildInSpider" format="boolean" />
<attr name="spiderMargin" format="dimension" />
<attr name="maxScore" />
<attr name="hierarchyCount" />
<attr name="angleCount" />
<attr name="spiderStrokeWidth" />
<attr name="scoreStrokeWidth" />
<attr name="spiderStokeBg" />
<attr name="spiderBg" />
<attr name="spiderEndBg" />
<attr name="scoreBg" />
<attr name="scoreStokeBg" />
<attr name="isSpiderBg" />
<attr name="isScoreBg" />
<attr name="isSpiderStroke" />
<attr name="isScoreStroke" />
<attr name="isComplexOffset" />
<attr name="isGradientSpider" />
</declare-styleable>
attr属性的解析:
/**
* @value spiderWebPaint 蜘蛛网线条画笔
* @value spiderBgPaint 蜘蛛网背景颜色画笔
* @value scoreBgPaint 分数背景颜色画笔
* @value scoreStrokePaint 分数线条画笔
* @value path 图形
* @value scores 用户每一个评分点的具体数值分数列表
* @value angleCount 整个蛛网有几个角
* @value hierarchyCount 整个蛛网分多少层(例如最大分数是10分,分5层,那么每层就代表2分)
* @value maxScore 每一个分数顶点代表分数值
* @value averageAngle 平均角度
* @value offsetAngle 偏移角度
* @value spiderColor 蜘蛛网背景颜色当设置为变色模式则为起始颜色
* @value scoreColor 分数图形的颜色
* @value spiderStrokeColor 蛛网线条的颜色
* @value scoreStrokeColor 分数图形描边的颜色
* @value spiderEndColor 蜘蛛网变色的结束颜色
* @value spiderStrokeWidth 蛛网线条的宽度
* @value scoreStrokeWidth 分数图形描边的宽度
* @value isScoreStroke 是否分数图形的描边
* @value isSpiderStroke 是否蜘蛛网状层次描边
* @value isSpiderBg 是否设置蜘蛛网背景
* @value isScoreBg 是否设置分数网背景
* @value isComplexOffset 是否设置复数边形的角度偏移量保证复数边形不会以一定角度旋转偏移默认不设置效果会好一点
* @value gradientSpiderColors 每一层变色后的颜色值
* @value radius 整个蛛网图的半径
* @value centerX 蛛网圆点的X轴坐标
* @value centerY 蛛网圆点的Y轴坐标
*/
绘制图形方法:
@Override
protected void onDraw(Canvas canvas) {
if (isSpiderBg && !isGradientSpider) drawSpiderBg(canvas);
if (isGradientSpider) drawGradientSpider(canvas);
if ((!isSpiderStroke) && (!isSpiderBg) || isSpiderStroke) drawSpiderStoke(canvas);
drawScore(canvas);
}
/**
* 绘制蜘蛛网中心点到最外层形状的的背景path
*/
protected void drawSpiderBg(Canvas canvas) {
canvas.drawPath(hierarchyByRadiusPath(radius), spiderBgPaint);
}
/**
* 绘制渐变的蜘蛛网 注意是从最外层到最内层间的绘制避免画笔颜色会被覆盖
*/
protected void drawGradientSpider(Canvas canvas) {
float averageRadius = radius / hierarchyCount;
for (int i = hierarchyCount - 1; i >= 0; i--) {
try {
paintSetColor(spiderBgPaint, gradientSpiderColors[i]);
canvas.drawPath(hierarchyByRadiusPath(averageRadius * (i + 1)), spiderBgPaint);
} catch (Exception e) {
Log.i("ypzZZ", e.getMessage());
}
}
}
/**
* 绘制描边蜘蛛网每一层的形状Path
* 最后绘制每一个顶点到圆心的路径
*/
protected void drawSpiderStoke(Canvas canvas) {
float averageRadius = radius / hierarchyCount;
for (int w = 1; w <= hierarchyCount; w++)
canvas.drawPath(hierarchyByRadiusPath(averageRadius * w), spiderWebPaint);
float nextAngle;
float nextRadians;
float nextPointX;
float nextPointY;
for (int position = 0; position < angleCount; position++) {
nextAngle = offsetAngle + (position * averageAngle);
nextRadians = (float) Math.toRadians(nextAngle);
nextPointX = (float) (centerX + Math.sin(nextRadians) * radius);
nextPointY = (float) (centerY - Math.cos(nextRadians) * radius);
canvas.drawLine(centerX, centerY, nextPointX, nextPointY, spiderWebPaint);
}
}
/**
* 绘制分数图形
*/
protected void drawScore(Canvas canvas) {
if (scores == null || scores.length <= 0) return;
path.reset();
Log.i("ypzZZ","drawScore");
float nextAngle;
float nextRadians;
float nextPointX;
float nextPointY;
float currentRadius;
for (int position = 0; position < angleCount; position++) {
currentRadius = (scores[position] / maxScore) * radius;
nextAngle = offsetAngle + (position * averageAngle);
nextRadians = (float) Math.toRadians(nextAngle);
nextPointX = (float) (centerX + Math.sin(nextRadians) * currentRadius);
nextPointY = (float) (centerY - Math.cos(nextRadians) * currentRadius);
if (position == 0) path.moveTo(nextPointX, nextPointY);
else path.lineTo(nextPointX, nextPointY);
}
path.close();
canvas.drawPath(path, scoreBgPaint);
// 绘制描边
if (isScoreStroke) canvas.drawPath(path, scoreStrokePaint);
}
/**
* 返回每一层蜘蛛网的path路径
*/
private Path hierarchyByRadiusPath(float currentRadius) {
path.reset();
float nextAngle;
float nextRadians;
float nextPointX;
float nextPointY;
for (int position = 0; position < angleCount; position++) {
nextAngle = offsetAngle + (position * averageAngle);
nextRadians = (float) Math.toRadians(nextAngle);
nextPointX = (float) (centerX + Math.sin(nextRadians) * currentRadius);
nextPointY = (float) (centerY - Math.cos(nextRadians) * currentRadius);
if (position == 0) path.moveTo(nextPointX, nextPointY);
else path.lineTo(nextPointX, nextPointY);
}
path.close();
return path;
}
由于想要每一个评分项可以有动态的显示的分数标题所以使用一个自定义的Viewgroup去实现其中这个ViewGroup可以选择是否直接包含评分控件或者搭配评分控件一起使用就设置了isBuildInSpider这一个xml属性
其中这一个Viewgroup牵涉到一点就是子view中meause时绘制长宽出现为0这样一个情况使用了
measureChildren方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measureChildren(widthMeasureSpec, heightMeasureSpec);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (!isSetDimen(widthMeasureSpec)) {
widthMeasureSpec = RxDimenUtils.dpToPx(150, getResources());
}
if (!isSetDimen(heightMeasureSpec)) {
heightMeasureSpec = RxDimenUtils.dpToPx(150, getResources());
}
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
}
其次根据添加的标题的分数View动态计算其位置
/**
* 根据角度判断所处的方位,一个圆分成了8个方位(东、南、西、北、西北、东北、西南、东南),不同的方位有不同的偏移方式
*
* @param angle 角度
* @return 方位
*/
private int calculateLocationByAngle(float angle) {
if ((angle >= 337.5f && angle <= 360f) || (angle >= 0f && angle <= 22.5f)) {
return LOCATION_NORTH;
} else if (angle >= 22.5f && angle <= 67.5f) {
return LOCATION_EAST_NORTH;
} else if (angle >= 67.5f && angle <= 112.5f) {
return LOCATION_EAST;
} else if (angle >= 112.5f && angle <= 157.5) {
return LOCATION_EAST_SOUTH;
} else if (angle >= 157.5 && angle <= 202.5) {
return LOCATION_SOUTH;
} else if (angle >= 202.5 && angle <= 247.5) {
return LOCATION_WEST_SOUTH;
} else if (angle >= 247.5 && angle <= 292.5) {
return LOCATION_WEST;
} else if (angle >= 292.5 && angle <= 337.5) {
return LOCATION_WEST_NORTH;
} else {
throw new IllegalArgumentException("error angle " + angle);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (childCount == 0) return;
// 循环处理每个位置的子View,先计算出子View在圆上所处的位置,然后按照其子View的宽高偏移一定的距离,保证子View全部在圆圈之外,并且子View的中心点和其圆上的点连同圆心在一条直线上
View childView;
float nextAngle;
float nextRadians;
float nextPointX;
float nextPointY;
int childViewMeasuredWidth;
int childViewMeasuredHeight;
float childViewLeft;
float childViewTop;
float averageAngle;
if (isBuildIn && spiderWeb != null)
averageAngle = childCount > 1 ? 360 / (childCount - 1) : 0;
else averageAngle = childCount > 0 ? 360 / childCount : 0;
float offsetAngle = 0;
if (isComplexOffset) offsetAngle = averageAngle / 2;
for (int position = 0; position < childCount; position++) {
childView = getChildAt(position);
childViewMeasuredWidth = childView.getMeasuredWidth();
childViewMeasuredHeight = childView.getMeasuredHeight();
if (childView == spiderWeb) {
childViewLeft = centerX - measureSpec / 2;
childViewTop = centerY - measureSpec / 2;
childView.layout((int) childViewLeft, (int) childViewTop, (int) (childViewLeft + measureSpec), (int) (childViewTop + measureSpec));
} else {
nextAngle = offsetAngle + (position * averageAngle);
nextRadians = (float) Math.toRadians(nextAngle);
if (isBuildIn){
float builderRadius = radius-spiderWebMargin/8*7;
nextPointX = (float) (centerX + Math.sin(nextRadians) * builderRadius);
nextPointY = (float) (centerY - Math.cos(nextRadians) * builderRadius);
spacing /= 8;
}else {
nextPointX = (float) (centerX + Math.sin(nextRadians) * radius);
nextPointY = (float) (centerY - Math.cos(nextRadians) * radius);
}
childViewLeft = nextPointX;
childViewTop = nextPointY;
switch (calculateLocationByAngle(nextAngle)) {
case LOCATION_NORTH :
childViewLeft -= childViewMeasuredWidth / 2;
childViewTop -= childViewMeasuredHeight;
childViewTop -= spacing;
break;
case LOCATION_EAST_NORTH :
childViewTop -= childViewMeasuredHeight / 2;
childViewLeft += spacing;
break;
case LOCATION_EAST :
childViewTop -= childViewMeasuredHeight / 2;
childViewLeft += spacing;
break;
case LOCATION_EAST_SOUTH :
childViewLeft += spacing;
childViewTop += spacing;
break;
case LOCATION_SOUTH :
childViewLeft -= childViewMeasuredWidth / 2;
childViewTop += spacing;
break;
case LOCATION_WEST_SOUTH :
childViewLeft -= childViewMeasuredWidth;
childViewLeft -= spacing;
childViewTop += spacing;
break;
case LOCATION_WEST :
childViewLeft -= childViewMeasuredWidth;
childViewTop -= childViewMeasuredHeight / 2;
childViewLeft -= spacing;
break;
case LOCATION_WEST_NORTH :
childViewLeft -= childViewMeasuredWidth;
childViewTop -= childViewMeasuredHeight / 2;
childViewLeft -= spacing;
childViewTop -= spacing;
break;
}
childView.layout((int) childViewLeft, (int) childViewTop, (int) (childViewLeft + childViewMeasuredWidth), (int) (childViewTop + childViewMeasuredHeight));
}
}
}
最后整个技术难点已经代码方式描述完附上github地址
https://github.com/KilleTom/KilleTomRxMaterialDesignUtil