Android 自定义View——打造自己的专属控件
前段时间看到一个天气应用,界面做的很好看。里面一个风车转动动画和一个日出日落的动画挺有意思的,于是自己也照着他的界面做了一个。
先看一下界面
自定义View一般有两种情况
- 继承自原有控件,扩展原有控件的功能。如前面介绍的自定义View实现圆角图片
- 完全自定义控件,实现控件的测量,绘制等方法。打造自己的专属控件。
这里采用的是第二种方法,自己来绘制风车控件。
自定义控件需要继承View,并实现onMeasure,onLayout,onDraw方法。从命名可以看出来这三个方法分别实现了控件的测量,定位和绘制。
先来说一下原理
代码主要工作在onDraw中实现。风车有下面的身体和上面的叶片组成,身体比较简单,就是两条成一定夹角的线,三个叶片成120度夹角,为了简化计算,这里每个叶片由两个三角形组合,计算好每个顶点的坐标之后用path来连线。上面比较麻烦的地方是每个顶点的计算,用到了三角函数,对于我这种大一高数学完之后就没有碰过三角函数的人来说顶点的计算真是一种煎熬ORZ(每次绘制的不对了,sin cos换一下,或者加减换一下,差点开始怀疑人生了)。
风车绘制好之后我们还要让它动起来,我们通过mAngel来控制角度,通过不断的改变角度,调用invalidate来实现旋转动画。
下面我们来实现风车的绘制
public class WindMillView extends View {
private static final int DEFAULT_COLOR = Color.WHITE;
private static final int DEFAULT_WIDTH = 1;//画笔宽度1dp
private static final float LENGTH_1 = 5;//下面三角形高度5dp
private static final float ALPHA = (float) (Math.PI / 6);//旋转角度
private static final int DELAY = 30;
private Paint mPaint;
private Path mPath;
private float mAngle = 0;//旋转角度 通过改变角度实现旋转动画
public WindMillView(Context context) {
this(context, null);
}
public WindMillView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public WindMillView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
//初始化
private void init() {
mPaint = new Paint();
mPath = new Path();
mPaint.setColor(DEFAULT_COLOR);
mPaint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mAngle = (float) (mAngle + 3 * Math.PI / 360);
float centerX = getWidth() / 2;
float centerY = getHeight() * 4 / 9.0f;
mPaint.setStrokeWidth(Utils.dp2px(getContext(), DEFAULT_WIDTH));
//绘制风车的身体
canvas.drawLine(centerX, centerY, centerX - getWidth() / 10, getHeight(), mPaint);
canvas.drawLine(centerX, centerY, centerX + getWidth() / 10, getHeight(), mPaint);
//绘制叶片 叶片由两个三角形组成 length1是下面三角形的高 length为整个叶片长度
float length = (float) (Utils.dp2px(getContext(), LENGTH_1) * Math.sin(ALPHA)
+ getHeight() * 2 / 9.0f);
float length1 = Utils.dp2px(getContext(), LENGTH_1);
//分别计算叶片4个顶点的坐标 通过path来绘制
float alpha = (float) (Math.PI / 2 - ALPHA + mAngle);
mPath.moveTo(centerX, centerY);
mPath.lineTo((float) (centerX + length1 * Math.cos(alpha)),
(float) (centerY - length1 * Math.sin(alpha)));
mPath.lineTo((float) (centerX + length * Math.cos(Math.PI / 2 + mAngle)),
(float) (centerY - length * Math.sin(Math.PI / 2 + mAngle)));
mPath.lineTo((float) (centerX + length1 * Math.cos(alpha + 2 * ALPHA)),
(float) (centerY - length1 * Math.sin(alpha + 2 * ALPHA)));
mPath.close();
canvas.drawPath(mPath, mPaint);
//叶片之间夹角是2/3PI
alpha = (float) (Math.PI / 2 - ALPHA + mAngle + Math.PI * 2 / 3);
mPath.moveTo(centerX, centerY);
mPath.lineTo((float) (centerX + length1 * Math.cos(alpha)),
(float) (centerY - length1 * Math.sin(alpha)));
mPath.lineTo((float) (centerX + length * Math.cos(Math.PI / 2 + mAngle + Math.PI * 2 / 3)),
(float) (centerY - length * Math.sin(Math.PI / 2 + mAngle + Math.PI * 2 / 3)));
mPath.lineTo((float) (centerX + length1 * Math.cos(alpha + 2 * ALPHA)),
(float) (centerY - length1 * Math.sin(alpha + 2 * ALPHA)));
mPath.close();
canvas.drawPath(mPath, mPaint);
alpha = (float) (Math.PI / 2 - ALPHA + mAngle - Math.PI * 2 / 3);
mPath.moveTo(centerX, centerY);
mPath.lineTo((float) (centerX + length1 * Math.cos(alpha)),
(float) (centerY - length1 * Math.sin(alpha)));
mPath.lineTo((float) (centerX + length * Math.cos(Math.PI / 2 + mAngle - Math.PI * 2 / 3)),
(float) (centerY - length * Math.sin(Math.PI / 2 + mAngle - Math.PI * 2 / 3)));
mPath.lineTo((float) (centerX + length1 * Math.cos(alpha + 2 * ALPHA)),
(float) (centerY - length1 * Math.sin(alpha + 2 * ALPHA)));
mPath.close();
canvas.drawPath(mPath, mPaint);
mPath.reset();
postInvalidateDelayed(DELAY);
}
}
上面的代码里面所有的顶点都是由计算得到,整个过程看起来比较繁琐,涉及到很多三角函数的计算,实际上,三个叶片是一样的,只是绕着center旋转了一定角度而已,所以,我们只要计算好其中一个叶片之后,其他的叶片顶点就是这个叶片的顶点绕center旋转一定角度。
上述的计算中,我们在计算好一个叶片之后,通过一定旋转角度,用三角函数来重新计算别的叶片的顶点坐标,这对数学好的人应该是比较容易理解的,但是对与我这样数学不好的人来说简直要开始怀疑人生ORZ。好在Android中有更简单的实现方法。刚才我们是画布不动,通过旋转坐标来计算新的坐标,那如果我们坐标不动,旋转一下画布是不是也能达到这种效果。canvas中就提供了旋转画布的方法。
rotate(float degrees, float px, float py)
Preconcat the current matrix with the specified rotation.
canvas的rotate可以让canvas绕一个点旋转一定角度,所以,我们只需要计算出一个叶片的顶点坐标之后再旋转两次就好了,是不是比刚才简单了很多呢。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mAngle = (float) (mAngle + 3 * Math.PI / 360);
float centerX = getWidth() / 2;
float centerY = getHeight() * 4 / 9.0f;
mPaint.setStrokeWidth(Utils.dp2px(getContext(), DEFAULT_WIDTH));
//绘制风车的身体
canvas.drawLine(centerX, centerY, centerX - getWidth() / 10, getHeight(), mPaint);
canvas.drawLine(centerX, centerY, centerX + getWidth() / 10, getHeight(), mPaint);
//绘制叶片 叶片由两个三角形组成 length1是下面三角形的高 length为整个叶片长度
length = (float) (Utils.dp2px(getContext(), LENGTH_1) * Math.sin(ALPHA)
+ getHeight() * 2 / 9.0f);
length1 = Utils.dp2px(getContext(), LENGTH_1);
float alpha = (float) (Math.PI / 2 - ALPHA + mAngle);
drawWindMill(canvas, centerX, centerY, alpha);
canvas.save();
canvas.rotate(120,centerX,centerY);
drawWindMill(canvas, centerX, centerY, alpha);
canvas.restore();
canvas.save();
canvas.rotate(240,centerX,centerY);
drawWindMill(canvas, centerX, centerY, alpha);
canvas.restore();
mPath.reset();
postInvalidateDelayed(DELAY);
}
private void drawWindMill(Canvas canvas, float centerX, float centerY, float alpha) {
mPath.moveTo(centerX, centerY);
mPath.lineTo((float) (centerX + length1 * Math.cos(alpha)),
(float) (centerY - length1 * Math.sin(alpha)));
mPath.lineTo((float) (centerX + length * Math.cos(Math.PI / 2 + mAngle)),
(float) (centerY - length * Math.sin(Math.PI / 2 + mAngle)));
mPath.lineTo((float) (centerX + length1 * Math.cos(alpha + 2 * ALPHA)),
(float) (centerY - length1 * Math.sin(alpha + 2 * ALPHA)));
mPath.close();
canvas.drawPath(mPath, mPaint);
}
除了旋转之外,canvas还提供了平移,缩放等方法,可以大大减少计算。这里要注意的是在进行变换之前先调用save方法保存当前状态,之后通过restore方法恢复。
好了,风车动画就介绍到这里,有什么意见或者建议欢迎交流。