本系列为记录学习自定义控件的文章,尽管Google为我们提供了各式各样的的原生控件,但是大多数时候,原生控件并不能满足项目的需求,这时候就需要自定义控件了。
那么,
自定义控件是什么?怎么写?怎么用?
懵逼三连!!!
没错,你是否正处于这样尴尬的局面,是否一提到自定义控件就望而却步,没关系,下面跟我一起走进自定义控件的奇妙世界。
注:本文中出现的所有图片均来自网络,如有侵权,请联系我删除,谢谢。
绘图基础
Android当中的绘图和我们平时画图其实是一样的,无论在精美的图画,都是通过两样工具画出来的,画笔和画布,那么在Android中画笔和画布又是什么呢?
Paint(画笔)
具有设置颜色、填充样式、大小等特性。
Canvas(画布)
具有旋转、位移、缩放等特性。
其实这两个工具,具有很多特性,这里就不一一赘述了。回到主题,使用绘图工具画一个五角星,要实现这样的自定义控件,就不得不提到Android当中的坐标系,因为自定义控件很多时候都涉及到坐标的计算,角度的计算,已经弧度的计算。
要知道,Android坐标系和数学坐标系是有区别的,如图所示:
数学坐标系
Android坐标系
在解释图2、3之前,先来了解下Android中的坐标原点,见图4、5:
在Android中,屏幕左上角为坐标原点,既如图4所示,以原点垂直向右为X轴正方向,垂直向下Y轴正方向,并且Android中的角度坐标系和数学的角度坐标系也不一样,以原点竖直向下为90°,竖直向上为270°,如图5所示。讲到这里,想必图2、3应该就很容易理解了。
既然说到Android的角度坐标系,不妨说下Android中的正负角度,这对本文主题至关重要,见图1:
先假设红点为A,则∠p1OA=-30°,∠AOp2=30°,可能很奇怪,为什么∠p1OA为负?
前面已经讲到,Android角度坐标系同样区别与数学角度坐标系,想必聪明的你已经发现了区别在哪里。
在Android中,以OA为轴,顺时针为角度增大方向,逆时针为角度负增大方向。
与角度对应的还有一个弧度的概念,简单来说角度和弧度都是为了描述一个角的大小,但是两者也有区别,角度是60进制,弧度是10进制,并且定义也不相同:
角度: 两条射线从圆心向圆周射出,形成一个夹角和夹角正对的一段弧。当这段弧长正好等于圆周长的360分之一时,两条射线的夹角的大小为1度.
弧度: 两条射线从圆心向圆周射出,形成一个夹角和夹角正对的一段弧。当这段弧长正好等于圆的半径时,两条射线的夹角大小为1弧度.
如图6、7所示:
言归正传,回到本文,画一个五角星,先来看下效果图,如图9所示:
分析
五角星其实就是由10的点连接组成,关键是怎么算出这10的点的坐标,仔细想一想,把一个圆平均分成五份,以圆心为起点,画五条直线把圆分成均等五份,五条直线与圆周的交点假设为A、B、C、D、E(逆时针方向),然后按照A-D-B-E-C-A,连接起来,是不是就构成一个互相交错的五角星?
画圆
画圆很简单,先计算圆心坐标以及半径,这里就不再赘述,直接看代码:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
radius = Math.min(w, h) / 2 * 0.9f;
secondRadius = radius / 2;
centerX = w / 2;
centerY = h / 2;
postInvalidate();
super.onSizeChanged(w, h, oldw, oldh);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(centerX, centerY, radius, mPaint);
}
外五点坐标 A-E
如果你不熟悉三角函数,请自行了解。既然提到三角函数,请注意,在Android中,Math.cos()函数中,这里的参数是弧度而不是角度。
角度与弧度的换算公式:
弧度 = 角度 * Math.PI /180°
角度 = 弧度 * 180° / Math.PI
同样在Android中可以使用Math函数来计算:
角度: Math.toDegrees()
弧度:Math.toRadians()
A点:
A点,相对于其他几点是最简单的一个,因为A点并不涉及到弧度以及三角函数的计算。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(centerX, centerY, radius, mPaint);
Path path = new Path();
path.moveTo(centerX,centerY);
//A
path.lineTo(centerX, centerY - radius);
canvas.drawPath(path,mPaint);
}
如图10所示:
E点:
为了方便讲解,先见图11所示:
圆中间有两条直线,顺时针方向,第一条为OA,第二条为OE,把圆平均分成五份,则每两条相邻直线所形成的角度=360/5=72°,那么OE与X轴所形成的角度同样通过计算可知90-72=18°,别忘了,在Android中,逆时针方向为负,则∠EOX = -18°,∠EOX所对应的弧长=Math.toRadians(-18),弧长得到,通过三角函数便可得到E点的坐标,最后比忘了加上圆心的X、Y坐标。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(centerX, centerY, radius, mPaint);
Path path = new Path();
path.moveTo(centerX,centerY);
//A
path.lineTo(centerX, centerY - radius);
//E
path.lineTo(centerX,centerY);
path.lineTo(centerX + (float) Math.cos(Math.toRadians(-18)) * radius, centerY + (float) Math.sin(Math.toRadians(-18)) * radius);
canvas.drawPath(path,mPaint);
}
D点:
同理可知,∠EOD=72°,∠EOX = 18°,则∠XOD = 72-18 = 54°,既∠XOD所对应的弧长为Math.toRadians(54)
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(centerX, centerY, radius, mPaint);
Path path = new Path();
path.moveTo(centerX,centerY);
//A
path.lineTo(centerX, centerY - radius);
//E
path.lineTo(centerX,centerY);
path.lineTo(centerX + (float) Math.cos(Math.toRadians(-18)) * radius, centerY + (float) Math.sin(Math.toRadians(-18)) * radius);
//D
path.lineTo(centerX,centerY);
path.lineTo(centerX + (float) Math.cos(Math.toRadians(54)) * radius, centerY + (float) Math.sin(Math.toRadians(54)) * radius);
canvas.drawPath(path,mPaint);
}
C、B点:
同理,通过计算可知C点角度=126°,B点角度=198°
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(centerX, centerY, radius, mPaint);
Path path = new Path();
path.moveTo(centerX,centerY);
//A
path.lineTo(centerX, centerY - radius);
//E
path.lineTo(centerX,centerY);
path.lineTo(centerX + (float) Math.cos(Math.toRadians(-18)) * radius, centerY + (float) Math.sin(Math.toRadians(-18)) * radius);
//D
path.lineTo(centerX,centerY);
path.lineTo(centerX + (float) Math.cos(Math.toRadians(54)) * radius, centerY + (float) Math.sin(Math.toRadians(54)) * radius);
//C
path.lineTo(centerX,centerY);
path.lineTo(centerX + (float) Math.cos(Math.toRadians(126)) * radius, centerY + (float) Math.sin(Math.toRadians(126)) * radius);
//B
path.lineTo(centerX,centerY);
path.lineTo(centerX + (float) Math.cos(Math.toRadians(180 + 18)) * radius, centerY + (float) Math.sin(Math.toRadians(180 + 18)) * radius);
canvas.drawPath(path,mPaint);
}
A-E坐标已经确定完毕,先来看下效果图,如图12所示:
这样已经把圆均分为5等份,但是这好像并没有满足我们的需求,没关系,A-E(逆时针方向)五个点的坐标已经计算出来,接下来就按照之前规定好的顺序连接起来便可,A-D-B-E-C-A
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(centerX, centerY, radius, mPaint);
Path path = new Path();
path.moveTo(centerX, centerY - radius);
path.lineTo(centerX + (float) Math.cos(Math.toRadians(54)) * radius, centerY + (float) Math.sin(Math.toRadians(54)) * radius);
path.lineTo(centerX + (float) Math.cos(Math.toRadians(180 + 18)) * radius, centerY + (float) Math.sin(Math.toRadians(180 + 18)) * radius);
path.lineTo(centerX + (float) Math.cos(Math.toRadians(-18)) * radius, centerY + (float) Math.sin(Math.toRadians(-18)) * radius);
path.lineTo(centerX + (float) Math.cos(Math.toRadians(126)) * radius, centerY + (float) Math.sin(Math.toRadians(126)) * radius);
path.close();
canvas.drawPath(path,mPaint);
}
如图13所示:
可以看到,一个互相连接的五角星已经形成,但是看上去好像和效果图有一些不太一样,别急,前文提到,要实现一个效果图中的五角星,其实就是由10个点连接而成,现在完成只是最外边的5个点而已。
仔细回想一下,外边的5个点,是把一个圆均分为五份,然后取边上的5个点,那么里边的5点,是不是可以以相同的方法获取,圆心相同,半径为外圆的一半,然后再均分为五份,两者唯一的区别就是角度和弧度不同。
内五点坐标 F、G、H、I、K(顺时针方向)
先画一个同心圆
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//外圆
canvas.drawCircle(centerX, centerY, radius, mPaint);
//内圆
canvas.drawCircle(centerX, centerY, secondRadius, leftPaint);
Path path = new Path();
path.moveTo(centerX, centerY - radius);
path.lineTo(centerX + (float) Math.cos(Math.toRadians(54)) * radius, centerY + (float) Math.sin(Math.toRadians(54)) * radius);
path.lineTo(centerX + (float) Math.cos(Math.toRadians(180 + 18)) * radius, centerY + (float) Math.sin(Math.toRadians(180 + 18)) * radius);
path.lineTo(centerX + (float) Math.cos(Math.toRadians(-18)) * radius, centerY + (float) Math.sin(Math.toRadians(-18)) * radius);
path.lineTo(centerX + (float) Math.cos(Math.toRadians(126)) * radius, centerY + (float) Math.sin(Math.toRadians(126)) * radius);
path.close();
canvas.drawPath(path, mPaint);
}
为了便于理解,连接五角星同样保留,如图14所示:
先忽略红色的外圆与连接的五角星,只看蓝色的内圆,前面讲到只需要在内圆上同样找到降内圆均分五份的内点,在与外圆上的五点依次连接便可完成开篇讲到的五角星,其实找点的方法已经讲过了,内圆与外圆的区别只是角度不同而已。为了便于讲解,见图8所示:
F点
逆时针方向A-E为外五点,顺时针方向F-K为内五点,十条直线两条相邻直线所形成的角度=360/10=36°,直线圆心到F(简称F直线,下文皆使用简称)与X轴所形成的角度为=36+18=-54°,切记在Android中逆时针方向为负。通过外五点的计算很容易得到F点坐标的计算方法。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//外圆
canvas.drawCircle(centerX, centerY, radius, mPaint);
//内圆
canvas.drawCircle(centerX, centerY, secondRadius, leftPaint);
Path path = new Path();
path.moveTo(centerX, centerY - radius);
path.lineTo(centerX + (float) Math.cos(Math.toRadians(54)) * radius, centerY + (float) Math.sin(Math.toRadians(54)) * radius);
path.lineTo(centerX + (float) Math.cos(Math.toRadians(180 + 18)) * radius, centerY + (float) Math.sin(Math.toRadians(180 + 18)) * radius);
path.lineTo(centerX + (float) Math.cos(Math.toRadians(-18)) * radius, centerY + (float) Math.sin(Math.toRadians(-18)) * radius);
path.lineTo(centerX + (float) Math.cos(Math.toRadians(126)) * radius, centerY + (float) Math.sin(Math.toRadians(126)) * radius);
path.close();
canvas.drawPath(path, mPaint);
Path path1 = new Path();
//F
path1.moveTo(centerX, centerY);
path1.lineTo(centerX + (float) Math.cos(Math.toRadians(-(72 / 2 + 18))) * secondRadius,
centerY + (float) Math.sin(Math.toRadians(-(72 / 2 + 18))) * secondRadius);
canvas.drawPath(path1,leftPaint);
}
如图15所示:
G、H、I、K点
这四个点和F点计算方法是一样,关于在于角度和弧度的计算,因此不再一一赘述。通过计算可知四点角度分别为:
G:18
H:90
I:36 * 4 + 18
K:36 * 6 + 18
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//外圆
canvas.drawCircle(centerX, centerY, radius, mPaint);
//内圆
canvas.drawCircle(centerX, centerY, secondRadius, leftPaint);
Path path = new Path();
path.moveTo(centerX, centerY - radius);
path.lineTo(centerX + (float) Math.cos(Math.toRadians(54)) * radius, centerY + (float) Math.sin(Math.toRadians(54)) * radius);
path.lineTo(centerX + (float) Math.cos(Math.toRadians(180 + 18)) * radius, centerY + (float) Math.sin(Math.toRadians(180 + 18)) * radius);
path.lineTo(centerX + (float) Math.cos(Math.toRadians(-18)) * radius, centerY + (float) Math.sin(Math.toRadians(-18)) * radius);
path.lineTo(centerX + (float) Math.cos(Math.toRadians(126)) * radius, centerY + (float) Math.sin(Math.toRadians(126)) * radius);
path.close();
canvas.drawPath(path, mPaint);
Path path1 = new Path();
//F
path1.moveTo(centerX, centerY);
path1.lineTo(centerX + (float) Math.cos(Math.toRadians(-(72 / 2 + 18))) * secondRadius,
centerY + (float) Math.sin(Math.toRadians(-(72 / 2 + 18))) * secondRadius);
//G
path1.lineTo(centerX, centerY);
path1.lineTo(centerX + (float) Math.cos(Math.toRadians(18)) * secondRadius,
centerY + (float) Math.sin(Math.toRadians(18)) * secondRadius);
//H
path1.lineTo(centerX, centerY);
path1.lineTo(centerX + (float) Math.cos(Math.toRadians(90)) * secondRadius,
centerY + (float) Math.sin(Math.toRadians(90)) * secondRadius);
//I
path1.lineTo(centerX, centerY);
path1.lineTo(centerX + (float) Math.cos(Math.toRadians(36 * 4 + 18)) * secondRadius,
centerY + (float) Math.sin(Math.toRadians(18 + 36 * 4)) * secondRadius);
//K
path1.lineTo(centerX, centerY);
path1.lineTo(centerX + (float) Math.cos(Math.toRadians(36 * 6 + 18)) * secondRadius,
centerY + (float) Math.sin(Math.toRadians(18 + 36 * 6)) * secondRadius);
canvas.drawPath(path1, leftPaint);
}
如图16所示:
图8和图16对比可以看出,红色外圆上的五点逆时针方向分别为A-E,蓝色内圆上的五点顺时针方向分别为F-K,根据开篇得知五角星,就是通过10个点连接而成,现在10个点的坐标已经求出,按照图8所示,只需要按照A-F-E-G-D-H-C-I-B-K-A顺序连接,便可形成一个五角星,光说不做假把式,直接看代码。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//把10个点连接起来 A-F-E-G-D-H-C-I-B-K-A
Path path = new Path();
path.moveTo(centerX, centerY - radius);
path.lineTo(centerX + (float) Math.cos(Math.toRadians(-(72 / 2 + 18))) * secondRadius,
centerY + (float) Math.sin(Math.toRadians(-(72 / 2 + 18))) * secondRadius);
path.lineTo(centerX + (float) Math.cos(Math.toRadians(-18)) * radius, centerY + (float) Math.sin(Math.toRadians(-18)) * radius);
path.lineTo(centerX + (float) Math.cos(Math.toRadians(18)) * secondRadius,
centerY + (float) Math.sin(Math.toRadians(18)) * secondRadius);
path.lineTo(centerX + (float) Math.cos(Math.toRadians(54)) * radius, centerY + (float) Math.sin(Math.toRadians(54)) * radius);
path.lineTo(centerX + (float) Math.cos(Math.toRadians(90)) * secondRadius,
centerY + (float) Math.sin(Math.toRadians(90)) * secondRadius);
path.lineTo(centerX + (float) Math.cos(Math.toRadians(126)) * radius, centerY + (float) Math.sin(Math.toRadians(126)) * radius);
path.lineTo(centerX + (float) Math.cos(Math.toRadians(36 * 4 + 18)) * secondRadius,
centerY + (float) Math.sin(Math.toRadians(18 + 36 * 4)) * secondRadius);
path.lineTo(centerX + (float) Math.cos(Math.toRadians(180 + 18)) * radius, centerY + (float) Math.sin(Math.toRadians(180 + 18)) * radius);
path.lineTo(centerX + (float) Math.cos(Math.toRadians(36 * 6 + 18)) * secondRadius,
centerY + (float) Math.sin(Math.toRadians(18 + 36 * 6)) * secondRadius);
path.close();
canvas.drawPath(path, mPaint);
}
如图17所示:
至于为什么是空心的,只是画笔Paint的样式为描边,改为填充便可实现和开篇一样的效果了,当然还可以实现半边填充,如图18所示:
除此之外,还有一个蜘蛛网,只要掌握了计算方法,一样很好实现,放一张效果图,源码在文末。
Android自定义控件远不止这些,这只是长征路上的第一步,不过不要气馁,越过眼前的这座山,便可进入自定义控件的美好世界,加油,我与你并肩前行!!!!
五角星
源码:https://github.com/qylfzy/FiveStarDemo