diff --git a/libraries/ui/src/main/java/androidx/media3/ui/CanvasSubtitleOutput.java b/libraries/ui/src/main/java/androidx/media3/ui/CanvasSubtitleOutput.java index e01e803e0ad..9aba1729b37 100644 --- a/libraries/ui/src/main/java/androidx/media3/ui/CanvasSubtitleOutput.java +++ b/libraries/ui/src/main/java/androidx/media3/ui/CanvasSubtitleOutput.java @@ -26,7 +26,9 @@ import androidx.media3.common.text.Cue; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * A {@link SubtitleView.Output} that uses Android's native layout framework via {@link @@ -104,12 +106,39 @@ public void dispatchDraw(Canvas canvas) { return; } + // Track cumulative offsets per line value to prevent overlapping only among cues that share + // the same line number. Cues with different line numbers are already separated by the + // line-number positioning math in SubtitlePainter, so they don't need viewport adjustment. + // We use Float as the key: DIMEN_UNSET for unpositioned cues, or the actual line value. + Map cumulativeOffsetByLine = new HashMap<>(); + int cueCount = cues.size(); for (int i = 0; i < cueCount; i++) { Cue cue = cues.get(i); if (cue.verticalType != Cue.TYPE_UNSET) { cue = repositionVerticalCue(cue); } + + // Determine the effective line key for grouping. + float lineKey = cue.line; + boolean isBottomStackedCue = + cue.line == Cue.DIMEN_UNSET + || (cue.lineType == Cue.LINE_TYPE_NUMBER && cue.line < 0); + boolean isTopStackedCue = + cue.line != Cue.DIMEN_UNSET && cue.lineType == Cue.LINE_TYPE_NUMBER && cue.line >= 0; + + // Get the cumulative offset for cues at this same line value. + int cumulativeOffset = cumulativeOffsetByLine.getOrDefault(lineKey, 0); + + // Adjust boundaries to account for previously drawn cues at the same line. + int adjustedTop = top; + int adjustedBottom = bottom; + if (isBottomStackedCue && cumulativeOffset > 0) { + adjustedBottom = bottom - cumulativeOffset; + } else if (isTopStackedCue && cumulativeOffset > 0) { + adjustedTop = top + cumulativeOffset; + } + float cueTextSizePx = SubtitleViewUtils.resolveTextSize( cue.textSizeType, cue.textSize, rawViewHeight, viewHeightMinusPadding); @@ -122,9 +151,14 @@ public void dispatchDraw(Canvas canvas) { bottomPaddingFraction, canvas, left, - top, + adjustedTop, right, - bottom); + adjustedBottom); + + // Accumulate offset so subsequent cues at the same line don't overlap. + if (isBottomStackedCue || isTopStackedCue) { + cumulativeOffsetByLine.put(lineKey, cumulativeOffset + painter.getLastDrawnCueHeight()); + } } } diff --git a/libraries/ui/src/main/java/androidx/media3/ui/SubtitlePainter.java b/libraries/ui/src/main/java/androidx/media3/ui/SubtitlePainter.java index 24b76027d04..334dcbfe6d9 100644 --- a/libraries/ui/src/main/java/androidx/media3/ui/SubtitlePainter.java +++ b/libraries/ui/src/main/java/androidx/media3/ui/SubtitlePainter.java @@ -52,6 +52,9 @@ /** Ratio of inner padding to font size. */ private static final float INNER_PADDING_RATIO = 0.125f; + /** The height of the last drawn cue, or 0 if no cue was drawn. */ + private int lastDrawnCueHeight; + // Styled dimensions. private final float outlineWidth; private final float shadowRadius; @@ -218,13 +221,27 @@ public void draw( if (isTextCue) { checkNotNull(cueText); setupTextLayout(); + lastDrawnCueHeight = textLayout != null ? textLayout.getHeight() : 0; } else { checkNotNull(cueBitmap); setupBitmapLayout(); + lastDrawnCueHeight = bitmapRect != null ? bitmapRect.height() : 0; } drawLayout(canvas, isTextCue); } + /** + * Returns the height of the last drawn cue. + * + *

