Android多段线条动画:如何高效保存Canvas状态?
解决多线段动画绘制中保存已完成线段的问题
你不需要保存Canvas的状态——因为每次onDraw()调用时,Canvas都会被重新绘制(默认会清屏),正确的思路是保存已完成的线段数据,每次绘制时先把所有已完成的线段画出来,再画当前正在动画的那一段。这样完全不需要用Bitmap来缓存,只需要一个简单的集合存储已完成线段即可,完美适配大量线条的场景。
具体实现步骤:
- 定义线段数据类:用来存储每一段已完成线段的起点和终点
- 增加已完成线段集合:保存所有已经绘制完成的线段
- 修改动画逻辑:每完成一段线段动画,就把该线段加入集合
- 更新
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
相关产品推荐
相关产品推荐

