掌盟中能力七星图截图
仿照完成的效果截图
基本上模仿的与原控件一致了,就是文字与顶点的距离有一些小瑕疵,这块还需需要优化。
本文目的
可以使读者:
1. 巩固自定义控件的基础知识以及正多边形的绘制,熟悉绘制流程。
2. 复习了高中的一点数学几何知识。
源码地址
整体思路
通过数学几何知识计算出每一圈(多边形)的顶点坐标,然后用Path这个类就能绘制出多边形,一层一层的绘制,就出现了颜色不同的圈,再通过能力值和所在的能力对应的半径计算出这个能力点在这条半径上所在的位置,也就是坐标,然后通过path就能绘制出能力的线。至于文字的话就是最外圈的顶点半径延长一点所在的坐标点绘制文字即可。
数学知识
要在自定义控件中绘制一个正多边形,也就是说比较标准、对称的多边形就要用到一点数学几何中的知识。
如图,在一平面直角坐标系上有一点P且坐标为(x,y),原点到P点的距离为r,且这两点所在的直线与X轴正半轴形成的夹角为θ,那么:
通过正余弦定理,能得到关系式:
x=rcosθ
y=rsinθ
如果不懂正余弦定理的自行百度或者看看高中的数学书,很简单,勾股定理推广得来的。
通过这个关系式,其实我们就能知道
在平面直角坐标系上的任意一点,都可以通过这个点到原点的距离(r),和两点所在的直线与X轴正半轴的夹角(θ)表示出来。
这里的夹角(θ)还需要注意一点,是X正半轴按逆时针的方向旋转然后得到的夹角,如图。
这样、我们想要一个点在平面的位置通过r和θ就能得到,而我们所绘制的多边形这个两个值其实是很好拿到的。
半径r:我们自定定义,这个值用来控制多边形的大小
角度θ:这个值是算出来的。
我们要画正七边形,通过图可以看出来,就分成了7份,而我们都知道,一周是360°
那么θ=360° / 7 ,就得到了θ,
不过需要注意的是,度数(°)是60进制的,而我们直接用360° / 7 得到的θ去参与坐标运算(10进制)是有问题的,所以这里应该是θ=2π / 7 ,π是弧度(π = 180°),而弧度是10进制的,这样就没有问题了。
最后需要注意一点的是,数学上的笛卡尔坐标系(直角坐标系)的如上面的图,Y轴的正向是向上的,而安卓中的视图坐标系的Y轴是向下的。
所以、我们在计算点的时候,注意是顺时针方向的(数学上是逆时针方向的)。
具体实现
AbilityBean
首先我们需要一个数据的实体类,通过效果图可以看出来,只需要7个能力的文字描述以及7个能力的能力值,这里能力值就用整形0~100,代表这个能力值是百分之多少。
public class AbilityBean { //有哪个些能力 public static final String[] abilitys = {"击杀", "生存", "助攻", "物理", "魔法", "防御", "金钱"}; //每个能力的值,范围0~100,单位% private int kill; private int survival; private int assist; private int ad; private int ap; private int defense; private int money; public AbilityBean(int kill, int survival, int assist, int ad, int ap, int defense, int money) { this.kill = kill; this.survival = survival; this.assist = assist; this.ad = ad; this.ap = ap; this.defense = defense; this.money = money; } public static String[] getAbilitys() { return abilitys; } public int getKill() { return kill; } public void setKill(int kill) { this.kill = kill; } public int getSurvival() { return survival; } public void setSurvival(int survival) { this.survival = survival; } public int getAssist() { return assist; } public void setAssist(int assist) { this.assist = assist; } public int getAd() { return ad; } public void setAd(int ad) { this.ad = ad; } public int getAp() { return ap; } public void setAp(int ap) { this.ap = ap; } public int getDefense() { return defense; } public void setDefense(int defense) { this.defense = defense; } public int getMoney() { return money; } public void setMoney(int money) { this.money = money; } public int[] getAllAbility() { int[] allAbility = {kill, survival, assist, ad, ap, defense, money}; return allAbility; }12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
控件参数
private AbilityBean data; //元数据 private int n; //边的数量或者能力的个数 private float R; //最外圈的半径,顶点到中心点的距离 private int intervalCount; //间隔数量,就把半径分为几段 private float angle; //两条顶点到中线点的线之间的角度 private Paint linePaint; //画线的笔 private Paint textPaint; //画文字的笔 private int viewHeight; //控件宽度 private int viewWidth; //控件高度 private ArrayList<ArrayList<PointF>> pointsArrayList; //存储多边形顶点数组的数组 private ArrayList<PointF> abilityPoints; //存储能力点的数组12345678910111213
部分参数示意图
初始化
由于硬件加速会引起自定义view出现问题,我们这里需要关闭硬件加速。
关闭硬件加速的方法是在AndroidManifest.xml里加入一句
android:hardwareAccelerated=”false”
放在< application />节点下表示关闭整个项目的硬件加速
放在< activity />下表示关闭该组件硬件加速
创建一个AbilityMapView类并继承View,实现1~3个参数的构造,并在1和2个参数的构造函数中依次调用。
public AbilityMapView(Context context) { //这地方改为this,使得不管怎么初始化都会进入第三个构造函数中 this(context, null); } public AbilityMapView(Context context, @Nullable AttributeSet attrs) { //这地方改为this,使得不管怎么初始化都会进入第三个构造函数中 this(context, attrs, 0); } public AbilityMapView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initSize(context); initPoints(); initPaint(context); }12345678910111213141516171819
在第三个构造函数中依次调用:initSize(context)、initPoints(context)、initPaint()。
/** * 初始化一些固定数据 * * @param context */ private void initSize(Context context) { n = 7; //七条边 R = dp2pxF(context, 100); //半径暂时设为100dp intervalCount = 4; //有四层 angle = (float) ((2 * Math.PI) / n); //一周是2π,这里用π,因为进制的问题,不能用360度,画出来会有问题 //拿到屏幕的宽高,单位是像素 int screenWidth = getResources().getDisplayMetrics().widthPixels; //控件设置为正方向 viewWidth = screenWidth; viewHeight = screenWidth; } /** * 初始化多边形的所有点 每一圈7个点,有4圈 */ private void initPoints() { //一个数组中每个元素又一是一个点数组,有几个多边形就有几个数组 pointsArrayList = new ArrayList<>(); float x; float y; for (int i = 0; i < intervalCount; i++) { //创建一个存储点的数组 ArrayList<PointF> points = new ArrayList<>(); for (int j = 0; j < n; j++) { float r = R * ((float) (4 - i) / intervalCount); //每一圈的半径都按比例减少 //这里减去Math.PI / 2 是为了让多边形逆时针旋转90度,所以后面的所有用到cos,sin的都要减 x = (float) (r * Math.cos(j * angle - Math.PI / 2)); y = (float) (r * Math.sin(j * angle - Math.PI / 2)); points.add(new PointF(x, y)); } pointsArrayList.add(points); } } /** * 初始化画笔 * * @param context */ private void initPaint(Context context) { //画线的笔 linePaint = new Paint(Paint.ANTI_ALIAS_FLAG); //设置线宽度 linePaint.setStrokeWidth(dp2px(context, 1f)); //画文字的笔 textPaint = new Paint(Paint.ANTI_ALIAS_FLAG); textPaint.setTextAlign(Paint.Align.CENTER); //设置文字居中 textPaint.setColor(Color.BLACK); textPaint.setTextSize(sp2pxF(context, 14f)); }12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
在initSize我把viewWidth和viewHeigh都设置为了屏幕宽度,因为这里弄成大一点的正方形好看一点。
initPoints中,是初始化了多边形的坐标,具体逻辑就是用到了开篇讲的数学知识,相信读者配合注释一眼就能看明白,需要注意的是这里是两个循环,并且把点装在了二维数组中(就是数组中的元素还是数组),因为我们这里要绘制4个多边形。而每一层的多边形中的半径都按比例减小。
接下来我们需要给view设置元数据,所以提供一个对外公开方法:setData()
/** * 传入元数据 * * @param data */ public void setData(AbilityBean data) { if (data == null) { return; } this.data = data; //View本身调用迫使view重画 invalidate(); }1234567891011121314
重写onMeasure、onSizeChanged
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //设置控件的最终视图大小(宽高) setMeasuredDimension(viewWidth, viewHeight); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); initSize(getContext()); }12345678910111213
onDraw
好了,终于到了最核心的地方了,这里绘制控件、编写核心逻辑的地方。
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //把画布的原点移动到控件的中心点 canvas.translate(viewWidth / 2, viewHeight / 2); drawPolygon(canvas); drawOutLine(canvas); drawAbilityLine(canvas); drawAbilityText(canvas); }12345678910111213141516
在onDraw中我们首先把画布的坐标原点移动到控件的中心点,因为默认画布的原点在左上角,而且我们算多边形顶点坐标的时候原点也是在控件中心。然后分了4步来绘制这个控件,下面来一一讲解每一步。
1.drawPolygon(canvas)
/** * 绘制多边形框,每一层都绘制 * * @param canvas */ private void drawPolygon(Canvas canvas) { canvas.save();//保存画布当前状态(平移、放缩、旋转、裁剪等),和canvas.restore()配合使用 linePaint.setStyle(Paint.Style.FILL_AND_STROKE); //设置为填充且描边 Path path = new Path(); //路径 for (int i = 0; i < intervalCount; i++) { //循环、一层一层的绘制 //每一层的颜色都都不同 switch (i) { case 0: linePaint.setColor(Color.parseColor("#D4F0F3")); break; case 1: linePaint.setColor(Color.parseColor("#99DCE2")); break; case 2: linePaint.setColor(Color.parseColor("#56C1C7")); break; case 3: linePaint.setColor(Color.parseColor("#278891")); break; } for (int j = 0; j < n; j++) { //每一层有n个点 float x = pointsArrayList.get(i).get(j).x; float y = pointsArrayList.get(i).get(j).y; if (j == 0) { //如果是每层的第一个点就把path的起点设置为这个点 path.moveTo(x, y); } else { path.lineTo(x, y); } } path.close(); //设置为闭合的 canvas.drawPath(path, linePaint); path.reset(); //清除path存储的路径 } canvas.restore(); }1234567891011121314151617181920212223242526272829303132333435363738394041424344
这里就是绘制多边形的,由于我们前面已经把多边形的顶点都出计算出来了,所以这里我们只需通过循环给Path设置好路径,然后一层一层的画,通过switch为每一层设置不同的颜色,就能看出层次感,一圈一圈的。需要注意的是要调用linePaint.setStyle(Paint.Style.FILL_AND_STROKE);,因为我们是要绘制实心的。
效果图:
2.drawOutLine(Canvas canvas)
/** * 画轮廓线 * 1.先画最外面的多边形轮廓 * 2.再画顶点到中心的线 * * @param canvas */ private void drawOutLine(Canvas canvas) { canvas.save();//保存画布当前状态(平移、放缩、旋转、裁剪等),和canvas.restore()配合使用 linePaint.setColor(Color.parseColor("#99DCE2")); linePaint.setStyle(Paint.Style.STROKE); //设置空心的 //先画最外面的多边形轮廓 Path path = new Path(); //路径 for (int i = 0; i < n; i++) { //只需要第一组的点 float x = pointsArrayList.get(0).get(i).x; float y = pointsArrayList.get(0).get(i).y; if (i == 0) { //如果是第一个点就把path的起点设置为这个点 path.moveTo(x, y); } else { path.lineTo(x, y); } } path.close(); //闭合路径 canvas.drawPath(path, linePaint); //再画顶点到中心的线 for (int i = 0; i < n; i++) { float x = pointsArrayList.get(0).get(i).x; float y = pointsArrayList.get(0).get(i).y; canvas.drawLine(0, 0, x, y, linePaint); //起点都是中心点 } canvas.restore(); }1234567891011121314151617181920212223242526272829303132333435363738
这里是画轮廓线的,因为我们只需要画最外圈的轮廓和半径线、所以只需要最外圈的7个点和中心点原点就够了,外圈用Path来画,注意这里画笔就要设置linePaint.setStyle(Paint.Style.STROKE),要空心的,还有别忘了是闭合路径,调用path.close()。而半径线就循环7次一根一根画出来就可以了,看注释,很简单。
效果图:
3.drawAbilityLine(Canvas canvas)
/** * 画能力线 * * @param canvas */ private void drawAbilityLine(Canvas canvas) { canvas.save(); //先把能力点初始化出来 abilityPoints = new ArrayList<>(); int[] allAbility = data.getAllAbility(); for (int i = 0; i < n; i++) { float r = R * (allAbility[i] / 100.0f); //能力值/100再乘以半径就是所占的比例 float x = (float) (r * Math.cos(i * angle - Math.PI / 2)); float y = (float) (r * Math.sin(i * angle - Math.PI / 2)); abilityPoints.add(new PointF(x, y)); } linePaint.setStrokeWidth(dp2px(getContext(), 2f)); linePaint.setColor(Color.parseColor("#E96153")); linePaint.setStyle(Paint.Style.STROKE); //设置空心的 Path path = new Path(); //路径 for (int i = 0; i < n; i++) { float x = abilityPoints.get(i).x; float y = abilityPoints.get(i).y; if (i == 0) { path.moveTo(x, y); } else { path.lineTo(x, y); } } path.close(); //别忘了闭合 canvas.drawPath(path, linePaint); canvas.restore(); }1234567891011121314151617181920212223242526272829303132333435363738
这里就是画能力线了,首先我们需要根据能力值把能力点计算出来,这里思路就是我们把原点到顶点的距离看成是0~100%,然后看能力值是多少,就在这个能力所对应的半径上哪个位置,能力值越低就离原点越近,通过能力点的半径就能算出坐标,因为角度已经全部是一样的了,然后这里画笔设置空心的linePaint.setStyle(Paint.Style.STROKE);通过闭合的Path就能画出来。
效果图:
3.drawAbilityText(Canvas canvas)
/** * 画能力描述的文字 * * @param canvas */ private void drawAbilityText(Canvas canvas) { canvas.save(); //先计算出坐标来 ArrayList<PointF> textPoints = new ArrayList<>(); for (int i = 0; i < n; i++) { float r = R + dp2pxF(getContext(), 15f); float x = (float) (r * Math.cos(i * angle - Math.PI / 2)); float y = (float) (r * Math.sin(i * angle - Math.PI / 2)); textPoints.add(new PointF(x, y)); } //拿到字体测量器 Paint.FontMetrics metrics = textPaint.getFontMetrics(); String[] abilitys = AbilityBean.getAbilitys(); for (int i = 0; i < n; i++) { float x = textPoints.get(i).x; //ascent:上坡度,是文字的基线到文字的最高处的距离 //descent:下坡度,,文字的基线到文字的最低处的距离 float y = textPoints.get(i).y - (metrics.ascent + metrics.descent) / 2; canvas.drawText(abilitys[i], x, y, textPaint); } canvas.restore(); }12345678910111213141516171819202122232425262728
这里很简单,我们只需要把半径R稍微延迟一点跟前面的计算方法一样,就能计算出坐标来,然后绘制文字即可,具体看注释。
效果图:
到了这里,能力七星图的自定义View就算是大功告成了。
编写完了自定义View的类别忘了添加到Activity里
activity_main.xml
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context="com.fu.abilitymapview.MainActivity"> <com.fu.abilitymapview.AbilityMapView android:id="@+id/ability_map_view" android:layout_width="match_parent" android:layout_height="match_parent" /></LinearLayout>1234567891011121314
MainActivity
public class MainActivity extends AppCompatActivity { private AbilityMapView abilitymapview; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); this.abilitymapview = (AbilityMapView) findViewById(R.id.ability_map_view); abilitymapview.setData(new AbilityBean(65, 70, 80, 70, 80, 80, 80)); } }123456789101112
补充说明
1.为什么每个点计算的时候sin、cos里面的角度都减去π/2?
为了使绘制出来的图形逆时针旋转90°,形成以Y轴为轴的对称图形。因为这样计算出来的点的坐标都是向逆时针方向旋转了90°,自然绘制出来的图形也旋转了90°
如果我不减去π/2呢,是个什么效果呢?
这样就是以X轴为轴的对称图形了,看上没那么舒服了,对吧。所以要减去π/2。
2.辅助坐标轴
在刚开始绘制的时候可以加个坐标坐标轴来辅助我们绘制。只需在onDraw中加三行代码。
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //把画布的原点移动到控件的中心点 canvas.translate(viewWidth / 2, viewHeight / 2); drawPolygon(canvas); drawOutLine(canvas); drawAbilityLine(canvas); drawAbilityText(canvas); //坐标轴x,y 辅助用 linePaint.setColor(Color.RED); canvas.drawLine(-(viewWidth / 2), 0, viewWidth / 2, 0, linePaint); canvas.drawLine(0, -(viewWidth / 2), 0, viewWidth / 2, linePaint); }1234567891011121314151617181920
注意一定要放在onDraw中的最后面,这样坐标轴才不会被覆盖而看不到。
效果图:
3.颜色提取
颜色提取器下载地址:https://github.com/qq908323236/AbilityMapView/blob/master/Colors.rar