贝塞尔曲线应用-魔法瓶子

今天一起来做一个装水的瓶子,效果如图所示:

《贝塞尔曲线应用-魔法瓶子》 201903060836491551875809173_small.gif

做这个动画的主要目的呢,主要在于应用和熟悉贝塞尔曲线以及画图中重要的Path类。

还是照旧列一下几个步骤:
1.绘制瓶子
2.绘制水波
3.绘制气泡

再分别理一下每个步骤的思路。

绘制瓶子

整个瓶子上面是一个矩形,下面是一个圆的一部分。确定了圆心的位置,矩形的宽高等参数后,就可以通过简单的数学运算,获取到矩形和圆形右边交点的左边,然后以此为起点采用路径一次绘制。

        paint.setStrokeWidth(containerStrokeWidth);
        circlePoint = new PointF(radius + containerStrokeWidth, topHeight + radius + containerStrokeWidth);
        Path containerPath = new Path();
        RectF rectF = new RectF(
                circlePoint.x - radius,
                circlePoint.y - radius,
                circlePoint.x + radius,
                circlePoint.y + radius);
        double degree = 180 * Math.asin(topWidth / (2 * radius)) / Math.PI;
        containerPath.addArc(rectF, (float) -(90 - degree), (float) (180 + 2 * (90 - degree)));
        containerPath.rLineTo(0, -(topHeight * 8 / 10));
        containerPath.rLineTo(topWidth / 10, -topHeight * 1 / 10);
        containerPath.rLineTo(-topWidth / 10, -topHeight * 1 / 10);
        containerPath.rLineTo(topWidth, 0);
        containerPath.rLineTo(0, topHeight);
        canvas.drawPath(containerPath, paint);
        canvas.clipPath(containerPath, Region.Op.INTERSECT);

注意,这句

canvas.clipPath(containerPath, Region.Op.INTERSECT);

作用是让后面的绘制(波浪和气泡)都在容器路径的范围内,不熟悉的可以下去了解一下clipPath这个方法。
其他需要注意的,public void rLineTo(float dx, float dy)这个方法,表示在当前路径结束点的横坐标和纵坐标分别追加相对位置,如rLineTo(100,100),就表示横纵坐标都增加100并连线到这个位置。
当然了,基本上canvas的其他类似方法带了r的都可以这么认为,比如下面要用到的rQuadTo方法,也是同样的道理,大家可以详细的看下文档,有些地方用起来比较方便。

绘制水波

水波纹的绘制当然是采用贝塞尔曲线来绘制了,其实这个也可以采用正弦函数来绘制,都能到达同样的效果。这里采用二阶贝塞尔曲线:

 path.moveTo(startX - wavWidth, waterLevel);
            for (int i = 0; i < mWidth + wavWidth; i += wavWidth) {
                path.rQuadTo(wavWidth / 4, wavHeight, wavWidth / 2, 0);
                path.rQuadTo(wavWidth / 4, -wavHeight, wavWidth / 2, 0);
            }
            path.lineTo(mWidth, mHeight);
            path.lineTo(0, mHeight);
            path.close();
            canvas.drawPath(path, wavPaint);

原理嘛,也是参考网上其他同学的做法,先绘制出一段正弦曲线,然后不停的改变曲线的横坐标(startX)起始位置。这里我自己试了下,这个做法要波浪的宽度足够宽看起来才有波浪的感觉,大家可以自己试试。

绘制气泡

气泡的绘制同样的也采用了二阶贝塞尔曲线,从瓶底上升到水位线位置,控制点随机生成坐标。

        PointF start = new PointF(startX, startY);
        PointF end = new PointF(endX, endY);
        final PointF control = new PointF(getRandom((int) startX, (int) endX), getRandom((int) startY, (int) endY));
        ValueAnimator objectAnimator = ValueAnimator.ofObject(new TypeEvaluator<PointF>() {
            @Override
            public PointF evaluate(float fraction, PointF startValue, PointF endValue) {
                float x = (float) (Math.pow(1 - fraction, 2) * startValue.x + 2 * fraction * (1 - fraction) * control.x + Math.pow(fraction, 2) * endValue.x);
                float y = (float) (Math.pow(1 - fraction, 2) * startValue.y + 2 * fraction * (1 - fraction) * control.y + Math.pow(fraction, 2) * endValue.y);
                return new PointF(x, y);
            }
        }, start, end);
        final String id = UUID.randomUUID().toString();
        popMap.put(id, new PointF());
        objectAnimator.setDuration(5500);
        objectAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
        objectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                PointF pointF = popMap.get(id);
                pointF.set((PointF) animation.getAnimatedValue());
                if (animation.getAnimatedFraction() >= 1) {
                    animation.removeAllUpdateListeners();
                    animation.cancel();
                    popMap.remove(id);
                }
            }
        });
        popAnimatorList.add(objectAnimator);
        objectAnimator.start();

其他的就是一些基本的属性动画的知识了,控件完整代码如下:

package com.example.administrator.beizer.widgets;

import android.animation.TypeEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.Region;
import android.graphics.Shader;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.LinearInterpolator;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.UUID;

