airbnb/lottie-android · error · IllegalStateException
Cannot nest start() calls on a single OffscreenBitmap - call
Error message
Cannot nest start() calls on a single OffscreenBitmap - call finish() first
What it means
Thrown by OffscreenLayer.start() when the method is called while a previous start() session is still active (parentCanvas != null at line 215). OffscreenLayer is a single-use-per-session offscreen rendering buffer — calling start() twice without calling finish() in between would corrupt the internal canvas state, transform matrix, and bitmap allocation tracking.
Source
Thrown at lottie/src/main/java/com/airbnb/lottie/utils/OffscreenLayer.java:216
}
private boolean needNewBitmap(@Nullable Bitmap bitmap, RectF bounds) {
if (bitmap == null) {
return true;
}
if (bounds.width() >= bitmap.getWidth() || bounds.height() >= bitmap.getHeight()) {
return true;
}
// If the required area has reduced in size considerably, trigger a reallocation, since
// we might be paying a large unnecessary penalty to work with a bitmap that big.
return bounds.width() < bitmap.getWidth() * 0.75f || bounds.height() < bitmap.getHeight() * 0.75f;
}
public Canvas start(Canvas parentCanvas, RectF bounds, ComposeOp op) {
if (this.parentCanvas != null) {
throw new IllegalStateException("Cannot nest start() calls on a single OffscreenBitmap - call finish() first");
}
// Determine the scaling applied by the parentCanvas' pre-existing transform matrix. This is an optimization
// to avoid creating bitmaps (or render nodes) with unreasonable sizes that will get scaled down when drawn
// onto parentCanvas anyhow.
if (preExistingTransform == null) preExistingTransform = new float[9];
if (parentCanvasMatrix == null) parentCanvasMatrix = new Matrix();
parentCanvas.getMatrix(parentCanvasMatrix);
parentCanvasMatrix.getValues(preExistingTransform);
float pixelScaleX = preExistingTransform[Matrix.MSCALE_X];
float pixelScaleY = preExistingTransform[Matrix.MSCALE_Y];
if (scaledBounds == null) scaledBounds = new RectF();
scaledBounds.set(
bounds.left * pixelScaleX,
bounds.top * pixelScaleY,
bounds.right * pixelScaleX,View on GitHub (pinned to 05ea92e903)
Solutions
- Ensure every start() call is paired with a finish() in a try/finally block so finish() runs even if drawing throws
- If you need nested offscreen rendering, use a separate OffscreenLayer instance for each nesting level
- Audit custom drawing code for all code paths between start() and finish() to guarantee finish() is always reached
Example fix
// before:
// layer.start(canvas, bounds, op);
// drawStuff(layer.getCanvas()); // if this throws, finish() is skipped
// layer.finish();
// layer.start(canvas, bounds2, op); // throws: still active
// after: use try/finally to guarantee finish()
layer.start(canvas, bounds, op);
try {
drawStuff(layer.getCanvas());
} finally {
layer.finish();
}
layer.start(canvas, bounds2, op); // now safe Defensive patterns
Strategy: validation
Validate before calling
// Track session state to prevent nested start()
private boolean offscreenActive = false;
Canvas safeStart(OffscreenLayer layer, Canvas canvas, RectF bounds, ComposeOp op) {
if (offscreenActive) {
throw new IllegalStateException("OffscreenLayer already active");
}
Canvas result = layer.start(canvas, bounds, op);
offscreenActive = true;
return result;
}
void safeFinish(OffscreenLayer layer) {
if (offscreenActive) {
layer.finish();
offscreenActive = false;
}
} Try / catch
// Always pair start() and finish() with try/finally
layer.start(canvas, bounds, op);
try {
drawContent(childCanvas);
} finally {
layer.finish(); // guaranteed to run
} Prevention
- Always wrap start()/finish() in try/finally to guarantee cleanup
- Use a separate OffscreenLayer instance for each nesting level
- Track session state with a boolean flag if reusing the same instance
When it happens
Trigger: Calling offscreenLayer.start(canvas, bounds, op) when a previous start() has not been matched by a finish(). The field this.parentCanvas is non-null from the prior start(), triggering the throw at line 216. This typically occurs in custom drawing code that draws multiple layers but forgot to finish one before starting the next, or in exception paths where finish() was skipped due to an earlier error.
Common situations: Custom Lottie drawable rendering code that nests offscreen layers incorrectly. An exception in the drawing logic between start() and finish() that prevented finish() from being called, leaving the layer in an active state. Reusing a single OffscreenLayer instance across multiple draw operations where the lifecycle is not properly managed. Recursive drawing that attempts to reuse the same layer.
Related errors
- OffscreenBitmap: finish() call without matching start()
- Cannot call getKeyframes on AnimatableSplitDimensionPathValu
- Cannot interpolate between gradients. Lengths vary ({} vs {}
- Unknown trim path type {}
- Unknown point starts with {}
AI-assisted analysis of airbnb/lottie-android@05ea92e903 (2026-08-14).
Data as JSON: /api/errors/fdb625b54fbff960.
Report an issue: GitHub.