android自定义动画实现牛顿撞球
来源:互联网 发布:怎么看自己的淘宝信誉 编辑:程序博客网 时间:2024/06/01 22:16
效果
最近实现了一个不错的自定义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()实现的。理解了这一点就知道自定义动画的实现原理了。
源码:https://github.com/acxingyun/WaveBalls
- android自定义动画实现牛顿撞球
- Android实现自定义动画
- 撞球
- 撞球
- android实现牛顿摆
- Android Drawable实现自定义动画
- Android:自定义view实现动画
- Android自定义动画学习,实现左右摇摆动画
- Android自定义加载动画的实现
- Android自定义progressDialog实现载入动画
- Android自定义弹出菜单+动画实现
- Android自定义View实现雷达扫描动画
- android自定义SurfaceView实现跑男动画
- Android 自定义SurfaceView实现加载GIF动画
- Android自定义View实现雷达扫描动画
- Android复杂自定义动画的实现思路
- Android 自定义View 实现loading动画
- Android 自定义TextView实现文字渐变动画
- 模拟高校的三个老师同时分发80份学习笔记
- Java后台框架篇--spring+websocket整合
- 真正从零开始,TensorFlow详细安装入门图文教程!
- 关闭开启 mysql 服务
- VLD(Visual LeakDetector)内存泄漏库的使用
- android自定义动画实现牛顿撞球
- ue4 c++ anim notify
- JDK并发包委婉
- Android图片加载框架最全解析(五),Glide强大的图片变换功能
- POJ1062(有限制的最短路)
- 下步计划
- 最强阵容加强版
- Android 8.0 HIDL
- javaweb实现手机APP注册