Android实现漂亮的波纹动画
Android实现漂亮的波纹动画
本文章讲述如何使用二维画布canvas和camera、矩阵实现二、三维波纹动画效果(波纹大小变化、画笔透明度变化、画笔粗细变化)
一、UI界面
界面主要分为三部分
第一部分:输入框,根据输入x轴、Y轴、Z轴倾斜角度绘制波纹动画立体效果
第二部分:点击按钮PLAY:开始绘制动画;点击按钮STOP:停止绘制动画
第三部分:绘制波纹动画的自定义view
二、主要代码实现
1. 新建一个WaveView类继承自View
声明类成员变量和实现构造函数初始化
public class WaveView extends View{public WaveView(Context context) {super(context);init();}public WaveView(Context context, AttributeSet attrs) {super(context, attrs);init();}public WaveView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);init();}
2. 添加动画
(1)圆圈等比变大动画
ValueAnimator radiusAnimator = ValueAnimator.ofFloat(mRippleInfo.minRadius, mRippleInfo.maxRadius);
radiusAnimator.setDuration(mRippleInfo.durationTime); // 动画持续时间
radiusAnimator.setStartDelay(i * rippleDelay); // 延迟启动
radiusAnimator.setRepeatCount(ValueAnimator.INFINITE);
radiusAnimator.setRepeatMode(ValueAnimator.RESTART);
radiusAnimator.addUpdateListener(animation -> {mRadiusArray[index] = (float) animation.getAnimatedValue();invalidate(); // 重绘视图
});
(2)透明度渐变动画
ValueAnimator alphaAnimator = ObjectAnimator.ofFloat( 0.1f, 0.8f, 0.8f,0.4f, 0);
alphaAnimator.setDuration(mRippleInfo.durationTime); // 动画持续时间
alphaAnimator.setStartDelay(i * rippleDelay); // 延迟启动
alphaAnimator.setRepeatCount(ValueAnimator.INFINITE);
alphaAnimator.setRepeatMode(ValueAnimator.RESTART);
alphaAnimator.addUpdateListener(animation -> {mAlphaArray[index] = (float) animation.getAnimatedValue();invalidate(); // 重绘视图
});
(3)画笔由细变粗动画
ValueAnimator strokeAnimator = ObjectAnimator.ofFloat(mRippleInfo.minStrokenWidth, mRippleInfo.maxStrokenWidth);
strokeAnimator.setDuration(mRippleInfo.durationTime); // 动画持续时间
strokeAnimator.setStartDelay(i * rippleDelay); // 延迟启动
strokeAnimator.setRepeatCount(ValueAnimator.INFINITE);
strokeAnimator.setRepeatMode(ValueAnimator.RESTART);
strokeAnimator.addUpdateListener(animation -> {mStrokenArray[index] = (float) animation.getAnimatedValue();invalidate(); // 重绘视图
});
3. 应用矩阵变换
需要注意坐标变换中心点,这里是以画布左下角作为坐标中心点
mRectF.set(0, 0, width, height);
// 应用相机变换
mCamera.save();
mCamera.rotateX(30); // 绕X轴旋转,顺时针为正
mCamera.rotateY(0); // 绕Y轴旋转
mCamera.rotateZ(30); // 绕Z轴旋转
mCamera.getMatrix(mMatrix); // 获取变换后的矩阵
mCamera.restore();mMatrix.preTranslate(0, -height);//1️以左下角为坐标中心点
mMatrix.postTranslate(0, height);mMatrix.mapRect(mRectF);
float newCenterX = mRectF.centerX();// 获取变换后的图像的新中心点
float newCenterY = mRectF.centerY();
float translateX = centerX - newCenterX;// 计算需要的平移量
float translateY = centerY - newCenterY;
mMatrix.postTranslate(translateX, translateY);// 将平移应用到矩阵
4. 重写onDraw方法
@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);if (!mIsDrawing){return;}canvas.save();// 计算中心点centerX = getWidth() / 2.0f;centerY = getHeight() / 2.0f;canvas.concat(mMatrix);// 将矩阵应用到画布上//绘制多圈波纹for (int i = 0; i < mRippleInfo.rippleCount; i++) {if (mRadiusArray[i] > 0) {paint.setStrokeWidth(mStrokenArray[i]);paint.setAlpha((int)(255 * mAlphaArray[i]));canvas.drawCircle(centerX, centerY,mRadiusArray[i],paint);}}// 中心点绘制 logocanvas.drawBitmap(mLogoBitmap, (getWidth()- mRippleInfo.minRadius)/2, (getHeight()- mRippleInfo.minRadius)/2, null);canvas.restore();}
三、效果展示
二维波纹动画
waveAnimation_2D
三维波纹动画
waveAnimation_3D
四、 完整代码实现:
activity_main.xml实现如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:padding="16dp"android:gravity="center"android:orientation="vertical"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"android:layout_marginBottom="8dp"><LinearLayoutandroid:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:orientation="vertical"android:gravity="center_horizontal"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="X轴倾斜" /><EditTextandroid:id="@+id/editText_X"android:layout_width="match_parent"android:layout_height="wrap_content"android:inputType="number"android:text="0"/></LinearLayout><LinearLayoutandroid:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:orientation="vertical"android:gravity="center_horizontal"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="Y轴倾斜" /><EditTextandroid:id="@+id/editText_Y"android:layout_width="match_parent"android:layout_height="wrap_content"android:inputType="number"android:text="0"/></LinearLayout><LinearLayoutandroid:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:orientation="vertical"android:gravity="center_horizontal"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="Z轴倾斜" /><EditTextandroid:id="@+id/editText_Z"android:layout_width="match_parent"android:layout_height="wrap_content"android:inputType="number"android:text="0"/></LinearLayout></LinearLayout><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"android:gravity="center"android:paddingTop="16dp"><Buttonandroid:id="@+id/playButton"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="Play"android:layout_marginEnd="8dp"/><Buttonandroid:id="@+id/stopButton"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="Stop"android:layout_marginEnd="8dp"/></LinearLayout><FrameLayoutandroid:layout_width="match_parent"android:layout_height="0dp"android:layout_weight="1"android:layout_gravity="center"><!-- WaveView --><com.example.waveanimationbysingleview.animation.WaveViewandroid:id="@+id/wave_view"android:layout_width="match_parent"android:layout_height="606dp"android:layout_gravity="center" /></FrameLayout></LinearLayout>
自定义view——WaveView.java实现如下:
package com.example.waveanimationbysingleview.animation;import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Camera;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.SweepGradient;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.View;import com.example.waveanimationbysingleview.R;/*** @author xibao* @since 2025/02/26*/
public class WaveView extends View{private RippleAnimationInfo mRippleInfo;private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);private float centerX, centerY;private float[] mRadiusArray; // 存储当前绘制每圈光波的半径private float[] mAlphaArray; //存储当前绘制每圈光波的透明度private float[] mStrokenArray; //存储当前绘制每圈光波的画笔宽度private final Camera mCamera = new Camera();private final Matrix mMatrix = new Matrix();private final RectF mRectF = new RectF();private Boolean mIsDrawing = false;private Bitmap mLogoBitmap;private AnimatorSet mAnimatorSet = new AnimatorSet();//多动画组合类private List<Animator> mAnimatorList = new ArrayList<>();public WaveView(Context context) {super(context);init();}public WaveView(Context context, AttributeSet attrs) {super(context, attrs);init();}public WaveView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);init();}private void init() {paint.setAntiAlias(true);//抗锯齿paint.setStrokeCap(Paint.Cap.ROUND);paint.setStrokeWidth(2);paint.setStyle(Paint.Style.STROKE);// 填充样式 描边paint.setColor(Color.parseColor("#fde48a"));this.setVisibility(View.VISIBLE);//可见initRippleAnimationInfos();// 加载 logo 图片mLogoBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.logo);mLogoBitmap = Bitmap.createScaledBitmap(mLogoBitmap, mRippleInfo.minRadius, mRippleInfo.minRadius, true);}private void initRippleAnimationInfos() {int[] colors = {0xFF5CC0C6};//int[] colors = {0xFF5CC0C6, 0xFF5CC0C6, 0xFFA353CB, 0xFFCFB852, 0xFFCFB852, 0xFFA353CB, 0xFF5CC0C6};//头尾颜色一样过渡自然mRippleInfo = new RippleAnimationInfo(30,300,1,10,3000, new Vector3D(0,0,0), colors,5);//初始化动画属性mRadiusArray = new float[mRippleInfo.rippleCount];mAlphaArray = new float[mRippleInfo.rippleCount];mStrokenArray = new float[mRippleInfo.rippleCount];initAnimation();}private void initAnimation() {int rippleDelay = mRippleInfo.durationTime/mRippleInfo.rippleCount;for (int i = 0; i < mRippleInfo.rippleCount; i++) {//动画1-半径变大final int index = i;ValueAnimator radiusAnimator = ValueAnimator.ofFloat(mRippleInfo.minRadius, mRippleInfo.maxRadius);radiusAnimator.setDuration(mRippleInfo.durationTime); // 动画持续时间radiusAnimator.setStartDelay(i * rippleDelay); // 延迟启动radiusAnimator.setRepeatCount(ValueAnimator.INFINITE);radiusAnimator.setRepeatMode(ValueAnimator.RESTART);radiusAnimator.addUpdateListener(animation -> {mRadiusArray[index] = (float) animation.getAnimatedValue();invalidate(); // 重绘视图});//动画2-画笔变粗ValueAnimator strokeAnimator = ObjectAnimator.ofFloat(mRippleInfo.minStrokenWidth, mRippleInfo.maxStrokenWidth);strokeAnimator.setDuration(mRippleInfo.durationTime); // 动画持续时间strokeAnimator.setStartDelay(i * rippleDelay); // 延迟启动strokeAnimator.setRepeatCount(ValueAnimator.INFINITE);strokeAnimator.setRepeatMode(ValueAnimator.RESTART);strokeAnimator.addUpdateListener(animation -> {mStrokenArray[index] = (float) animation.getAnimatedValue();invalidate(); // 重绘视图});//动画3-颜色淡出ValueAnimator alphaAnimator = ObjectAnimator.ofFloat( 0.1f, 0.8f, 0.8f,0.4f, 0);alphaAnimator.setDuration(mRippleInfo.durationTime); // 动画持续时间alphaAnimator.setStartDelay(i * rippleDelay); // 延迟启动alphaAnimator.setRepeatCount(ValueAnimator.INFINITE);alphaAnimator.setRepeatMode(ValueAnimator.RESTART);alphaAnimator.addUpdateListener(animation -> {mAlphaArray[index] = (float) animation.getAnimatedValue();invalidate(); // 重绘视图});mAnimatorList.add(radiusAnimator);mAnimatorList.add(strokeAnimator);mAnimatorList.add(alphaAnimator);}mAnimatorSet.playTogether(mAnimatorList);}private void initMatrix(float width, float height, float centerX, float centerY) {// if (mMatrixInited) {// return;// }// mMatrixInited = true;mRectF.set(0, 0, width, height);// 应用相机变换mCamera.save();mCamera.rotateX(mRippleInfo.matrixVector3D.x); // 向Y轴旋转mCamera.rotateY(mRippleInfo.matrixVector3D.y); // 向Z轴旋转mCamera.rotateZ(mRippleInfo.matrixVector3D.z); // 向Z轴旋转3mCamera.getMatrix(mMatrix); // 获取变换后的矩阵mCamera.restore();mMatrix.preTranslate(0, -height);//1️以左下角为坐标中心点mMatrix.postTranslate(0, height);mMatrix.mapRect(mRectF);float newCenterX = mRectF.centerX();// 获取变换后的图像的新中心点float newCenterY = mRectF.centerY();float translateX = centerX - newCenterX;// 计算需要的平移量float translateY = centerY - newCenterY;mMatrix.postTranslate(translateX, translateY);// 将平移应用到矩阵}@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);centerX = w / 2.0f;centerY = h / 2.0f;initMatrix(w,h,centerX,centerY);}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);if (!mIsDrawing){return;}canvas.save();// 计算中心点centerX = getWidth() / 2.0f;centerY = getHeight() / 2.0f;canvas.concat(mMatrix);// 将矩阵应用到画布上for (int i = 0; i < mRippleInfo.rippleCount; i++) {if (mRadiusArray[i] > 0) {paint.setStrokeWidth(mStrokenArray[i]);paint.setAlpha((int)(255 * mAlphaArray[i]));canvas.drawCircle(centerX, centerY,mRadiusArray[i],paint);}}// 绘制 logocanvas.drawBitmap(mLogoBitmap, (getWidth()- mRippleInfo.minRadius)/2, (getHeight()- mRippleInfo.minRadius)/2, null);canvas.restore();}public void stopAnimation() {if (mAnimatorSet != null) {mAnimatorSet.cancel();mAnimatorList.clear();}}public void startAnimation(int x,int y,int z) {if(mAnimatorSet != null) {mRippleInfo.matrixVector3D.x = x;mRippleInfo.matrixVector3D.y = y;mRippleInfo.matrixVector3D.z = z;initMatrix(getWidth(),getHeight(),getWidth()/2,getHeight()/2);mAnimatorSet.start();mIsDrawing = true;}}
}
自定义三维坐标系类vectror3D.java
package com.example.waveanimationbysingleview.animation;public class Vector3D {public int x, y, z;public Vector3D(int x, int y, int z) {this.x = x;this.y = y;this.z = z;}// 获取X坐标public int getX() { return x; }// 获取Y坐标public int getY() {return y;}// 获取Z坐标public int getZ() {return z;}@Overridepublic String toString() {return "Vector3D{" +"x=" + x +", y=" + y +", z=" + z +'}';}
}
光波动画属性类RippleAnimationInfo.java
package com.example.waveanimationbysingleview.animation;public class RippleAnimationInfo {public final int minRadius;//波纹半径public final int maxRadius;// 最大波纹半径public final int minStrokenWidth;//画笔最小宽度public final int maxStrokenWidth;//画笔最大宽度public final int durationTime;//动画时长public final Vector3D matrixVector3D;//三维倾斜角度集合public final int[] color;//画笔颜色:支持多色渐变绘制public final int rippleCount; //波纹数量public RippleAnimationInfo(int minRadius, int maxRadius, int minStrokenWidth, int maxStrokenWidth, int durationTime, Vector3D vector3D, int[] color, int rippleCount){this.minRadius = minRadius;this.maxRadius = maxRadius;this.minStrokenWidth = minStrokenWidth;this.maxStrokenWidth = maxStrokenWidth;this.durationTime = durationTime;this.matrixVector3D = vector3D;this.color = color;this.rippleCount = rippleCount;}
}