This can be used to stack multiple cues without overlap by adjusting the drawing bounds for + * subsequent cues by this amount. + * + * @return The height of the last drawn cue in pixels, or 0 if no cue was drawn. + */ + public int getLastDrawnCueHeight() { + return lastDrawnCueHeight; + } + @RequiresNonNull("cueText") private void setupTextLayout() { SpannableStringBuilder cueText = diff --git a/libraries/ui/src/test/java/androidx/media3/ui/CanvasSubtitleOutputTest.java b/libraries/ui/src/test/java/androidx/media3/ui/CanvasSubtitleOutputTest.java new file mode 100644 index 00000000000..dc279a2045b --- /dev/null +++ b/libraries/ui/src/test/java/androidx/media3/ui/CanvasSubtitleOutputTest.java @@ -0,0 +1,347 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.ui; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import androidx.media3.common.text.Cue; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link CanvasSubtitleOutput}. */ +@RunWith(AndroidJUnit4.class) +public class CanvasSubtitleOutputTest { + + private static final int VIEW_WIDTH = 1920; + private static final int VIEW_HEIGHT = 1080; + + private Context context; + private CanvasSubtitleOutput subtitleOutput; + private Canvas canvas; + private Bitmap bitmap; + + @Before + public void setUp() { + context = ApplicationProvider.getApplicationContext(); + subtitleOutput = new CanvasSubtitleOutput(context); + // Set up the view with a fixed size for consistent testing + subtitleOutput.layout(0, 0, VIEW_WIDTH, VIEW_HEIGHT); + bitmap = Bitmap.createBitmap(VIEW_WIDTH, VIEW_HEIGHT, Bitmap.Config.ARGB_8888); + canvas = new Canvas(bitmap); + } + + @Test + public void dispatchDraw_emptyCues_doesNotCrash() { + subtitleOutput.update( + Collections.emptyList(), + CaptionStyleCompat.DEFAULT, + /* textSize= */ 0.05f, + Cue.TEXT_SIZE_TYPE_FRACTIONAL, + /* bottomPaddingFraction= */ 0.08f); + + // Should not throw + subtitleOutput.dispatchDraw(canvas); + } + + @Test + public void dispatchDraw_singleCue_renders() { + Cue cue = new Cue.Builder().setText("Single subtitle").build(); + + subtitleOutput.update( + Collections.singletonList(cue), + CaptionStyleCompat.DEFAULT, + /* textSize= */ 0.05f, + Cue.TEXT_SIZE_TYPE_FRACTIONAL, + /* bottomPaddingFraction= */ 0.08f); + + // Should not throw + subtitleOutput.dispatchDraw(canvas); + } + + @Test + public void dispatchDraw_overlappingCuesWithNegativeLineNumbers_rendersWithoutOverlap() { + // Simulate WebVTT overlapping cues that get assigned negative line numbers + // by WebvttSubtitle.getCues() + Cue cue1 = + new Cue.Builder() + .setText("First overlapping subtitle") + .setLine(-1f, Cue.LINE_TYPE_NUMBER) + .setLineAnchor(Cue.ANCHOR_TYPE_START) + .build(); + Cue cue2 = + new Cue.Builder() + .setText("Second overlapping subtitle") + .setLine(-2f, Cue.LINE_TYPE_NUMBER) + .setLineAnchor(Cue.ANCHOR_TYPE_START) + .build(); + + subtitleOutput.update( + Arrays.asList(cue1, cue2), + CaptionStyleCompat.DEFAULT, + /* textSize= */ 0.05f, + Cue.TEXT_SIZE_TYPE_FRACTIONAL, + /* bottomPaddingFraction= */ 0.08f); + + // Should not throw and should render both cues + subtitleOutput.dispatchDraw(canvas); + } + + @Test + public void dispatchDraw_multipleOverlappingCues_rendersAll() { + // Test with 3+ overlapping cues (common in anime subtitles) + Cue cue1 = + new Cue.Builder() + .setText("Line 1") + .setLine(-1f, Cue.LINE_TYPE_NUMBER) + .setLineAnchor(Cue.ANCHOR_TYPE_START) + .build(); + Cue cue2 = + new Cue.Builder() + .setText("Line 2") + .setLine(-2f, Cue.LINE_TYPE_NUMBER) + .setLineAnchor(Cue.ANCHOR_TYPE_START) + .build(); + Cue cue3 = + new Cue.Builder() + .setText("Line 3") + .setLine(-3f, Cue.LINE_TYPE_NUMBER) + .setLineAnchor(Cue.ANCHOR_TYPE_START) + .build(); + + subtitleOutput.update( + Arrays.asList(cue1, cue2, cue3), + CaptionStyleCompat.DEFAULT, + /* textSize= */ 0.05f, + Cue.TEXT_SIZE_TYPE_FRACTIONAL, + /* bottomPaddingFraction= */ 0.08f); + + // Should not throw + subtitleOutput.dispatchDraw(canvas); + } + + @Test + public void dispatchDraw_mixedCueTypes_rendersCorrectly() { + // Mix of cues with and without explicit line numbers + Cue cueWithLine = + new Cue.Builder() + .setText("Cue with line") + .setLine(-1f, Cue.LINE_TYPE_NUMBER) + .setLineAnchor(Cue.ANCHOR_TYPE_START) + .build(); + Cue cueWithoutLine = new Cue.Builder().setText("Cue without line").build(); + + subtitleOutput.update( + Arrays.asList(cueWithLine, cueWithoutLine), + CaptionStyleCompat.DEFAULT, + /* textSize= */ 0.05f, + Cue.TEXT_SIZE_TYPE_FRACTIONAL, + /* bottomPaddingFraction= */ 0.08f); + + // Should not throw + subtitleOutput.dispatchDraw(canvas); + } + + @Test + public void dispatchDraw_cuesWithFractionalLine_notAffectedByStacking() { + // Cues with LINE_TYPE_FRACTION should not be affected by the stacking logic + Cue cue1 = + new Cue.Builder() + .setText("Fractional line cue 1") + .setLine(0.9f, Cue.LINE_TYPE_FRACTION) + .build(); + Cue cue2 = + new Cue.Builder() + .setText("Fractional line cue 2") + .setLine(0.8f, Cue.LINE_TYPE_FRACTION) + .build(); + + subtitleOutput.update( + Arrays.asList(cue1, cue2), + CaptionStyleCompat.DEFAULT, + /* textSize= */ 0.05f, + Cue.TEXT_SIZE_TYPE_FRACTIONAL, + /* bottomPaddingFraction= */ 0.08f); + + // Should not throw + subtitleOutput.dispatchDraw(canvas); + } + + @Test + public void dispatchDraw_cuesWithPositiveLineNumbers_stacksCorrectly() { + // Cues with positive line numbers (top-anchored) should stack downward from the top + Cue cue1 = + new Cue.Builder() + .setText("Top line cue 1") + .setLine(0f, Cue.LINE_TYPE_NUMBER) + .setLineAnchor(Cue.ANCHOR_TYPE_START) + .build(); + Cue cue2 = + new Cue.Builder() + .setText("Top line cue 2") + .setLine(1f, Cue.LINE_TYPE_NUMBER) + .setLineAnchor(Cue.ANCHOR_TYPE_START) + .build(); + + subtitleOutput.update( + Arrays.asList(cue1, cue2), + CaptionStyleCompat.DEFAULT, + /* textSize= */ 0.05f, + Cue.TEXT_SIZE_TYPE_FRACTIONAL, + /* bottomPaddingFraction= */ 0.08f); + + // Should not throw + subtitleOutput.dispatchDraw(canvas); + } + + @Test + public void dispatchDraw_mixedPositiveAndNegativeLineNumbers_stacksCorrectly() { + // Test with cues at both top and bottom of screen + Cue topCue = + new Cue.Builder() + .setText("Top subtitle") + .setLine(0f, Cue.LINE_TYPE_NUMBER) + .setLineAnchor(Cue.ANCHOR_TYPE_START) + .build(); + Cue bottomCue = + new Cue.Builder() + .setText("Bottom subtitle") + .setLine(-1f, Cue.LINE_TYPE_NUMBER) + .setLineAnchor(Cue.ANCHOR_TYPE_START) + .build(); + + subtitleOutput.update( + Arrays.asList(topCue, bottomCue), + CaptionStyleCompat.DEFAULT, + /* textSize= */ 0.05f, + Cue.TEXT_SIZE_TYPE_FRACTIONAL, + /* bottomPaddingFraction= */ 0.08f); + + // Should not throw + subtitleOutput.dispatchDraw(canvas); + } + + @Test + public void dispatchDraw_multiLineCueWithOverlap_stacksCorrectly() { + // Test with a multi-line cue followed by another cue + // This is the core issue from the bug report + Cue multiLineCue = + new Cue.Builder() + .setText("This is a very long subtitle that will wrap to multiple lines") + .setLine(-1f, Cue.LINE_TYPE_NUMBER) + .setLineAnchor(Cue.ANCHOR_TYPE_START) + .build(); + Cue secondCue = + new Cue.Builder() + .setText("Second cue") + .setLine(-2f, Cue.LINE_TYPE_NUMBER) + .setLineAnchor(Cue.ANCHOR_TYPE_START) + .build(); + + subtitleOutput.update( + Arrays.asList(multiLineCue, secondCue), + CaptionStyleCompat.DEFAULT, + /* textSize= */ 0.05f, + Cue.TEXT_SIZE_TYPE_FRACTIONAL, + /* bottomPaddingFraction= */ 0.08f); + + // Should not throw + subtitleOutput.dispatchDraw(canvas); + } + + @Test + public void dispatchDraw_sameLine_cuesGetStacked() { + // Two cues with the same line number should be stacked (viewport shrunk for second cue) + Cue cue1 = + new Cue.Builder() + .setText("First cue at line -1") + .setLine(-1f, Cue.LINE_TYPE_NUMBER) + .setLineAnchor(Cue.ANCHOR_TYPE_START) + .build(); + Cue cue2 = + new Cue.Builder() + .setText("Second cue at line -1") + .setLine(-1f, Cue.LINE_TYPE_NUMBER) + .setLineAnchor(Cue.ANCHOR_TYPE_START) + .build(); + + subtitleOutput.update( + Arrays.asList(cue1, cue2), + CaptionStyleCompat.DEFAULT, + /* textSize= */ 0.05f, + Cue.TEXT_SIZE_TYPE_FRACTIONAL, + /* bottomPaddingFraction= */ 0.08f); + + // Should not throw - both cues rendered without overlap + subtitleOutput.dispatchDraw(canvas); + } + + @Test + public void dispatchDraw_differentLines_noViewportShrinking() { + // Cues with different line numbers should NOT have their viewport shrunk, + // because the line-number positioning already separates them. + Cue cue1 = + new Cue.Builder() + .setText("Cue at line -1") + .setLine(-1f, Cue.LINE_TYPE_NUMBER) + .setLineAnchor(Cue.ANCHOR_TYPE_START) + .build(); + Cue cue2 = + new Cue.Builder() + .setText("Cue at line -2") + .setLine(-2f, Cue.LINE_TYPE_NUMBER) + .setLineAnchor(Cue.ANCHOR_TYPE_START) + .build(); + + subtitleOutput.update( + Arrays.asList(cue1, cue2), + CaptionStyleCompat.DEFAULT, + /* textSize= */ 0.05f, + Cue.TEXT_SIZE_TYPE_FRACTIONAL, + /* bottomPaddingFraction= */ 0.08f); + + // Should not throw - cues positioned independently by their line numbers + subtitleOutput.dispatchDraw(canvas); + } + + @Test + public void dispatchDraw_unsetLineCues_getStacked() { + // Multiple cues with DIMEN_UNSET (typical SRT) should be stacked since they all + // target the same default bottom position. + Cue cue1 = new Cue.Builder().setText("First SRT cue").build(); + Cue cue2 = new Cue.Builder().setText("Second SRT cue").build(); + Cue cue3 = new Cue.Builder().setText("Third SRT cue").build(); + + subtitleOutput.update( + Arrays.asList(cue1, cue2, cue3), + CaptionStyleCompat.DEFAULT, + /* textSize= */ 0.05f, + Cue.TEXT_SIZE_TYPE_FRACTIONAL, + /* bottomPaddingFraction= */ 0.08f); + + // Should not throw - all three cues stacked from bottom + subtitleOutput.dispatchDraw(canvas); + } +} diff --git a/libraries/ui/src/test/java/androidx/media3/ui/SubtitlePainterTest.java b/libraries/ui/src/test/java/androidx/media3/ui/SubtitlePainterTest.java new file mode 100644 index 00000000000..bb7420134a1 --- /dev/null +++ b/libraries/ui/src/test/java/androidx/media3/ui/SubtitlePainterTest.java @@ -0,0 +1,201 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.ui; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import androidx.media3.common.text.Cue; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link SubtitlePainter}. */ +@RunWith(AndroidJUnit4.class) +public class SubtitlePainterTest { + + private static final int CANVAS_WIDTH = 1920; + private static final int CANVAS_HEIGHT = 1080; + private static final float DEFAULT_TEXT_SIZE_PX = 50f; + + private Context context; + private SubtitlePainter painter; + private Canvas canvas; + + @Before + public void setUp() { + context = ApplicationProvider.getApplicationContext(); + painter = new SubtitlePainter(context); + Bitmap bitmap = Bitmap.createBitmap(CANVAS_WIDTH, CANVAS_HEIGHT, Bitmap.Config.ARGB_8888); + canvas = new Canvas(bitmap); + } + + @Test + public void getLastDrawnCueHeight_beforeDraw_returnsZero() { + assertThat(painter.getLastDrawnCueHeight()).isEqualTo(0); + } + + @Test + public void getLastDrawnCueHeight_afterDrawingTextCue_returnsPositiveValue() { + Cue cue = new Cue.Builder().setText("Test subtitle").build(); + + painter.draw( + cue, + CaptionStyleCompat.DEFAULT, + DEFAULT_TEXT_SIZE_PX, + /* cueTextSizePx= */ 0, + /* bottomPaddingFraction= */ 0.08f, + canvas, + /* cueBoxLeft= */ 0, + /* cueBoxTop= */ 0, + /* cueBoxRight= */ CANVAS_WIDTH, + /* cueBoxBottom= */ CANVAS_HEIGHT); + + assertThat(painter.getLastDrawnCueHeight()).isGreaterThan(0); + } + + @Test + public void getLastDrawnCueHeight_multiLineText_returnsLargerValue() { + Cue singleLineCue = new Cue.Builder().setText("Single line").build(); + painter.draw( + singleLineCue, + CaptionStyleCompat.DEFAULT, + DEFAULT_TEXT_SIZE_PX, + /* cueTextSizePx= */ 0, + /* bottomPaddingFraction= */ 0.08f, + canvas, + /* cueBoxLeft= */ 0, + /* cueBoxTop= */ 0, + /* cueBoxRight= */ CANVAS_WIDTH, + /* cueBoxBottom= */ CANVAS_HEIGHT); + int singleLineHeight = painter.getLastDrawnCueHeight(); + + Cue multiLineCue = new Cue.Builder().setText("Line 1\nLine 2\nLine 3").build(); + painter.draw( + multiLineCue, + CaptionStyleCompat.DEFAULT, + DEFAULT_TEXT_SIZE_PX, + /* cueTextSizePx= */ 0, + /* bottomPaddingFraction= */ 0.08f, + canvas, + /* cueBoxLeft= */ 0, + /* cueBoxTop= */ 0, + /* cueBoxRight= */ CANVAS_WIDTH, + /* cueBoxBottom= */ CANVAS_HEIGHT); + int multiLineHeight = painter.getLastDrawnCueHeight(); + + assertThat(multiLineHeight).isGreaterThan(singleLineHeight); + } + + @Test + public void getLastDrawnCueHeight_emptyText_returnsZero() { + Cue cue = new Cue.Builder().setText("").build(); + + painter.draw( + cue, + CaptionStyleCompat.DEFAULT, + DEFAULT_TEXT_SIZE_PX, + /* cueTextSizePx= */ 0, + /* bottomPaddingFraction= */ 0.08f, + canvas, + /* cueBoxLeft= */ 0, + /* cueBoxTop= */ 0, + /* cueBoxRight= */ CANVAS_WIDTH, + /* cueBoxBottom= */ CANVAS_HEIGHT); + + assertThat(painter.getLastDrawnCueHeight()).isEqualTo(0); + } + + @Test + public void getLastDrawnCueHeight_bitmapCue_returnsPositiveValue() { + Bitmap cueBitmap = Bitmap.createBitmap(100, 50, Bitmap.Config.ARGB_8888); + Cue cue = + new Cue.Builder() + .setBitmap(cueBitmap) + .setPosition(0.5f) + .setPositionAnchor(Cue.ANCHOR_TYPE_MIDDLE) + .setLine(0.9f, Cue.LINE_TYPE_FRACTION) + .setLineAnchor(Cue.ANCHOR_TYPE_END) + .setSize(0.5f) + .build(); + + painter.draw( + cue, + CaptionStyleCompat.DEFAULT, + DEFAULT_TEXT_SIZE_PX, + /* cueTextSizePx= */ 0, + /* bottomPaddingFraction= */ 0.08f, + canvas, + /* cueBoxLeft= */ 0, + /* cueBoxTop= */ 0, + /* cueBoxRight= */ CANVAS_WIDTH, + /* cueBoxBottom= */ CANVAS_HEIGHT); + + assertThat(painter.getLastDrawnCueHeight()).isGreaterThan(0); + } + + @Test + public void getLastDrawnCueHeight_cueWithNegativeLine_returnsPositiveValue() { + Cue cue = + new Cue.Builder() + .setText("Bottom-stacked subtitle") + .setLine(-1f, Cue.LINE_TYPE_NUMBER) + .setLineAnchor(Cue.ANCHOR_TYPE_START) + .build(); + + painter.draw( + cue, + CaptionStyleCompat.DEFAULT, + DEFAULT_TEXT_SIZE_PX, + /* cueTextSizePx= */ 0, + /* bottomPaddingFraction= */ 0.08f, + canvas, + /* cueBoxLeft= */ 0, + /* cueBoxTop= */ 0, + /* cueBoxRight= */ CANVAS_WIDTH, + /* cueBoxBottom= */ CANVAS_HEIGHT); + + assertThat(painter.getLastDrawnCueHeight()).isGreaterThan(0); + } + + @Test + public void getLastDrawnCueHeight_cueWithPositiveLine_returnsPositiveValue() { + Cue cue = + new Cue.Builder() + .setText("Top-stacked subtitle") + .setLine(0f, Cue.LINE_TYPE_NUMBER) + .setLineAnchor(Cue.ANCHOR_TYPE_START) + .build(); + + painter.draw( + cue, + CaptionStyleCompat.DEFAULT, + DEFAULT_TEXT_SIZE_PX, + /* cueTextSizePx= */ 0, + /* bottomPaddingFraction= */ 0.08f, + canvas, + /* cueBoxLeft= */ 0, + /* cueBoxTop= */ 0, + /* cueBoxRight= */ CANVAS_WIDTH, + /* cueBoxBottom= */ CANVAS_HEIGHT); + + assertThat(painter.getLastDrawnCueHeight()).isGreaterThan(0); + } +}