/**
 * Created by H.Anthony on 2019/2/27.
 */

public class WavView extends View {
    Paint paint, wavPaint, popPaint;
    private static final String TAG = "WavView";
    /**
     * 控件宽度
     */
    float mWidth = 0;
    /**
     * 控件高度
     */
    float mHeight = 0;

    /**
     * 瓶子圆形圆心
     */
    PointF circlePoint;
    /**
     * 瓶子圆半径
     */
    float radius;
    /**
     * 瓶子口直径
     */
    float topWidth;
    /**
     * 瓶子颈高度
     */
    float topHeight;

    /**
     * 画笔宽度
     */
    float containerStrokeWidth;

    float popStrokeWidth = 5;


    /**
     * 波浪高度
     */
    float wavHeight = 15;

    /**
     * 气泡半径
     */
    float popR;


    /**
     * 水位线
     */
    float waterLevel = 0;

    int progress = 0;

    ValueAnimator valueAnimator;

    /**
     * 整个容器面积
     */
    float S = 0;


    /**
     * 波浪横坐标开始位置,不停的变化
     */
    int startX;

    /**
     * 波浪的宽度
     */
    float wavWidth = 0;


    /**
     * 通过这里不停的生成气泡
     */
    Handler handler = new Handler(Looper.myLooper(), new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            handler.sendEmptyMessageDelayed(0, 500);
            if (progress > 0) {
                startPop(getRandom(0, (int) mWidth), mHeight, getRandom(0, (int) mWidth), waterLevel + popR + wavHeight);
            }
            return false;
        }
    });

    public WavView(Context context) {
        super(context);
    }

    public WavView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int hegiht = getMeasuredHeight();
        int width = getMeasuredWidth();
        mWidth = width;
        mHeight = hegiht;

        topHeight = (mHeight - (containerStrokeWidth * 2)) * 1 / 2;
        radius = ((mHeight - (containerStrokeWidth * 2)) - topHeight) / 2;
        mWidth = radius * 2 + containerStrokeWidth * 2;
        topWidth = radius * 2 / 3;//可变
        setMeasuredDimension((int) mWidth, (int) mHeight);
        float s1 = topWidth * topHeight;
        float s2 = (float) (Math.PI * Math.pow(radius, 2));
        LinearGradient lg = new LinearGradient(0, 0, mWidth, mHeight, Color.CYAN, Color.BLUE, Shader.TileMode.MIRROR);
        wavPaint.setShader(lg);
        S = (s1 + s2);

        popPaint = new Paint();
        popPaint.setColor(Color.BLUE);
        popPaint.setStrokeWidth(popStrokeWidth);
        popPaint.setStyle(Paint.Style.STROKE);
        popPaint.setDither(false);
        popPaint.setAntiAlias(true);
        popR = radius / 20;
        handler.sendEmptyMessage(0);
    }


    /**
     * 设置进度(0-100),代表水位线的位置
     * @param progress
     */
    public void setProgress(int progress) {
        this.progress = progress;
        float areaCircle = (float) (Math.PI * Math.pow(radius, 2));
        float now = S * progress / 100;
        if (areaCircle >= now) {
            float perOfCircle = (now) / areaCircle;
            waterLevel = ((1 - perOfCircle) * (radius * 2)) + topHeight;
        } else {
            float left = now - areaCircle;
            float heightInheihgt = left / topWidth;
            waterLevel = topHeight - heightInheihgt;
        }
        wavWidth = 2 * radius;
        stopPop();
        startWavAni();
    }

    private void stopPop() {
        for (ValueAnimator valueAnimator : popAnimatorList) {
            if (valueAnimator != null) {
                valueAnimator.cancel();
            }
        }
        popMap.clear();
        popAnimatorList.clear();
    }


    private void init() {
        paint = new Paint();
        paint.setDither(false);
        paint.setAntiAlias(true);
        paint.setColor(Color.parseColor("#0000CD"));
        paint.setStyle(Paint.Style.STROKE);
        wavPaint = new Paint();
        wavPaint.setDither(false);
        wavPaint.setAntiAlias(true);
        wavPaint.setColor(Color.parseColor("#0000CD"));
        wavPaint.setStyle(Paint.Style.FILL);
        containerStrokeWidth = 5;
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawContainer(canvas);
        drawWav(canvas);
        drawPop(canvas);
    }


    /**
     * 绘制容器
     *
     * @param canvas
     */
    private void drawContainer(Canvas canvas) {
        paint.setStrokeWidth(containerStrokeWidth);
        circlePoint = new PointF(radius + containerStrokeWidth, topHeight + radius + containerStrokeWidth);
        Path containerPath = new Path();
        RectF rectF = new RectF(
                circlePoint.x - radius,
                circlePoint.y - radius,
                circlePoint.x + radius,
                circlePoint.y + radius);
        double degree = 180 * Math.asin(topWidth / (2 * radius)) / Math.PI;
        containerPath.addArc(rectF, (float) -(90 - degree), (float) (180 + 2 * (90 - degree)));
        containerPath.rLineTo(0, -(topHeight * 8 / 10));
        containerPath.rLineTo(topWidth / 10, -topHeight * 1 / 10);
        containerPath.rLineTo(-topWidth / 10, -topHeight * 1 / 10);
        containerPath.rLineTo(topWidth, 0);
        containerPath.rLineTo(0, topHeight);
        canvas.drawPath(containerPath, paint);
        canvas.clipPath(containerPath, Region.Op.INTERSECT);
    }


    /**
     * 绘制波浪
     *
     * @param canvas
     */
    private void drawWav(Canvas canvas) {
        if (wavWidth > 0) {
            Path path = new Path();
            path.reset();
            path.moveTo(startX - wavWidth, waterLevel);
            for (int i = 0; i < mWidth + wavWidth; i += wavWidth) {
                path.rQuadTo(wavWidth / 4, wavHeight, wavWidth / 2, 0);
                path.rQuadTo(wavWidth / 4, -wavHeight, wavWidth / 2, 0);
            }
            path.lineTo(mWidth, mHeight);
            path.lineTo(0, mHeight);
            path.close();
            canvas.drawPath(path, wavPaint);
        }
    }


    /**
     * 绘制气泡
     *
     * @param canvas
     */
    private void drawPop(Canvas canvas) {
        for (Map.Entry<String, PointF> entry : popMap.entrySet()) {
            canvas.drawCircle(entry.getValue().x, entry.getValue().y, popR, popPaint);
        }
    }

    /**
     * 开始波浪动画
     */
    void startWavAni() {
        if (valueAnimator != null && valueAnimator.isRunning()) {
            valueAnimator.cancel();
        }
        valueAnimator = ValueAnimator.ofInt(0, (int) wavWidth);
        valueAnimator.setDuration(1000);
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.setRepeatCount(-1);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                startX = (int) animation.getAnimatedValue();
                invalidate();
            }
        });
        valueAnimator.start();
    }


    Map<String, PointF> popMap = new HashMap<>();
    List<ValueAnimator> popAnimatorList = new ArrayList<>();

    /**
     * 生成气泡
     *
     * @param startX
     * @param startY
     * @param endX
     * @param endY
     */
    public void startPop(final float startX, float startY, float endX, float endY) {
        PointF start = new PointF(startX, startY);
        PointF end = new PointF(endX, endY);
        final PointF control = new PointF(getRandom((int) startX, (int) endX), getRandom((int) startY, (int) endY));
        ValueAnimator objectAnimator = ValueAnimator.ofObject(new TypeEvaluator<PointF>() {
            @Override
            public PointF evaluate(float fraction, PointF startValue, PointF endValue) {
                float x = (float) (Math.pow(1 - fraction, 2) * startValue.x + 2 * fraction * (1 - fraction) * control.x + Math.pow(fraction, 2) * endValue.x);
                float y = (float) (Math.pow(1 - fraction, 2) * startValue.y + 2 * fraction * (1 - fraction) * control.y + Math.pow(fraction, 2) * endValue.y);
                return new PointF(x, y);
            }
        }, start, end);
        final String id = UUID.randomUUID().toString();
        popMap.put(id, new PointF());
        objectAnimator.setDuration(5500);
        objectAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
        objectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                PointF pointF = popMap.get(id);
                pointF.set((PointF) animation.getAnimatedValue());
                if (animation.getAnimatedFraction() >= 1) {
                    animation.removeAllUpdateListeners();
                    animation.cancel();
                    popMap.remove(id);
                }
            }
        });
        popAnimatorList.add(objectAnimator);
        objectAnimator.start();
    }


    /**
     * 关闭动画
     */
    public void cancel() {
        if (valueAnimator != null) {
            valueAnimator.cancel();
        }
        if (handler != null) {
            handler.removeCallbacksAndMessages(null);
        }
        stopPop();
    }

    public static int getRandom(int min, int max) {
        Random random = new Random();
        if (max <= 0) {
            max = 1;
        }
        int value = random.nextInt(max) % ((max - min + 1) != 0 ? (max - min + 1) : 1) + min;
        return value;
    }
}

这个是应用控件的示例代码:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:gravity="center_horizontal"
    android:orientation="vertical"
    tools:context="com.example.administrator.beizer.MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <SeekBar
            android:id="@+id/bar"
            android:layout_width="match_parent"
            android:layout_height="100dp" />
    </LinearLayout>
    <com.example.administrator.beizer.widgets.WavView
        android:id="@+id/wav"
        android:layout_width="350dp"
        android:layout_height="350dp" />
</LinearLayout>
public class MainActivity extends AppCompatActivity {

    WavView wavView;

    SeekBar seekBar;

    private static final String TAG = "MainActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        CrashReport.initCrashReport(getApplicationContext(), "2932c56723", false);
        setContentView(R.layout.activity_main);
        Log.e(TAG, "onCreate: " );
        wavView = findViewById(R.id.wav);
        seekBar = findViewById(R.id.bar);
        seekBar.setMax(100);
        seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                wavView.setProgress(progress);
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {

            }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {

            }
        });
    }
}

大家可以参考然后自己做一些更改,比如颜色,气泡,瓶子的形状等等。

    原文作者:我的阿福
    原文地址: https://www.jianshu.com/p/02980d70723d
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