今天一起来做一个装水的瓶子,效果如图所示:
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) {
}
});
}
}
大家可以参考然后自己做一些更改,比如颜色,气泡,瓶子的形状等等。
网友评论