Android 绘制沿贝塞尔曲线运动的气泡动画

使用了德卡斯特里奥算法 来计算曲线轨迹点,参考文章
https://blog.csdn.net/venshine/article/details/51750906

BezierData 贝塞尔曲线数据类,用于存储控制点,计算曲线轨迹点

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) / 2;
        if (mCache == null || mCache.length != size) {
    
    
            mCache = new float[size];
        }
        Arrays.fill(mCache, Float.NaN);
    }

    public PointF getLocation(float t) {
    
    
        getOrder();
        initCache();
        float x = deCasteljau(mOrder, 0, t, true);
        initCache();
        float y = deCasteljau(mOrder, 0, t, false);
        mTmpPoint.set(x, y);
        return mTmpPoint;
    }

    /**
     * deCasteljau algorithm
     *
     * @param curOrder current order
     * @param j        control point index
     * @param t        time
     * @param isX      true for x, false for y
     */
    private float deCasteljau(int curOrder, int j, float t, boolean isX) {
    
    
        int index = (mOrder - curOrder) * (mOrder - curOrder + 1) / 2 + j;
        if (!Float.isNaN(mCache[index])) {
    
    
            //Log.d("CACHED_VALUE", "cached value " + (isX ? "x = " : "y = ") + mCache[index]);
            return mCache[index];
        }
        if (curOrder == 1) {
    
    
            PointF pJ = mControlPoints.get(j);
            PointF pJ1 = mControlPoints.get(j + 1);
            if (isX) {
    
    
                mCache[index] = (1 - t) * pJ.x + t * pJ1.x;
            } else {
    
    
                mCache[index] = (1 - t) * pJ.y + t * pJ1.y;
            }
            return mCache[index];
        }
        mCache[index] = (1 - t) * deCasteljau(curOrder - 1, j, t, isX)
                + t * deCasteljau(curOrder - 1, j + 1, t, isX);
        return mCache[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 模式)

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 = getRandomValue(point.x, newRange);
            float y = getRandomValue(point.y, newRange);
            mBezierData.addControlPoint(new PointF(x, y));
        }
        return this;
    }

    public void setLocation(float t) {
    
    
        setXY(mBezierData.getLocation(t));
    }
}

BezierBubbleView 用于增删调整控制点,以及绘制气泡的View

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(getRandomValue(mBubbleRadius, mBubbleRadiusRange))
                        .setColor(mBubbleColor)
                        .setAlpha(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 = getRandomValue(BEZIER_TIME_END - mBezierTimeRange, mBezierTimeRange);
            ObjectAnimator bezierAnimator =
                    ObjectAnimator.ofFloat(bubble, LOCATION, BEZIER_TIME_START, endTime);
            long bezierDelay = RAND.nextInt(mBubbleDelayRange);
            bezierAnimator.setStartDelay(bezierDelay);
            long bezierDuration = 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 随机数工具类

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>

效果如下(模拟器有些卡):
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/hegan2010/article/details/89399913
今日推荐