(自定义控件)RaderView

图1:预期效果图

先上一张效果图,该图为我制作用于展示游戏中,玩家的各项能力数值的一张雷达图。

对于自定义控件你得需要有以下知识储备:(RaderView涉及到的只是点较为基础)

1.自定义属性

2.onMeasure/onDraw

鸿洋大神有一篇对自定义View介绍的的比较详细的文章,附链接。

http://blog.csdn.net/lmj623565791/article/details/24252901/


本文将结合上文的基础知识,进行实践性地实现一个满足特殊需求的自定义View。本文将从正常流程来编写RaderView,包括碰到的许多问题及思考。我将尽力为您展现一个比较完整的自定义View的实现过程。

一.自定义属性

结合我们预期的效果图,这算是一个比较简单的自定义View,图中只有两个属性是我们需要关注的。

1.图中的文字大小

2.由中心点到某一个点的线段的长度

综合以上两点,在value资源文件下添加attrs.xml文件,并添加如下属性。

自定义属性的格式为:名称name,格式format(string,color,demension,integer,enum,reference,float,boolean,fraction,flag)

<resources>

    <attr name="diameter" format="dimension"></attr>

    <attr name="textsize" format="dimension"></attr>

    <declare-styleable name="radar_view">

        <attr name="diameter"/>

        <attr name="textsize"/>

    </declare-styleable>

</resources>

以上则是对属性的定义部分。在RaderView的构造方法中获取我们已经定义好的属性:

privateint diameter;

privateint textsize;

    public RadarView(Context context, AttributeSet attrs, int defStyleAttr) {

        super(context, attrs, defStyleAttr);

        ……

        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.radar_view, defStyleAttr, 0);

        int n = a.getIndexCount();

        for (int i = 0; i < n; i++) {

            int attr = a.getIndex(i);

            switch (attr) {

                case R.styleable.radar_view_diameter:

                    diameter = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));

                    break;

                case R.styleable.radar_view_textsize:

                    textsize = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));

                    break;

            }

        }……

        a.recycle();

    }

需要注意的是,TypedValue.applyDimension方法用于将sp转换px,属性a记得recycle,上述代码记得写到构造器中。

二.控件的长宽测量

顾名思义,这里要实现复写onMeasure方法。先上一部分代码(这里为了节省版面,只上宽度的测量过程)

  int widthMode = MeasureSpec.getMode(widthMeasureSpec);

  int widthSize = MeasureSpec.getSize(widthMeasureSpec);

  if (widthMode == MeasureSpec.EXACTLY) {

            if (getWidth() / 2 > centerX) {

                centerX = getWidth() / 2;

            } else {

                centerX = diameter + getPaddingLeft();

            }

        } else {

            float mWidth = diameter;

            width = desired * 2;

            centerX = width / 2;

        }

        width=width+4*textsize;

        if (width / 2 < diameter) {

            scaleX = width / 2 * 1.0f / diameter;

        }

        if (scaleX < scaleY) {

            scaleY = scaleX;

        } else {

            scaleX = scaleY;

        }

        //由于左边text导致中心右移

        centerX = centerX + 2 * textsize;

        setMeasuredDimension(width, height);

那么上面的代码即为比较完整的,测量宽度的代码。下面将提出几个我在实现过程中遇到的问题,以及几个变量代表的含义。

MeasureSpec.EXACTLY:当宽度设置为精确值或者MATCH_PARENT

MeasureSpec.AT_MOST:当宽度设置为WRAP_CONTENT

按照我的构思以及预期的图,我直接将width的值设置为widthSize+padding(左右),但是当绘制时,无法将左边和右边的字(生存、伤害)展示完全,所以应该加上4*textsize的宽度,最终的结果才是这个控件整体的宽度。这里还有一个地方要注意的是,我在这里 加上 4*textsize的做法是十分不建议的,考虑到拓展性,我们应该可以自己设置左右的字体,那么当X轴上添加的字体不为4的时候,就会出现问题。因此,在这应该添加记录字体个数的变量,例如mTextCount,最终宽度加上mTextCount * textSize就好啦~

