上次看到一个小伙伴实现了牛顿摆,发现挺有意思的,于是乎自己也想着做一个,虽然说是重复造轮子,我看中的是这个过程,顺便巩固一下自定义view,并且我做了一个扩展,可以自定义线和小球的尺寸与颜色,还有初始角度。效果图如下:
其实通过做这个项目可以扩展学习到很多姿势,首当其冲的就是物理姿势(我发现我连重力加速等于多少都忘了,so sad)。通过一步步推算得出角速度与偏移角度的公式(在线编辑数学公式网站):
为啥要算这个角速度与偏移角度的关系?
其实我要的是view绘制的频率,知道了角速度,我们就知道偏移一个单位的角度需要间隔多长的时间,这个时间就是我们绘制的间隔,间隔的长短就可以提现出速度的快慢。
自定义View步骤:
1、新建一个类
继承自View;
2、在构造函数中获取自定义参数:
先在res/values目录下新建attrs.xml文件,定义我们要使用属性的名字:
这样在使用我们的自定义View时就能设置自定义属性:
android:layout_width="300dp"
android:layout_height="200dp"
android:background="#00ee00"
test:ballColor="#CCCCCC"
test:lineColor="#333333"
test:lineWidth="2dp"
test:radius="10dp"
test:initAngle="30"/>
然后就可以在构造函数中获取设置的自定义参数值了:
public NewtonCradle(Context context) {
this(context, null);
}
public NewtonCradle(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public NewtonCradle(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.NewtonCradle, defStyleAttr, 0);
lineColor = typedArray.getColor(R.styleable.NewtonCradle_lineColor, Color.BLACK);
ballColor = typedArray.getColor(R.styleable.NewtonCradle_ballColor, Color.BLACK);
lineWidth = typedArray.getDimension(R.styleable.NewtonCradle_lineWidth, 1f);
radius = typedArray.getDimension(R.styleable.NewtonCradle_radius, 10f);
initAngle = typedArray.getInt(R.styleable.NewtonCradle_initAngle, 10);
swingAngle = initAngle; // 初始偏移角度
typedArray.recycle();
}注意使用完typedArray后释放。
3、在onMeasure方法中计算View的大小
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width;
int height;
int widthSize;
int widthMode;
int heightSize;
int heightMode;
widthSize = MeasureSpec.getSize(widthMeasureSpec);
widthMode = MeasureSpec.getMode(widthMeasureSpec);
heightSize = MeasureSpec.getSize(heightMeasureSpec);
heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else {
width = (int) (2 * lineLength + 10 * radius);
}
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
height = (int) (lineLength + 2 * radius);
}
// 根据宽高获取符合的最小线长
lineLength = (int) Math.min((width-10*radius)/2, height-2*radius);
setMeasuredDimension(width, height);
init();
}这时又百度了一下MeasureSpec的知识,所以只有自己动手才能发现发现知识的死角。当指定view精确大小时,使用指定的大小;没有指定精确大小时,根据线长lineLength和小球半径radius来计算最小宽度和高度,lineLength有一个默认初始值。根据宽高计算最小线长。
同时初始化画笔Paint:
private void init() {
linePaint = new Paint();
linePaint.setColor(lineColor);
linePaint.setStrokeWidth(lineWidth);
linePaint.setStyle(Paint.Style.FILL);
ballPaint = new Paint();
ballPaint.setColor(ballColor);
ballPaint.setStyle(Paint.Style.FILL);
}
4、在onDraw绘制摆球
@Override
protected void onDraw(Canvas canvas) {
int width = getWidth();
// 画中间的圆
canvas.drawLine(width/2, 0, width/2, lineLength, linePaint);
canvas.drawCircle(width/2, lineLength, radius, ballPaint);
// 画左一的圆
canvas.drawLine(width/2-2*radius, 0, width/2-2*radius, lineLength, linePaint);
canvas.drawCircle(width/2-2*radius, lineLength, radius, ballPaint);
// 画右一的圆
canvas.drawLine(width/2+2*radius, 0, width/2+2*radius, lineLength, linePaint);
canvas.drawCircle(width/2+2*radius, lineLength, radius, ballPaint);
pSswingAngle = Math.toRadians(swingAngle);
switch (location) {
case LOCATION_LEFT:
// 画右二的圆
canvas.drawLine(width/2+4*radius, 0, width/2+4*radius, lineLength, linePaint);
canvas.drawCircle(width/2+4*radius, lineLength, radius, ballPaint);
// 画左二的圆
ballX = (int) (width/2-4*radius - lineLength*Math.sin(pSswingAngle));
ballY = (int) (lineLength * Math.cos(pSswingAngle));
canvas.drawLine(width/2-4*radius, 0, ballX, ballY, linePaint);
canvas.drawCircle(ballX, ballY, radius, ballPaint);
delay = (long) Math.sqrt(((long)lineLength) / (2*g*(Math.cos(swingAngle - cosInitAngle))));
if (isLeftSwing) {
swingAngle ++;
if (swingAngle >= initAngle) {
swingAngle = initAngle;
isLeftSwing = false;
}
} else {
swingAngle --;
if (swingAngle <= 0) {
swingAngle = 0;
location = LOCATION_RIGHT;
}
}
break;
case LOCATION_MIDDLE:
// 画左二的圆
canvas.drawLine(width/2-4*radius, 0, width/2-4*radius, lineLength, linePaint);
canvas.drawCircle(width/2-4*radius, lineLength, radius, ballPaint);
// 画右二的圆
canvas.drawLine(width/2+4*radius, 0, width/2+4*radius, lineLength, linePaint);
canvas.drawCircle(width/2+4*radius, lineLength, radius, ballPaint);
if (isLeftSwing)
location = LOCATION_LEFT;
else
location = LOCATION_RIGHT;
delay = 200; // 中间停顿毫秒数
break;
case LOCATION_RIGHT:
// 画左二的圆
canvas.drawLine(width/2-4*radius, 0, width/2-4*radius, lineLength, linePaint);
canvas.drawCircle(width/2-4*radius, lineLength, radius, ballPaint);
// 画右二的圆
ballX = (int) (width/2+4*radius + lineLength*Math.sin(pSswingAngle));
ballY = (int) (lineLength * Math.cos(pSswingAngle));
canvas.drawLine(width/2+4*radius, 0, ballX, ballY, linePaint);
canvas.drawCircle(ballX, ballY, radius, ballPaint);
delay = (long) Math.sqrt(((long)lineLength) / (2*g*(Math.cos(swingAngle - cosInitAngle))));
if (isLeftSwing) {
swingAngle --;
if (swingAngle <= 0) {
swingAngle = 0;
location = LOCATION_MIDDLE;
}
} else {
swingAngle ++;
if (swingAngle >= initAngle) {
swingAngle = initAngle;
isLeftSwing = true;
}
}
break;
}
postInvalidateDelayed(delay);
}一个有5个球,中间3个球是不动的,所以先绘制中间3个静止的球。
在使用sin和cos计算时,使用的是弧度,并不是我们平时说的角度,所以先要把角度转换成弧度,使用Math.toRadians()方法。
摆球的运动可分为3类,一类是左边的一个球在动,一类是中间传递(都不动),还有一类是右边的摆球在动。
4.1 左边球运动时
此时右边的球是静止不动的,先绘制它。
由于我们每次都是偏移单位角度,所以其实我们是知道偏移角度的,根据偏移角度就能算出运动小球瞬间的中心坐标。
上面我们推算出角速度与偏移角度的公式,利用该公式就可以知道偏移单位角度需要的时间间隔,我们就在这个时间间隔后重新绘制。
小球运动时可能会有两个方向,向左或者向右运动。向左运动到初始角度时,就要换向了。当偏移角度为0时,就进入了中间传递状态。
4.2 中间传递状态
实际上,这个传递过程是需要时间的,这时5个球都短暂的静止,然后继续向左或者向右传递。
4.3 右边球运动时
与4.1左边球运动一致。
最后延迟绘制就行了。
github地址如下:
另外还学会了两个东西:
参考: