Android自定义View之乘风破浪的小船

效果图:


2.gif

一、思路分析

整个效果分为两部分,第一部分是波浪形的水波,第二部分是小船沿着水波移动,并且水波是和小船向着相反的方向移动的。
水波我们可以通过贝塞尔曲线来实现,小船沿着水波移动的效果通过PathMeasure来实现,然后使用属性动画让水波和小船动起来。

二、功能实现

1.首先通过贝塞尔曲线实现水波

private void drawWave(Canvas canvas){
    mPath.reset();
    mPath.moveTo(0 - mDeltaX, mHeight / 2);
    for (int i = 0; i <= getWidth() + waveLength; i += waveLength) {
        mPath.rQuadTo(halfWaveLength / 2, waveHeight, halfWaveLength, 0);
        mPath.rQuadTo(halfWaveLength / 2, -waveHeight, halfWaveLength, 0);
    }
    mPath.lineTo(getWidth() + waveLength, getHeight());
    mPath.lineTo(0, getHeight());
    mPath.close();
    canvas.drawPath(mPath, mPaint);
}

mDeltaX为水波水平方向移动的距离。
waveLength为水波长度,一个上弧加一个下弧为一个波长。
halfWaveLength为半个水波长度。
先把path的起始点移动到屏幕的一半高度的位置,然后循环画曲线,长度为屏幕的宽度加上一个波长,然后连接到整个屏幕高度的位置,最后形成一个封闭的区域,这样就实现了一个填充的水波效果。


水波.png

利用属性动画来不断的改变path起始点的x值,使水波水平移动

private void startAnim(){
    ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
    animator.addUpdateListener(animation -> {
        mDeltaX = waveLength * ((float) animation.getAnimatedValue());
        postInvalidate();
    });
    animator.setDuration(13000);
    animator.setInterpolator(new LinearInterpolator());
    animator.setRepeatCount(ValueAnimator.INFINITE);
    animator.start();
}
水波动画.gif

2.接着就是把小船给绘制到水波上,要绘制小船,得先知道要绘制到哪里,也就是要知道绘制的坐标点,这里可以通过PathMeasure来得到。
PathMeasure有个getMatrix方法,通过这个方法可以获取指定长度的位置坐标及该点Matrix。先来看下这个方法:

boolean getMatrix (float distance, Matrix matrix, int flags)

distance:距离Path起点的长度
matrix:根据flags封装好的矩阵
flags:选择哪些内容会传入到matrix中,可选值有POSITION_MATRIX_FLAG(位置)和ANGENT_MATRIX_FLAG(正切)。
有了这个方法,我们就可以绘制小船了

private void drawBitmap(Canvas canvas) {
    mPathMeasure.setPath(mPath, false);
    mMatrix.reset();
    mPathMeasure.getMatrix(mDistance, mMatrix, PathMeasure.TANGENT_MATRIX_FLAG | PathMeasure.POSITION_MATRIX_FLAG);
    canvas.drawBitmap(mBitMap,mMatrix,mPaint);
}

mDistance就是距离Path起点的长度。
我们发现小船在沿着水波移动的时候,会根据波浪的高低倾斜


33.png

上面提到getMatrix方法,不仅返回指定长度的位置坐标,还会返回该点的matrix,这个时候就需要用到返回的matrix,使用matrix的preTranslate方法来实现小船角度的倾斜。

private void drawBitmap(Canvas canvas) {
    mPathMeasure.setPath(mPath, false);
    mMatrix.reset();
    mPathMeasure.getMatrix(mDistance, mMatrix, PathMeasure.TANGENT_MATRIX_FLAG | PathMeasure.POSITION_MATRIX_FLAG);
    mMatrix.preTranslate(- mBitMap.getWidth() / 2, - mBitMap.getHeight());
    canvas.drawBitmap(mBitMap,mMatrix,mPaint);
}

关于matrix以及matrix的preTranslate方法详解,请移步这里。
至此我们实现了小船的绘制,但是小船并没有移动起来,我们还需要利用属性动画来使小船移动起来

private void startAnim(){
    ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
    animator.addUpdateListener(animation -> {
        mDeltaX = waveLength * ((float) animation.getAnimatedValue());

        mDistance = (getWidth() + waveLength + halfWaveLength) * ((float)animation.getAnimatedValue());
        postInvalidate();
    });
    animator.setDuration(13000);
    animator.setInterpolator(new LinearInterpolator());
    animator.setRepeatCount(ValueAnimator.INFINITE);
    animator.start();
}

贴上完整代码:

public class WaveView extends View {
    private Paint mPaint;
    private Path mPath;
    // 水波长度
  private int waveLength = 800;
    // 水波高度
  private int waveHeight = 150;
    private int mHeight;
    private int halfWaveLength = waveLength / 2;
    private float mDeltaX;
    private Bitmap mBitMap;
    private PathMeasure mPathMeasure;
    private Matrix mMatrix;
    private float mDistance;

    public WaveView(Context context) {
        this(context, null);
    }

    public WaveView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public WaveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public WaveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    private void init(){
        mPaint = new Paint();
        mPaint.setColor(getResources().getColor(R.color.sea_blue));
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setAntiAlias(true);

        mPath = new Path();

        mMatrix = new Matrix();
        mPathMeasure = new PathMeasure();

        Options opts = new Options();
        opts.inSampleSize = 3;
        mBitMap = BitmapFactory.decodeResource(getResources(), R.drawable.ship, opts);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mHeight = h;

        startAnim();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawWave(canvas);

        drawBitmap(canvas);
    }

    /**
     * 绘制水波
     * @param canvas
     */
    private void drawWave(Canvas canvas){
        mPath.reset();
        mPath.moveTo(0 - mDeltaX, mHeight / 2);
        for (int i = 0; i <= getWidth() + waveLength; i += waveLength) {
            mPath.rQuadTo(halfWaveLength / 2, waveHeight, halfWaveLength, 0);
            mPath.rQuadTo(halfWaveLength / 2, -waveHeight, halfWaveLength, 0);
        }
        mPath.lineTo(getWidth() + waveLength, getHeight());
        mPath.lineTo(0, getHeight());
        mPath.close();

        canvas.drawPath(mPath, mPaint);
    }

    /**
     * 绘制小船
     * @param canvas
     */
    private void drawBitmap(Canvas canvas) {
        mPathMeasure.setPath(mPath, false);
        mMatrix.reset();
        mPathMeasure.getMatrix(mDistance, mMatrix, PathMeasure.TANGENT_MATRIX_FLAG | PathMeasure.POSITION_MATRIX_FLAG);
        mMatrix.preTranslate(- mBitMap.getWidth() / 2, - mBitMap.getHeight());
        canvas.drawBitmap(mBitMap,mMatrix,mPaint);
    }

    /**
     * 平移动画
     */
    private void startAnim(){
        ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
        animator.addUpdateListener(animation -> {
            mDeltaX = waveLength * ((float) animation.getAnimatedValue());

            mDistance = (getWidth() + waveLength + halfWaveLength) * ((float)animation.getAnimatedValue());
            postInvalidate();
        });
        animator.setDuration(13000);
        animator.setInterpolator(new LinearInterpolator());
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.start();
    }
}

完整项目地址:https://github.com/loren325/CustomerView

你可能感兴趣的:(Android自定义View之乘风破浪的小船)