diamater:雷达图中心到某个定点的边长

centerX:记录中心点,用于从中心绘制雷达图的background,很好理解。

scaleX:缩放的比例。在宽度的测量中,经常会遇到长和宽设置的比较不搭配(高大于宽,或索性直接设置为MATCH_PARENT),这个时候就需要进行缩放。可以看出,当widthMode为MeasureSpec.EXACTLY时,说明我们为这个控件设置了具体的宽度或者为MATCH_PARENT(控件宽度并不是diameter)。那么当diameter(半径)>width/2时,就需要进行缩放。

当程序进行到这里的时候,我发现当我设置width和height相同的时候,并且都需要进行0.5倍缩放时,我的图是这样的:


图2

为什么会出现这种情况呢?答案是Y轴上需要*根号3/2(这是为什么呢??嘿嘿嘿)

其实这种情况不止要出现在需要缩放的情况下,只是设计进行到缩放这一步,会出现这个问题,原因就是纵轴上的高度,并不是我们设置的diameter,因为diameter被掰弯了。。。因此这个缩放比例很有必要。

如果将图片展示完全,那么就需要取scaleX和scaleY中比较小得值。

至此,onMeasure中的方法介绍完毕。

三:绘制

前面介绍的诸多变量都是为了给绘制打基础。即onDraw函数的实现

看到预期效果图中有三部分需要绘制

1.背景

2.透明的雷达填充部分

3.文字

首先背景的绘制,主要是骨架部分的绘制,这部分可以根据自己的需求绘制。没有难点,唯一比较复杂的就是计算添加交点的比例,和绘制的过程。

Hexagon记录了6个点的坐标,通过draw6point方法将其按照相邻顺序连线。

private void initBackground(Canvas canvas) {

        canvas.scale(scaleX, scaleY);

        for (int i = 1; i <= 4; i++) {

            dia = diameter / 4 * i;

            sqrt3diameter = (int) (Math.sqrt(3) * dia / 2);

            halfDiameter = dia / 2;

            Hexagon hexagon = new Hexagon(centerX + halfDiameter, centerY + sqrt3diameter, centerX + dia, centerY, centerX + halfDiameter, centerY - sqrt3diameter,

                    centerX - halfDiameter, centerY - sqrt3diameter, centerX - dia, centerY, centerX - halfDiameter, centerY + sqrt3diameter);

            draw6point(canvas, hexagon);

        }

    }

这里我的绘制方法是从中点到外圈逐圈绘制。

半透明的雷达填充部分方法如上,只是hexagon类中存放的是能力值终点的值。

文字更简单啦,调用canvas的drawText方法就可以了。

2,3部分的绘制请各位进行一个简单的思考。

四.动画效果

属性动画部分的有关问题请参考郭林写的

Android属性动画完全解析

动画效果则是用属性动画实现的一个非常简单的匀速扩散的效果。Evaluator如下

public class RadarEvaluator implements TypeEvaluator<Hexagon> {

    public Hexagon evaluate(float fraction, Hexagon startValue, Hexagon endValue) {

        int x1 = (int) (startValue.getX1() + fraction * (endValue.getX1() - startValue.getX1()));

        int y1 = (int) (startValue.getY1() + fraction * (endValue.getY1() - startValue.getY1()));

        ……

        return new Hexagon(x1, y1, x2, y2, x3, y3, x4, y4, x5, y5, x6, y6);

    }

}

五.总结

我做完这个控件后,对自定义View中onMeasure和onDraw部分有了一定理解。个人认为在图形的形状部分,算是比较简单的,唯一可能麻烦的应该是长宽的计算和图像的绘制。

写完这个控件后,我认为我只是知道了皮毛。

学无止境,共勉。如果有什么地方有问题、或者可以改进,希望可以共同探讨。

推荐阅读更多精彩内容