android自定义动画实现牛顿撞球

效果

最近实现了一个不错的自定义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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值