使用了德卡斯特里奥算法 来计算曲线轨迹点,参考文章
https://blog.csdn.net/venshine/article/details/51750906
BezierData 贝塞尔曲线数据类,用于存储控制点,计算曲线轨迹点
import android.graphics.PointF;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @author ganmin.he
* @date 2022/5/13
*/
public class BezierData implements Parcelable {
private int mOrder; // Order of the Bezier curve
private float[] mCache; // deCasteljau algorithm cache
private final List<PointF> mControlPoints; // Control point List
private final PointF mTmpPoint = new PointF();
public BezierData() {
mControlPoints = new ArrayList<>();
}
public BezierData(BezierData source) {
mControlPoints = (ArrayList) ((ArrayList) source.mControlPoints).clone();
}
public BezierData(Parcel parcel) {
mControlPoints = parcel.readArrayList(null);
}
public BezierData(Parcel parcel, ClassLoader loader) {
mControlPoints = parcel.readArrayList(loader);
}
public void clearControlPoints() {
mControlPoints.clear();
}
public void setControlPoints(BezierData source) {
clearControlPoints();
mControlPoints.addAll(source.mControlPoints);
}
public void addControlPoint(PointF point) {
mControlPoints.add(point);
}
public void removeControlPoint() {
if (!mControlPoints.isEmpty()) {
mControlPoints.remove(mControlPoints.size() - 1);
}
}
public PointF getControlPoint(int index) {
return mControlPoints.get(index);
}
public int getControlPointsCount() {
return mControlPoints.size();
}
private int getOrder() {
mOrder = mControlPoints.size() - 1;
return mOrder;
}
private void initCache() {
int size = (mOrder + 1) * (mOrder + 2);
if (mCache == null || mCache.length != size) {
mCache = new float[size];
}
Arrays.fill(mCache, Float.NaN);
}
public PointF getLocation(float t) {
getOrder();
initCache();
deCasteljau(mOrder, 0, t);
float x = mCache[0];
float y = mCache[1];
mTmpPoint.set(x, y);
return mTmpPoint;
}
/**
* deCasteljau algorithm
*
* @param curOrder current order
* @param j control point index
* @param t time
*/
private int deCasteljau(int curOrder, int j, float t) {
int index = (mOrder - curOrder) * (mOrder - curOrder + 1) / 2 + j;
int xIndex = 2 * index;
int yIndex = 2 * index + 1;
if (!Float.isNaN(mCache[xIndex])) {
//Log.d("CACHED_VALUE", "cached value " + mCache[xIndex] + ", " + mCache[yIndex]);
return index;
}
if (curOrder == 1) {
PointF pJ = mControlPoints.get(j);
PointF pJ1 = mControlPoints.get(j + 1);
mCache[xIndex] = (1 - t) * pJ.x + t * pJ1.x;
mCache[yIndex] = (1 - t) * pJ.y + t * pJ1.y;
return index;
}
int nextIndex1 = deCasteljau(curOrder - 1, j, t);
int nextIndex2 = deCasteljau(curOrder - 1, j + 1, t);
mCache[xIndex] = (1 - t) * mCache[2 * nextIndex1] + t * mCache[2 * nextIndex2];
mCache[yIndex] = (1 - t) * mCache[2 * nextIndex1 + 1] + t * mCache[2 * nextIndex2 + 1];
return index;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeList(mControlPoints);
}
public static final Parcelable.Creator<BezierData> CREATOR
= new Parcelable.ClassLoaderCreator<BezierData>() {
@Override
public BezierData createFromParcel(Parcel in) {
return new BezierData(in);
}
@Override
public BezierData createFromParcel(Parcel in, ClassLoader loader) {
return new BezierData(in, loader);
}
@Override
public BezierData[] newArray(int size) {
return new BezierData[size];
}
};
}
Bubble 气泡类,含坐标,半径,颜色,透明度等信息(偷懒,未使用 Builder 模式)
import android.graphics.PointF;
/**
* @author ganmin.he
* @date 2022/5/13
*/
public class Bubble implements Cloneable {
private float mX;
private float mY;
private int mRadius;
private int mColor;
private int mAlpha;
private final BezierData mBezierData = new BezierData();
public Bubble() {
}
public Bubble clone() {
Bubble clone = new Bubble();
clone.mX = mX;
clone.mY = mY;
clone.mRadius = mRadius;
clone.mColor = mColor;
clone.mAlpha = mAlpha;
clone.mBezierData.setControlPoints(mBezierData);
return clone;
}
public Bubble setX(float x) {
mX = x;
return this;
}
public float getX() {
return mX;
}
public Bubble setY(float y) {
mY = y;
return this;
}
public float getY() {
return mY;
}
public Bubble setXY(float x, float y) {
mX = x;
mY = y;
return this;
}
public Bubble setXY(PointF point) {
return setXY(point.x, point.y);
}
public Bubble setRadius(int radius) {
mRadius = radius;
return this;
}
public int getRadius() {
return mRadius;
}
public Bubble setColor(int color) {
mColor = color;
return this;
}
public int getColor() {
return mColor;
}
public Bubble setAlpha(int alpha) {
mAlpha = alpha;
return this;
}
public int getAlpha() {
return mAlpha;
}
public Bubble setControlPoints(BezierData source, float range) {
mBezierData.clearControlPoints();
int count = source.getControlPointsCount();
if (count < 1) {
return this;
}
mBezierData.addControlPoint(source.getControlPoint(0));
for (int i = 1; i < count; i++) {
PointF point = source.getControlPoint(i);
//float newRange = range / (float) Math.sqrt(i);
float newRange = range / i;
float x = RandomUtils.getRandomValue(point.x, newRange);
float y = RandomUtils.getRandomValue(point.y, newRange);
mBezierData.addControlPoint(new PointF(x, y));
}
return this;
}
public void setLocation(float t) {
setXY(mBezierData.getLocation(t));
}
}
BezierBubbleView 用于增删调整控制点,以及绘制气泡的View
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import java.util.ArrayList;
import java.util.List;
/**
* @author ganmin.he
* @date 2022/5/13
*/
public class BezierBubbleView extends View implements Animator.AnimatorListener,
ValueAnimator.AnimatorUpdateListener {
private static final boolean ADJUST_CONTROL_POINT = false;
private static final boolean DEBUG = false;
private static final int BEZIER_WIDTH = 10; // Line width of the Bezier curve
private static final int TEXT_SIZE = 40; // Text paint size
private static final int DEFAULT_BUBBLE_SIZE = 50;
private static final int DEFAULT_BUBBLE_COLOR = Color.TRANSPARENT;
private static final int DEFAULT_BUBBLE_RADIUS = 5;
private static final int DEFAULT_BUBBLE_RADIUS_RANGE = 2;
private static final int DEFAULT_BUBBLE_ALPHA = 155;
private static final int DEFAULT_BUBBLE_ALPHA_RANGE = 100;
private static final int DEFAULT_DURATION = 2600;
private static final int DEFAULT_DURATION_RANGE = 400;
private static final int DEFAULT_DELAY_RANGE = 300;
private static final float DEFAULT_LOCATION_RANGE = 300f;
private static final float DEFAULT_BEZIER_TIME_RANGE = 1 / 3f;
private static final float BEZIER_TIME_START = 0.0f; // Bezier curve start time
private static final float BEZIER_TIME_END = 1.0f; // Bezier curve end time
private static final String LOCATION = "location";
private static final String ALPHA = "alpha";
private final int mBubbleSize; // Bubble size
private final int mBubbleRadius; // Bubble circle radius
private final int mBubbleRadiusRange; // Bubble circle radius range
private final int mBubbleColor; // Bubble color
private final int mBubbleAlpha; // Bubble alpha
private final int mBubbleAlphaRange; // Bubble alpha range
private final int mBubbleDuration; // Animator duration
private final int mBubbleDurationRange; // Animator duration range
private final int mBubbleDelayRange; // Animator delay range
private final float mControlPointLocationRange; // Control point location range
private final float mBezierTimeRange; // Bezier curve time range
private final BezierData mBezierData = new BezierData();
private final List<Bubble> mBubbles = new ArrayList<>();
private final Path mBezierPath = new Path();
private final Paint mBezierPaint = new Paint();
private final Paint mControlPaint = new Paint();
private final Paint mTextPaint = new Paint();
private final Paint mBubblePaint = new Paint();
private final AnimatorSet mBezierAnimatorSet = new AnimatorSet();
private final AnimatorSet mAlphaAnimatorSet = new AnimatorSet();
private boolean mAnimatorPrepared = false;
private boolean mDrawingBubble = false;
private boolean mCanAddControlPoint = false;
private PointF mTargetControlPoint; // Newly added control point or control point to move
{
mBezierPaint.setColor(Color.RED);
mBezierPaint.setStrokeWidth(BEZIER_WIDTH);
mBezierPaint.setStyle(Paint.Style.STROKE);
mBezierPaint.setAntiAlias(true);
mControlPaint.setColor(Color.BLUE);
mControlPaint.setStyle(Paint.Style.FILL);
mControlPaint.setAntiAlias(true);
mTextPaint.setColor(Color.BLACK);
mTextPaint.setTextSize(TEXT_SIZE);
mTextPaint.setAntiAlias(true);
mBubblePaint.setColor(DEFAULT_BUBBLE_COLOR);
mBubblePaint.setStyle(Paint.Style.FILL);
mBubblePaint.setAntiAlias(true);
}
public BezierBubbleView(Context context) {
this(context, null);
}
public BezierBubbleView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BezierBubbleView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public BezierBubbleView(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.BezierBubbleView, defStyleAttr, defStyleRes);
mBubbleSize = a.getInt(R.styleable.BezierBubbleView_bubbleSize, DEFAULT_BUBBLE_SIZE);
mBubbleRadius = a.getDimensionPixelSize(R.styleable.BezierBubbleView_bubbleRadius,
DEFAULT_BUBBLE_RADIUS);
mBubbleRadiusRange = a.getDimensionPixelSize(R.styleable.BezierBubbleView_bubbleRadiusRange,
DEFAULT_BUBBLE_RADIUS_RANGE);
int bubbleColor = a.getColor(R.styleable.BezierBubbleView_bubbleColor,
DEFAULT_BUBBLE_COLOR);
mBubbleAlpha = a.getInt(R.styleable.BezierBubbleView_bubbleAlpha, DEFAULT_BUBBLE_ALPHA);
mBubbleAlphaRange = a.getInt(R.styleable.BezierBubbleView_bubbleAlphaRange,
DEFAULT_BUBBLE_ALPHA_RANGE);
mBubbleDuration = a.getInt(R.styleable.BezierBubbleView_duration, DEFAULT_DURATION);
mBubbleDurationRange = a.getInt(R.styleable.BezierBubbleView_durationRange,
DEFAULT_DURATION_RANGE);
mBubbleDelayRange = a.getInt(R.styleable.BezierBubbleView_delayRange, DEFAULT_DELAY_RANGE);
mControlPointLocationRange = a.getDimension(
R.styleable.BezierBubbleView_controlPointLocationRange, DEFAULT_LOCATION_RANGE);
mBezierTimeRange = a.getFloat(R.styleable.BezierBubbleView_bezierTimeRange,
DEFAULT_BEZIER_TIME_RANGE);
int controlPointsResId = a.getResourceId(R.styleable.BezierBubbleView_controlPoints, 0);
if (controlPointsResId != 0) {
String[] controlPointsArr = getResources().getStringArray(controlPointsResId);
try {
String colorStr = controlPointsArr[0].trim();
if (bubbleColor == DEFAULT_BUBBLE_COLOR) {
bubbleColor = Color.parseColor(colorStr);
}
for (int i = 1; i < controlPointsArr.length; i++) {
String[] xy = controlPointsArr[i].trim().split("\\s*,\\s*");
float x = Float.parseFloat(xy[0]);
float y = Float.parseFloat(xy[1]);
mBezierData.addControlPoint(new PointF(x, y));
}
} catch (NumberFormatException e) {
e.printStackTrace();
}
}
mBubbleColor = bubbleColor;
a.recycle();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int count = mBezierData.getControlPointsCount();
if (count < 1) {
return;
}
canvas.save();
if (mDrawingBubble) {
for (Bubble bubble : mBubbles) {
if (bubble.getAlpha() <= 0) {
continue;
}
mBubblePaint.setColor(bubble.getColor());
mBubblePaint.setAlpha(bubble.getAlpha());
canvas.drawCircle(bubble.getX(), bubble.getY(), bubble.getRadius(), mBubblePaint);
}
} else if (ADJUST_CONTROL_POINT) {
for (int i = 0; i < count; i++) {
PointF controlPoint = mBezierData.getControlPoint(i);
canvas.drawCircle(controlPoint.x, controlPoint.y, mBubbleRadius, mControlPaint);
canvas.drawText("P" + i + "(" + controlPoint.x + "," + controlPoint.y + ")",
controlPoint.x + mBubbleRadius * 2,
controlPoint.y + mBubbleRadius * 2, mTextPaint);
}
if (count > 1) {
int segment = 100;
int delta = segment / 100;
for (int time = 0; time <= segment; time += delta) {
float t = (float) time / segment;
PointF trackPoint = mBezierData.getLocation(t);
if (t == 0.0f) {
mBezierPath.reset();
mBezierPath.moveTo(trackPoint.x, trackPoint.y);
} else {
mBezierPath.lineTo(trackPoint.x, trackPoint.y);
}
}
canvas.drawPath(mBezierPath, mBezierPaint);
}
}
canvas.restore();
if (DEBUG && mDrawingBubble) {
long curTime = SystemClock.elapsedRealtimeNanos() / 1000;
mLogSb.append(curTime - lastTime).append(' ');
lastTime = curTime;
}
}
private PointF getNearestControlPoint(float x, float y) {
int count = mBezierData.getControlPointsCount();
if (count < 1) {
return null;
}
PointF target = mBezierData.getControlPoint(0);
double minD = Math.hypot(x - target.x, y - target.y);
for (int i = 1; i < count; i++) {
PointF tmpPoint = mBezierData.getControlPoint(i);
double D = Math.hypot(x - tmpPoint.x, y - tmpPoint.y);
if (minD > D) {
minD = D;
target = tmpPoint;
}
}
return target;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (ADJUST_CONTROL_POINT) {
int action = event.getAction();
float x = event.getX();
float y = event.getY();
switch (action) {
case MotionEvent.ACTION_DOWN:
if (mCanAddControlPoint) {
mTargetControlPoint = new PointF(x, y);
mBezierData.addControlPoint(mTargetControlPoint);
postInvalidate();
} else {
mTargetControlPoint = getNearestControlPoint(x, y);
}
if (mTargetControlPoint != null) {
return true;
}
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP:
if (mTargetControlPoint != null) {
mTargetControlPoint.set(x, y);
postInvalidate();
return true;
}
default:
break;
}
}
return super.onTouchEvent(event);
}
public void setCanAddControlPoint(boolean canAddControlPoint) {
mCanAddControlPoint = canAddControlPoint;
}
public boolean isCanAddControlPoint() {
return mCanAddControlPoint;
}
public void deleteControlPoint() {
if (ADJUST_CONTROL_POINT) {
int count = mBezierData.getControlPointsCount();
if (count < 1) {
return;
}
mBezierData.removeControlPoint();
postInvalidate();
}
}
public void startAnimator(long delay) {
int count = mBezierData.getControlPointsCount();
if (count < 2) {
return;
}
setupBubbles();
setupAnimator();
mBezierAnimatorSet.setStartDelay(delay);
mAlphaAnimatorSet.setStartDelay(delay);
mBezierAnimatorSet.start();
mAlphaAnimatorSet.start();
}
private void setupBubbles() {
if (mBubbles.isEmpty()) {
for (int i = 0; i < mBubbleSize; i++) {
Bubble bubble = new Bubble()
.setRadius(RandomUtils.getRandomValue(mBubbleRadius, mBubbleRadiusRange))
.setColor(mBubbleColor)
.setAlpha(RandomUtils.getRandomValue(mBubbleAlpha, mBubbleAlphaRange));
mBubbles.add(bubble);
}
}
for (Bubble bubble : mBubbles) {
bubble.setControlPoints(mBezierData, mControlPointLocationRange);
}
}
private void setupAnimator() {
if (mAnimatorPrepared) {
return;
}
mAnimatorPrepared = true;
mBezierAnimatorSet.setInterpolator(new AccelerateInterpolator(0.8f));
mBezierAnimatorSet.addListener(this);
List<Animator> bezierAnimators = new ArrayList<>(mBubbleSize);
List<Animator> alphaAnimators = new ArrayList<>(mBubbleSize);
for (Bubble bubble : mBubbles) {
float endTime = RandomUtils.getRandomValue(BEZIER_TIME_END - mBezierTimeRange, mBezierTimeRange);
ObjectAnimator bezierAnimator =
ObjectAnimator.ofFloat(bubble, LOCATION, BEZIER_TIME_START, endTime);
final long bezierDelay;
if (mBubbleDelayRange == 0) {
bezierDelay = 0;
} else {
bezierDelay = RandomUtils.RAND.nextInt(mBubbleDelayRange);
}
bezierAnimator.setStartDelay(bezierDelay);
long bezierDuration = RandomUtils.getRandomValue(mBubbleDuration, mBubbleDurationRange);
bezierAnimator.setDuration(bezierDuration);
bezierAnimator.addUpdateListener(this);
bezierAnimators.add(bezierAnimator);
ObjectAnimator alphaAnimator = ObjectAnimator.ofInt(bubble, ALPHA, 0);
alphaAnimator.setInterpolator(new AccelerateInterpolator());
long alphaDuration = (long) (bezierDuration * 0.1f);
alphaAnimator.setStartDelay(bezierDelay + bezierDuration - alphaDuration);
alphaAnimator.setDuration(alphaDuration);
alphaAnimators.add(alphaAnimator);
}
mBezierAnimatorSet.playTogether(bezierAnimators);
mAlphaAnimatorSet.playTogether(alphaAnimators);
}
@Override
public void onAnimationUpdate(ValueAnimator animation) {
invalidate();
}
@Override
public void onAnimationStart(Animator animation) {
if (animation == mBezierAnimatorSet) {
mDrawingBubble = true;
}
}
@Override
public void onAnimationEnd(Animator animation) {
if (animation == mBezierAnimatorSet) {
mDrawingBubble = false;
if (DEBUG) {
printLog(mLogSb);
mLogSb.delete(0, mLogSb.length());
}
}
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
private long lastTime = 0;
private final StringBuilder mLogSb = DEBUG ? new StringBuilder() : null;
private static void printLog(StringBuilder sb) {
if (sb == null) {
return;
}
for (int i = 0, step = 1000, len = sb.length(); i < len; i += step) {
int end = Math.min(i + step, len);
Log.d("BEZIER_BUBBLE", sb.substring(i, end));
}
}
/*
@Override
protected Parcelable onSaveInstanceState() {
Parcelable parcelable = super.onSaveInstanceState();
SavedState ss = new SavedState(parcelable);
ss.mBezierData = mBezierData;
return ss;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
mBezierData.setControlPoints(ss.mBezierData);
}
public static class SavedState extends BaseSavedState {
private BezierData mBezierData;
public SavedState(Parcel source) {
super(source);
mBezierData = source.readParcelable(null);
}
public SavedState(Parcel source, ClassLoader loader) {
super(source, loader);
mBezierData = source.readParcelable(loader);
}
public SavedState(Parcelable superState) {
super(superState);
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeParcelable(mBezierData, flags);
}
public static final Parcelable.Creator<SavedState> CREATOR
= new Parcelable.ClassLoaderCreator<SavedState>() {
@Override
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
@Override
public SavedState createFromParcel(Parcel in, ClassLoader loader) {
return new SavedState(in, loader);
}
@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}*/
}
RandomUtils 随机数工具类
import android.os.SystemClock;
import java.util.Random;
/**
* @author ganmin.he
* @date 2022/5/13
*/
public class RandomUtils {
public static final Random RAND = new Random(SystemClock.elapsedRealtime());
public static int getRandomRange(int range) {
if (range == 0) {
return 0;
} else if (range < 0) {
range = -range;
}
return RAND.nextInt(2 * range) - range;
}
public static float getRandomRange(float range) {
return (2 * RAND.nextFloat() - 1) * range;
}
public static int getRandomValue(int base, int range) {
return base + getRandomRange(range);
}
public static float getRandomValue(float base, float range) {
return base + getRandomRange(range);
}
}
attrs.xml BezierBubbleView 可配置的属性
<resources>
<declare-styleable name="BezierBubbleView">
<attr name="bubbleSize" format="integer" />
<attr name="bubbleRadius" format="dimension" />
<attr name="bubbleRadiusRange" format="dimension" />
<attr name="bubbleColor" format="color" />
<attr name="bubbleAlpha" format="integer" />
<attr name="bubbleAlphaRange" format="integer" />
<attr name="duration" format="integer" />
<attr name="durationRange" format="integer" />
<attr name="delayRange" format="integer" />
<attr name="controlPointLocationRange" format="dimension" />
<attr name="bezierTimeRange" format="float" />
<attr name="controlPoints" format="reference" />
</declare-styleable>
</resources>
效果如下(模拟器有些卡):