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

原创粉丝点击