效果
最近实现了一个不错的自定义view,类似在商店里看到的牛顿撞球,先上效果:
一个球摆动:
两个球摆动:
三个球摆动:
感谢mp4转gif网站,甩格式工厂10条街:https://ezgif.com/video-to-gif
一开始的想法就是做一个等待时的动画效果,好看的动画效果能让用户耐心等待,撞球是我比较喜欢的效果。
使用
小球个数、颜色、半径、摆动球个数、最大摆动角度等都可以使用时在xml中自定义:
<acxingyun.cetcs.com.myviews.WaitingBallView
android:id="@+id/waitingBallView"
android:layout_marginTop="20dp"
android:layout_width="match_parent"
android:layout_height="100dp"
app:ballColor="@color/color3"
app:ballRadius="20dp"
app:lineColor="@color/color1"
app:lineWidth="2dp"
app:waveAngle="30"
app:ballCount="6"
app:barColor="@color/black"
app:barWidth="5dp"
app:animationPeriod="5000"
app:wavingBallCount="3"
/>
代码调用:
waitingBallView = findViewById(R.id.waitingBallView);
waitingBallView.startAnimation();
目前只有一个方法:startAnimation(),可以加一些设置属性的方法。
实现原理
就是一些几何计算,cos、sin之类的,还有就是弧度转角度。
总高度 = 线长 + 球半径;总高度和球半径是属性定义的,可以算出线长lineLength。
为了后面计算方便,先保存每个小球静止时的坐标,水平方向摆动时的坐标等于静止坐标加上偏移量;y方向坐标等于 线长*cosα:
以摆动最大角度时第一个小球左边位置为水平方向圆点,计算出第一个球静止时的横坐标X0,然后可以得出其它球的横坐标;所有球的Y0 = mHeight - 球半径。把静止时的坐标保存下来,方便以后计算摆动时的坐标。
下面是摆动时的坐标计算:
α是相对于y轴的,向左是负数,向右为正。
多个小球向右摆动,x坐标仍然是x0 + 偏移量,y坐标是线长*cosα:
在onDraw里面需要判断摆动角度的正负,为负数是左边的球摆,为正数是右边球摆,没有摆动的球坐标维持静止时的坐标。
除了小球,还要画线,线的一端坐标和球一样,另一端x坐标和小球静止时一样,y坐标为0。
实现
在values新建attrs.xml:
在attrs.xml定义属性:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="WaitingBallView">
<attr name="ballCount" format="integer"/>
<attr name="ballColor" format="color"/>
<attr name="ballRadius" format="dimension"/>
<attr name="lineColor" format="color"/>
<attr name="waveAngle" format="float"/>
<attr name="lineWidth" format="dimension"/>
<attr name="barColor" format="color"/>
<attr name="barWidth" format="dimension"/>
<attr name="animationPeriod" format="float"/>
<attr name="wavingBallCount" format="integer"/>
</declare-styleable>
</resources>
定义的属性用于自定义View的构造函数中读取配置的属性,declare-styleable标签中的名字一定要和view的名字一样,不然在activity_main.xml中无法识别自定义属性。
下面是java文件:
public class WaitingBallView extends View {
/**
* 球个数
*/
private int ballCount;
/**
* 球半径
*/
private float ballRadius;
/**
* 小球颜色
*/
private int ballColor;
/**
* 摆线的颜色
*/
private int lineColor;
/**
* 水平bar颜色
*/
private int barColor;
/**
* 水平bar宽度
*/
private float barWidth;
/**
* 摆线宽度
*/
private float lineWidth;
/**
* 画摆线画笔
*/
private Paint linePaint;
/**
* 画球的画笔
*/
private Paint ballPaint;
/**
* 摆动一个周期时长
*/
private float animationPeriod;
/**
* 最大摆动角度
*/
private float waveAngleMax;
/**
* 摆动中的角度
*/
private float wavingAngle;
/**
* 摆动小球个数
*/
private int wavingBallCount;
/**
* 自定义view的宽度
*/
private int mWidth;
/**
* 自定义view的高度
*/
private int mHeight;
/**
* 保存没有摆动时每个球的坐标
*/
private Map<Integer , List<Float>> locationMap = new HashMap<>();
private float lineLength;
public WaitingBallView(Context context){
this(context, null, 0);
}
public WaitingBallView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public WaitingBallView(Context context, AttributeSet attrs, int defStyleAttr){
super(context, attrs, defStyleAttr);
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.WaitingBallView, defStyleAttr, 0);
ballCount = a.getInteger(R.styleable.WaitingBallView_ballCount, 0);
ballRadius = a.getDimension(R.styleable.WaitingBallView_ballRadius, 0);
ballColor = a.getColor(R.styleable.WaitingBallView_ballColor, 0);
lineColor = a.getColor(R.styleable.WaitingBallView_lineColor, 0);
waveAngleMax = a.getFloat(R.styleable.WaitingBallView_waveAngle, 0);
lineWidth = a.getDimension(R.styleable.WaitingBallView_lineWidth, 0);
barColor = a.getColor(R.styleable.WaitingBallView_barColor, 0);
barWidth = a.getDimension(R.styleable.WaitingBallView_barWidth, 0);
animationPeriod = a.getFloat(R.styleable.WaitingBallView_animationPeriod, 0);
wavingBallCount = a.getInteger(R.styleable.WaitingBallView_wavingBallCount, 0);
a.recycle();
linePaint = new Paint();
ballPaint = new Paint();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Log.d(getClass().getSimpleName(), "onMeasure called...");
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int computeSize;
int specMode = MeasureSpec.getMode(widthMeasureSpec);
int spenSize = MeasureSpec.getSize(widthMeasureSpec);
Log.d(getClass().getSimpleName(), "spenSize:" + spenSize);
switch (specMode){
//为match_parent或指定xxdp
case MeasureSpec.EXACTLY:
mWidth = spenSize;
break;
//wrap_content
case MeasureSpec.AT_MOST:
computeSize = getPaddingLeft() + getPaddingRight() + spenSize;
//需要根据parent大小计算本身大小,取小的,保证不会超出parent
Log.d(getClass().getSimpleName(), "computeSize:" + computeSize);
mWidth = computeSize < spenSize?computeSize:spenSize;
break;
case MeasureSpec.UNSPECIFIED:
computeSize = getPaddingLeft() + getPaddingRight() + spenSize;
Log.d(getClass().getSimpleName(), "computeSize:" + computeSize);
mWidth = computeSize < spenSize?computeSize:spenSize;
break;
}
specMode = MeasureSpec.getMode(heightMeasureSpec);
spenSize = MeasureSpec.getSize(heightMeasureSpec);
Log.d(getClass().getSimpleName(), "spenSize:" + spenSize);
switch (specMode){
case MeasureSpec.EXACTLY:
mHeight = spenSize;
break;
case MeasureSpec.AT_MOST:
computeSize = getPaddingLeft() + getPaddingRight() + spenSize;
Log.d(getClass().getSimpleName(), "computeSize:" + computeSize);
mHeight = computeSize < spenSize?computeSize:spenSize;
break;
case MeasureSpec.UNSPECIFIED:
computeSize = getPaddingLeft() + getPaddingRight() + spenSize;
Log.d(getClass().getSimpleName(), "computeSize:" + computeSize);
mHeight = computeSize < spenSize?computeSize:spenSize;
break;
}
Log.d(getClass().getSimpleName(), "mWidth:" + mWidth);
Log.d(getClass().getSimpleName(), "mHeight:" + mHeight);
setMeasuredDimension(mWidth, mHeight);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
Log.d(getClass().getSimpleName(), "onSizeChanged called,w:" + w + " h:" + h + " oldw:" + oldw + " oldh:" + oldh);
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w;
mHeight = h;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Log.i(getClass().getSimpleName(), "wavingAngle:" + wavingAngle);
linePaint.setColor(barColor);
linePaint.setStyle(Paint.Style.FILL);
linePaint.setAntiAlias(true);
linePaint.setStrokeWidth(barWidth);
canvas.drawLine(0, 0, mWidth, 0, linePaint);
linePaint.setColor(lineColor);
linePaint.setStrokeWidth(lineWidth);
ballPaint.setColor(ballColor);
ballPaint.setStyle(Paint.Style.FILL);
ballPaint.setAntiAlias(true);
float ballX;
float ballY;
float lineStartX;
float lineStartY;
float lineStopX;
float lineStopY;
lineLength = mHeight - ballRadius;
List<Float> locationList;
//静止时的坐标
if (wavingAngle == 0){
for (int i = 0; i< ballCount; i++){
locationList = locationMap.get(i);
ballX = locationList.get(0);
ballY = locationList.get(1);
//先画线再花球,球上不会看到线,更好看
canvas.drawLine(ballX, 0, ballX, ballY, linePaint);
canvas.drawCircle( ballX, ballY, ballRadius, ballPaint);
}
}else if (wavingAngle < 0){
//绘制向左摆动时的小球
for (int i = 0; i < wavingBallCount; i++){
locationList = locationMap.get(i);
ballX = (float) (locationList.get(0) + lineLength * Math.sin(wavingAngle * Math.PI/180));
ballY = (float) (lineLength * Math.cos(wavingAngle * Math.PI / 180));
lineStartX = ballX;
lineStartY = ballY;
lineStopX = locationList.get(0);
lineStopY = 0;
canvas.drawLine(lineStartX, lineStartY, lineStopX, lineStopY, linePaint);
canvas.drawCircle( ballX, ballY, ballRadius, ballPaint);
}
//绘制静止不动的小球
for (int i = wavingBallCount; i < ballCount; i++){
locationList = locationMap.get(i);
ballX = locationList.get(0);
ballY = locationList.get(1);
canvas.drawLine(ballX, 0, ballX, ballY, linePaint);
canvas.drawCircle( ballX, ballY, ballRadius, ballPaint);
}
}else if (wavingAngle > 0){
//绘制向右摆动的小球
for (int i = ballCount - 1; i > ballCount - wavingBallCount - 1; i--){
locationList = locationMap.get(i);
ballX = (float) (locationList.get(0) + lineLength * Math.sin(wavingAngle * Math.PI/180));
ballY = (float) (lineLength * Math.cos(wavingAngle * Math.PI / 180));
lineStartX = locationMap.get(i).get(0);
lineStartY = 0;
lineStopX = ballX;
lineStopY = ballY;
canvas.drawLine( lineStartX, lineStartY, lineStopX, lineStopY, linePaint);
canvas.drawCircle( ballX, ballY, ballRadius, ballPaint);
}
//绘制静止不动的小球
for (int i = 0; i < ballCount - wavingBallCount; i++){
locationList = locationMap.get(i);
ballX = locationList.get(0);
ballY = locationList.get(1);
lineStartX = ballX;
lineStartY = ballY;
lineStopX = ballX;
lineStopY = 0;
canvas.drawLine( lineStartX, lineStartY, lineStopX, lineStopY, linePaint);
canvas.drawCircle( ballX, ballY, ballRadius, ballPaint);
}
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
Log.d(getClass().getSimpleName(), "changed:" + changed + " left:" + left + " top:" + top
+ " right:" + right + " bottom:" + bottom);
//onMeasure之后,mHeight和mWidth就确定了,此时计算静止时的坐标
float ballX;
float ballY;
for (int i = 0; i < ballCount; i++){
ballX = (float) ((mHeight - ballRadius*2 + ballRadius) * Math.sin(waveAngleMax * Math.PI / 180) + ballRadius + ballRadius * 2 * i);
ballY = mHeight - ballRadius*2 + ballRadius;
List<Float> locationArray = new ArrayList<>();
locationArray.add(0, ballX);
locationArray.add(1, ballY);
locationMap.put(i, locationArray);
}
super.onLayout(changed, left, top, right, bottom);
}
public void startAnimation(){
//范围变化:-waveAngleMax, 0 , waveAngleMax, 0, -waveAngleMax为一个周期
final ValueAnimator angleValue = ValueAnimator.ofFloat(-waveAngleMax, 0 , waveAngleMax, 0, -waveAngleMax);
//均匀变化
angleValue.setInterpolator(new LinearInterpolator());
//从左边开始摆动
wavingAngle = -waveAngleMax;
angleValue.setDuration((long) (animationPeriod));
//无限运动
angleValue.setRepeatCount(ValueAnimator.INFINITE);
angleValue.start();
angleValue.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
//更新摆动角度
wavingAngle = (float) angleValue.getAnimatedValue();
//invalidate()会调用onDraw(),实现了视图刷新
invalidate();
}
});
}
}
在MainActivity中调用:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="acxingyun.cetcs.com.myviews.MainActivity">
<acxingyun.cetcs.com.myviews.WaitingBallView
android:id="@+id/waitingBallView"
android:layout_marginTop="20dp"
android:layout_width="match_parent"
android:layout_height="100dp"
app:ballColor="@color/color3"
app:ballRadius="20dp"
app:lineColor="@color/color1"
app:lineWidth="2dp"
app:waveAngle="30"
app:ballCount="6"
app:barColor="@color/black"
app:barWidth="5dp"
app:animationPeriod="5000"
app:wavingBallCount="1"
/>
</RelativeLayout>
public class MainActivity extends Activity {
private WaitingBallView waitingBallView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
waitingBallView = findViewById(R.id.waitingBallView);
waitingBallView.startAnimation();
}
}
后记
一开始看了一篇别人的文章突发奇想,利用业余时间实现的,动画的是通过ValueAnimator不断变化,然后调用onDraw()实现的。理解了这一点就知道自定义动画的实现原理了。