You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android多段线条动画:如何高效保存Canvas状态?

解决多线段动画绘制中保存已完成线段的问题

你不需要保存Canvas的状态——因为每次onDraw()调用时,Canvas都会被重新绘制(默认会清屏),正确的思路是保存已完成的线段数据,每次绘制时先把所有已完成的线段画出来,再画当前正在动画的那一段。这样完全不需要用Bitmap来缓存,只需要一个简单的集合存储已完成线段即可,完美适配大量线条的场景。

具体实现步骤:

  1. 定义线段数据类:用来存储每一段已完成线段的起点和终点
  2. 增加已完成线段集合:保存所有已经绘制完成的线段
  3. 修改动画逻辑:每完成一段线段动画,就把该线段加入集合
  4. 更新onDraw():先绘制所有已完成线段,再绘制当前动画中的线段

修改后的完整代码:

public class DrawingView extends View {
    private Paint mLinePaint;
    // 已完成的线段集合
    private List<LineSegment> mCompletedSegments = new ArrayList<>();
    // 当前动画中的线段起点和终点
    private PointF mCurrentStart;
    private PointF mCurrentEnd;
    private PointF mCurrentAnimEnd;
    private float mDrawPercent = 0f;
    private float mPercentPerFrame;
    private static final long FRAME_DURATION = 17;
    private int mCurrentSegmentIndex;
    private PointF[] mAllPoints;

    public DrawingView(Context context) {
        super(context);
        init();
    }

    public DrawingView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public DrawingView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mLinePaint = new Paint();
        mLinePaint.setStyle(Paint.Style.STROKE);
        mLinePaint.setColor(0xFF00FF00);
        mLinePaint.setAntiAlias(true);
        mLinePaint.setStrokeWidth(5);
        mCurrentAnimEnd = new PointF();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        
        // 第一步:绘制所有已完成的线段
        for (LineSegment segment : mCompletedSegments) {
            canvas.drawLine(segment.start.x, segment.start.y, segment.end.x, segment.end.y, mLinePaint);
        }
        
        // 第二步:绘制当前正在动画的线段(如果有的话)
        if (mCurrentStart != null && mCurrentEnd != null && mDrawPercent > 0) {
            mCurrentAnimEnd = calculateCurrentEndPoint(mCurrentStart, mCurrentEnd, mDrawPercent);
            canvas.drawLine(mCurrentStart.x, mCurrentStart.y, mCurrentAnimEnd.x, mCurrentAnimEnd.y, mLinePaint);
        }
    }

    public void animateShape(long duration, PointF[] points) {
        if (points == null || points.length < 2) {
            return; // 至少需要两个点才能构成线段
        }
        
        // 重置状态
        mCompletedSegments.clear();
        mDrawPercent = 0f;
        mCurrentSegmentIndex = 0;
        mAllPoints = points;
        
        // 计算每帧需要增加的进度
        mPercentPerFrame = 1f / ((float) duration / FRAME_DURATION);
        
        // 启动第一段动画
        startNextSegment();
        postDelayed(mAnimationRunnable, FRAME_DURATION);
    }

    private void startNextSegment() {
        if (mCurrentSegmentIndex + 1 >= mAllPoints.length) {
            return; // 所有线段都已完成
        }
        mCurrentStart = mAllPoints[mCurrentSegmentIndex];
        mCurrentEnd = mAllPoints[mCurrentSegmentIndex + 1];
        mDrawPercent = 0f;
    }

    private final Runnable mAnimationRunnable = new Runnable() {
        @Override
        public void run() {
            mDrawPercent += mPercentPerFrame;
            
            if (mDrawPercent < 1f) {
                // 继续当前线段的动画
                invalidate();
                postDelayed(this, FRAME_DURATION);
            } else {
                // 当前线段动画完成,加入已完成集合
                mCompletedSegments.add(new LineSegment(mCurrentStart, mCurrentEnd));
                mDrawPercent = 1f;
                invalidate();
                
                // 切换到下一段线段
                mCurrentSegmentIndex++;
                if (mCurrentSegmentIndex + 1 < mAllPoints.length) {
                    startNextSegment();
                    postDelayed(this, FRAME_DURATION);
                }
            }
        }
    };

    private PointF calculateCurrentEndPoint(PointF startPoint, PointF endPoint, float drawPercent) {
        float deltaX = endPoint.x - startPoint.x;
        float deltaY = endPoint.y - startPoint.y;
        return new PointF(
                startPoint.x + deltaX * drawPercent,
                startPoint.y + deltaY * drawPercent
        );
    }

    // 内部类:存储已完成线段的起点和终点
    private static class LineSegment {
        PointF start;
        PointF end;

        LineSegment(PointF start, PointF end) {
            this.start = start;
            this.end = end;
        }
    }
}

关键改动说明:

  • mCompletedSegments集合:用ArrayList存储所有已经绘制完成的线段,每个LineSegment对象保存一段线段的起点和终点,内存占用极低,完全不用担心大量线条的问题。
  • onDraw()逻辑调整:先绘制所有已完成线段,再绘制当前动画中的线段,保证每次刷新屏幕时,之前的线段都能被保留。
  • 动画逻辑优化:把Runnable抽成成员变量,避免每次postDelayed创建新对象;完成一段线段后立即加入集合,再启动下一段动画。
  • 状态重置:在animateShape()开始时清空已完成集合,确保每次启动动画都是全新的绘制流程。

这种方案的优势在于:

  • 内存占用极小:只存储线段的坐标点,相比Bitmap缓存节省大量内存
  • 逻辑清晰:数据和绘制分离,维护起来更简单
  • 适配大量线条:哪怕有成百上千条线段,也不会有性能问题

内容的提问来源于stack exchange,提问作者Verde

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 07:50:37