听朋友说【王者荣耀】挺火的让我也下载,没事的时候一块玩几局(这里不是打广告啊,毕竟我不是腾讯的员工,大写的尴尬。。。),当我查看个人资料的时候,看到有对战资料选项,对战的详细数据是以雷达网图的形式、圆形进度条形式等方式向用户展示。网上已经有大牛实现了类似的功能,那么,自己就更应该去尝试一下了。对于圆形进度方式相对较容易,我在前面的自定义View系列中简单讲解过这里就不再赘述,倒是像蜘蛛网状的图形吸引了我,网是由一层层的正n边形组成。然,,加之有一段时间没有写自定义View相关的东西了,所以就也来模仿着实现一下。顺便再温习下自定义View
老规矩,没图说个棒槌啊,来张效果图
分析
- 首先是对n边形的绘制,毫无疑问,我们想到了path,那么就需要确定x、y坐标。从而就需要先知道这个公式:弧度 = 度 * π / 180 (其中 π = Math.PI)以及 x = cosα * r(半径) , y = sinα * r
- 具体绘制的n边形的n等于几?由上面的效果图可以看出来,蜘蛛网的最外层n边形的顶点上对应着需要向用户展示的数据点,所以,根据数据点的个数确定n的值
- 需要绘制多少层n边形,这是做什么用的呢?显而易见,是每项技能对应的数据。假设每项技能的最大输出值为100,我们绘制了4层,那么每层就占了相应总数据的 1/4 即:25,那么在绘制中间的遮罩层的话就可以确定数据点了
- 中心点到n边形的各个顶点的连线,通过canvas的drawLine()方法
- 最后就剩下中间遮罩层了,当然也是通过path连接各个数据点,关于这个数据点的确定上面已经说过了,是通过每一个值占总数据的比例来确定数据点的位置
主要思路我们已经分析了,那么,接下来就开撸吧,当然了,可不是撸王者荣耀,而是 撸!!代!!码!!
还是原来的套路,首先是我们的自定义属性,绘制中需要将哪些属性开放出来给开发者灵活使用,就定义哪些属性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MyRadarView">
<!-- 雷达网的数量,即:多少层 -->
<attr name="radar_num" format="integer"/>
<!-- 雷达网 数据最大值 -->
<attr name="radar_max_value" format="dimension"/>
<!-- 雷达网中线的宽度 -->
<attr name="radar_line_width" format="dimension"/>
<!-- 雷达网中线的颜色 -->
<attr name="radar_line_color" format="color"/>
<!-- 文字的大小 -->
<attr name="text_size" format="dimension"/>
<!-- 文字的颜色 -->
<attr name="text_color" format="color"/>
<!-- 文字距离雷达网的距离 -->
<attr name="text_radar_distance" format="dimension"/>
</declare-styleable>
</resources>
接下来就是去获取开发者在XML文件中自己设置的自定义属性
public MyRadarView(Context context) {
this(context, null);
}
public MyRadarView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyRadarView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyRadarView, defStyleAttr, 0);
int count = typedArray.getIndexCount();
for (int i = 0; i < count; i++){
int attr = typedArray.getIndex(i);
switch (attr){
case R.styleable.MyRadarView_radar_num:
radarNum = typedArray.getInt(attr, radarNum);
break;
case R.styleable.MyRadarView_radar_max_value:
maxValue = typedArray.getDimension(attr, maxValue);
break;
case R.styleable.MyRadarView_radar_line_width:
radarLineWidth = typedArray.getDimension(attr, radarLineWidth);
break;
case R.styleable.MyRadarView_radar_line_color:
radarLineColor = typedArray.getColor(attr, radarLineColor);
break;
case R.styleable.MyRadarView_text_size:
mTextSize = typedArray.getDimension(attr, mTextSize);
break;
case R.styleable.MyRadarView_text_color:
mTextColor = typedArray.getColor(attr, mTextColor);
break;
case R.styleable.MyRadarView_text_radar_distance:
mDistance = typedArray.getDimension(attr, mDistance);
break;
}
}
typedArray.recycle();
//初始化画笔
setPaint();
//初始化path
mPath = new Path();
//盛放文字的容器
rectBounds = new Rect();
}
上面的代码大家肯定很熟悉了,这里还是简单的啰嗦一下,方便刚开始学自定义view的童鞋。当我们在xml文件中去手动设置一些属性配置后,那么我们通过int count = typedArray.getIndexCount();就可以得到在xml文件中手动配置多少项,就可以继续往下走,将获取到的属性值赋值给我们定义的变量。当我们没有手动去配置这些公开的属性的话,那么count为0就不会继续往下走,所以我们的变量就得不到赋值,只能使用默认值。这也就是为什么有些童鞋在写自定义view时总是出不来效果,因为我们需要设置一些默认值,当其他小伙伴使用你这个自定义控件时,不想手动配置属性值的情况。好吧,太啰嗦了。。。
然后就是测量 onMeasure(),这里为了简便就没重写onMeasure方法了,重写了onSizeChanged()方法,因为我们知道,此方法中得到的宽、高就是整个自定义view的最终宽、高。为什么这样说呢?因为有时候父控件会影响到我们子view的绘制,这时候就会调用onSizeChanged()方法,字面意思也很好理解,尺寸发生改变时执行。
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
centerX = w / 2;
centerY = h / 2;
}
其中centerX、centerY 就是整个自定义view的中心点坐标
接下来,就到了我们的绘制了 onDraw()方法
按照文章开头的分析,首先,绘制正n边形,这时候那几个公式就派上用场了。注释的很清楚了,相信大家都能看懂
mRadarPaint.setStrokeWidth(dp2px(2));
float hudu = (float) (2 * Math.PI / pointCount); // 弧度 = 度 * π / 180,有几个顶点就有几个角所以 (角度)angle * pointCount(总个数)=360°(一圈360度)
float distance = mRadius / radarNum;//每一层的间距
for(int i = 1;i <= radarNum;i++){//总共有几层网,中心点不用绘制
float currentRadius = distance * i; //当前层所对应的间距(即:当前半径),由于每一层累加间距
mPath.reset();
for(int j = 1; j<=pointCount; j++){
float x = (float) (Math.cos(hudu*j) * currentRadius + centerX);
float y = (float) (Math.sin(hudu*j) * currentRadius + centerY);
if(j == 1){
mPath.moveTo(x, y);//起点坐标
}else{
mPath.lineTo(x, y);
}
}
mPath.close();
//绘制
canvas.drawPath(mPath, mRadarPaint);//有多少层就画多少个
}
接着是绘制顶点到中心点的连线(注意:这里用的是mRadius,是每一层半径相加的总和,即总的半径)
mRadarPaint.setColor(Color.parseColor("#E2E2E2"));
mRadarPaint.setStrokeWidth(dp2px(2));
for(int i = 1; i<=pointCount;i++){
float x = (float) (Math.cos(hudu*i) * mRadius + centerX);
float y = (float) (Math.sin(hudu*i) * mRadius + centerY);
//绘制连线
canvas.drawLine(centerX, centerY, x, y, mRadarPaint);
}
然后是绘制n边形最外层上面的文字说明(这里需要注意一下,以为360°都分配的有文字,即文字遍布四个象限,不同象限的cosα, sinα正负值的变化影响文字的位置绘制。其次,Android中的象限与数学中的还不太一样,Android中第一象限(右下角)范围0-90度,推算得出0<弧度<π/2)
//画正n边形最外层上面的文字
for(int i = 1; i<=pointCount;i++){
float y = (float) (Math.sin(hudu*i) * (mRadius + mDistance) + centerY);
float x = (float) (Math.cos(hudu*i) * (mRadius + mDistance) + centerX);
//绘制文字
String text = mTextList.get(i-1);
float textWidth = mTextPaint.measureText(text);
Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
float textHeight = (fontMetrics.descent - fontMetrics.ascent)/2;
//对于文字的绘制,在不同的象限则sinα 和 cosα 正负的影响
if(hudu*i >= 0 && hudu*i <= Math.PI / 2){
//第一象限,右下角区域,可正常显示
canvas.drawText(text, x, y + textHeight, mTextPaint);
}else if(hudu*i > Math.PI / 2 && hudu*i <= Math.PI){
//第二象限 左下角区域 需要进一步处理
canvas.drawText(text, x - textWidth, y + textHeight, mTextPaint);
}else if(hudu*i > Math.PI && hudu*i <= 3*Math.PI/2){
//第三象限 左上角区域
canvas.drawText(text, x - textWidth, y + textHeight/2, mTextPaint);
}else if(hudu*i > 3*Math.PI/2 && hudu*i < 2*Math.PI){
//第四项限 右上角区域 需要进一步处理
canvas.drawText(text, x, y, mTextPaint);
}else{
//hudu*i = 2*Math.PI 的情况
canvas.drawText(text, x, y + textHeight/2, mTextPaint);
}
}
最后,根据传入的数据来绘制遮罩层
private void drawDatasOver(Canvas canvas, float hudu) {
mPath.reset();//必须清空下,因为mPath被多处调用
for (int k = 1; k <= mDatas.size(); k++){
float value = mDatas.get(k - 1);
//计算出每一个值占最大值的比例
double percent = value / maxValue;
float x = (float) (centerX + Math.cos(hudu*k)*mRadius*percent);//占总的半径的 mRadius*percent
float y = (float) (centerY + Math.sin(hudu*k)*mRadius*percent);
if(k == 1){
mPath.moveTo(x, y);
}else{
mPath.lineTo(x, y);
}
}
mPath.close();
//画笔设置
mRadarValuePaint.setStyle(Paint.Style.FILL);
//设置透明度
mRadarValuePaint.setAlpha(150);
canvas.drawPath(mPath, mRadarValuePaint);
}
需要特别说明一下,这里我们多次使用mPath来绘制不同的路径,所以每次绘制不同的模块时,要记得先清空下mPath.reset(),防止导致绘制错乱。你也可以根据不同的绘制模块创建不同的path
接下来,我们可以get/set一些方法,提供给外部调用、传递
//数据源
public List<Float> getmDatas() {
return mDatas;
}
public void setmDatas(List<Float> mDatas) {
this.mDatas = mDatas;
}
/**
* 设置最大值,类似进度条,当我们设置最大值为100时,表示进度加载完成
*/
public float getMaxValue() {
return maxValue;
}
public void setMaxValue(float maxValue) {
this.maxValue = maxValue;
}
public List<String> getmTextList() {
return mTextList;
}
public void setmTextList(List<String> mTextList) {
this.mTextList = mTextList;
//得到文字数据集合后,比较,获取文字宽度最长的文字(用作自定义view总宽度的取值)
setTextDataSort();
}
private void setTextDataSort() {
//返回集合中最大长度的元素
maxLengthText = Collections.max(mTextList, new Comparator<String>() {
@Override
public int compare(String s, String t1) {
return s.length() - t1.length();
}
});
}
最后就如何如使用了,分别看下activity_main布局文件和MainActivity
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.alone.radarview.MainActivity">
<com.alone.radarview.MyRadarView
android:id="@+id/radarView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="60dp"
android:layout_marginRight="60dp"
app:radar_num="4"
/>
</RelativeLayout>
public class MainActivity extends AppCompatActivity {
private List<String> lists;//表示文字的集合
private List<Float> mDatas;//数据
private MyRadarView myRadarView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myRadarView = (MyRadarView) findViewById(R.id.radarView);
lists = new ArrayList<>();
Collections.addAll(lists,"团战", "发育", "输出", "推进", "战绩(KDA)", "生存");
myRadarView.setmTextList(lists);
//设置数据 以及 最大值
myRadarView.setMaxValue(100f);
mDatas = new ArrayList<>();
mDatas.add(30f);
mDatas.add(70f);
mDatas.add(20f);
mDatas.add(50f);
mDatas.add(80f);
mDatas.add(100f);
myRadarView.setmDatas(mDatas);
}
}