Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all articles
Browse latest Browse all 5930

仿王者荣耀对战资料选项中的【雷达网图展示详细数据】

$
0
0

听朋友说【王者荣耀】挺火的让我也下载,没事的时候一块玩几局(这里不是打广告啊,毕竟我不是腾讯的员工,大写的尴尬。。。),当我查看个人资料的时候,看到有对战资料选项,对战的详细数据是以雷达网图的形式、圆形进度条形式等方式向用户展示。网上已经有大牛实现了类似的功能,那么,自己就更应该去尝试一下了。对于圆形进度方式相对较容易,我在前面的自定义View系列中简单讲解过这里就不再赘述,倒是像蜘蛛网状的图形吸引了我,网是由一层层的正n边形组成。然,,加之有一段时间没有写自定义View相关的东西了,所以就也来模仿着实现一下。顺便再温习下自定义View

老规矩,没图说个棒槌啊,来张效果图
这里写图片描述

分析

  1. 首先是对n边形的绘制,毫无疑问,我们想到了path,那么就需要确定x、y坐标。从而就需要先知道这个公式:弧度 = 度 * π / 180 (其中 π = Math.PI)以及 x = cosα * r(半径) , y = sinα * r
  2. 具体绘制的n边形的n等于几?由上面的效果图可以看出来,蜘蛛网的最外层n边形的顶点上对应着需要向用户展示的数据点,所以,根据数据点的个数确定n的值
  3. 需要绘制多少层n边形,这是做什么用的呢?显而易见,是每项技能对应的数据。假设每项技能的最大输出值为100,我们绘制了4层,那么每层就占了相应总数据的 1/4 即:25,那么在绘制中间的遮罩层的话就可以确定数据点了
  4. 中心点到n边形的各个顶点的连线,通过canvas的drawLine()方法
  5. 最后就剩下中间遮罩层了,当然也是通过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);
    }
}

主要的内容已经分析讲解完了,完整代码有时间再上传吧,其实这个demo还可以再炫酷点,比如,再加上一些滑动触发等事件以及动画,那么效果就更赞了,这里就留给你们去实现吧。使用过程中有什么问题或者有写的不正确的地方也欢迎提出。

作者:xiaxiazaizai01 发表于2017/3/29 14:26:21 原文链接
阅读:33 评论:0 查看评论

Viewing all articles
Browse latest Browse all 5930

Trending Articles