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 e01e803e0a..f1561fb05b 100644 --- a/libraries/ui/src/main/java/androidx/media3/ui/CanvasSubtitleOutput.java +++ b/libraries/ui/src/main/java/androidx/media3/ui/CanvasSubtitleOutput.java @@ -41,6 +41,7 @@ private float textSize; private CaptionStyleCompat style; private float bottomPaddingFraction; + private int horizontalPadding = 0; public CanvasSubtitleOutput(Context context) { this(context, /* attrs= */ null); @@ -56,6 +57,10 @@ public CanvasSubtitleOutput(Context context, @Nullable AttributeSet attrs) { bottomPaddingFraction = DEFAULT_BOTTOM_PADDING_FRACTION; } + public void setHorizontalPadding(int horizontalPadding) { + this.horizontalPadding = horizontalPadding; + } + @Override public void update( List cues, @@ -70,7 +75,7 @@ public void update( this.bottomPaddingFraction = bottomPaddingFraction; // Ensure we have sufficient painters. while (painters.size() < cues.size()) { - painters.add(new SubtitlePainter(getContext())); + painters.add(new SubtitlePainter(getContext(), horizontalPadding)); } // Invalidate to trigger drawing. invalidate(); diff --git a/libraries/ui/src/main/java/androidx/media3/ui/PaddingLineBackgroundSpan.java b/libraries/ui/src/main/java/androidx/media3/ui/PaddingLineBackgroundSpan.java new file mode 100644 index 0000000000..c0167d7fd0 --- /dev/null +++ b/libraries/ui/src/main/java/androidx/media3/ui/PaddingLineBackgroundSpan.java @@ -0,0 +1,142 @@ +package androidx.media3.ui; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.text.style.LineBackgroundSpan; +import androidx.annotation.ColorInt; +import androidx.annotation.Nullable; +import androidx.annotation.Px; + +public class PaddingLineBackgroundSpan implements LineBackgroundSpan { + + private final int lineBackgroundColor; + private final int horizontalPadding; + private final float left; + private final float top; + private final float right; + private final float bottom; + private final float measureScale; + @Nullable + private final BackgroundSpanInfo[] spanInfos; + + public PaddingLineBackgroundSpan( + @ColorInt int lineBackgroundColor, + int horizontalPadding, + float left, + float top, + float right, + float bottom, + float measureScale, + @Nullable BackgroundSpanInfo[] spanInfos) { + this.lineBackgroundColor = lineBackgroundColor; + this.horizontalPadding = horizontalPadding; + this.left = left; + this.top = top; + this.right = right; + this.bottom = bottom; + this.measureScale = measureScale; + this.spanInfos = spanInfos; + } + + @Override + public void drawBackground( + Canvas canvas, + Paint paint, + @Px int left, + @Px int right, + @Px int top, + @Px int baseline, + @Px int bottom, + CharSequence text, + int start, + int end, + int lineNumber) { + drawBackgroundWithPadding(canvas, paint, text, start, end); + } + + private void drawBackgroundWithPadding( + Canvas canvas, + Paint paint, + CharSequence text, + int start, + int end) { + final int originColor = paint.getColor(); + if (spanInfos != null && spanInfos.length > 0) { + float left = this.left; + float right; + int textPos = start; + for (int index = 0; index < spanInfos.length; index++) { + BackgroundSpanInfo info = spanInfos[index]; + if (info.start > end) { + continue; + } + // draw line background + if (info.start > textPos && info.start < end) { + paint.setColor(lineBackgroundColor); + // draw left padding + if (index == 0) { + canvas.drawRect(this.left - horizontalPadding, top, this.left, bottom, paint); + } + + float textWidth = measureText(paint, text, textPos, info.start); + textPos = info.start; + right = left + textWidth; + canvas.drawRect(left, top, right, bottom, paint); + left = right; + } + // draw span background + if (info.end <= end) { + paint.setColor(info.color); + // draw left padding + if (info.start == start) { + canvas.drawRect(this.left - horizontalPadding, top, this.left, bottom, paint); + } + + float textWidth = measureText(paint, text, textPos, info.end); + textPos = info.end; + right = left + textWidth; + canvas.drawRect(left, top, right, bottom, paint); + left = right; + } + + BackgroundSpanInfo nextInfo = index < spanInfos.length - 1 ? spanInfos[index + 1] : null; + if (nextInfo == null) { + // draw line background and right padding + paint.setColor(info.end == end ? info.color : lineBackgroundColor); + right = this.right; + canvas.drawRect(left, top, right + horizontalPadding, bottom, paint); + left = right; + } + } + } else { + paint.setColor(lineBackgroundColor); + canvas.drawRect(left - horizontalPadding, top, right + horizontalPadding, bottom, paint); + } + paint.setColor(originColor); + } + + private float measureText(Paint paint, CharSequence text, int start, int end) { + if (start < 0 || end < start || end > text.length()) { + return 0f; + } + return paint.measureText(text, start, end) * measureScale; + } + + public static class BackgroundSpanInfo implements Comparable { + + public final int start; + public final int end; + public final int color; + + public BackgroundSpanInfo(int start, int end, int color) { + this.start = start; + this.end = end; + this.color = color; + } + + @Override + public int compareTo(BackgroundSpanInfo info) { + return start - info.start; + } + } +} 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 1c716d3d42..4858a4e30d 100644 --- a/libraries/ui/src/main/java/androidx/media3/ui/SubtitlePainter.java +++ b/libraries/ui/src/main/java/androidx/media3/ui/SubtitlePainter.java @@ -26,6 +26,7 @@ import android.graphics.Paint.Style; import android.graphics.Rect; import android.text.Layout.Alignment; +import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.StaticLayout; @@ -41,6 +42,9 @@ import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Log; import androidx.media3.common.util.Util; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull; @@ -95,9 +99,10 @@ private int textTop; private int textPaddingX; private @MonotonicNonNull Rect bitmapRect; + private final int horizontalPadding; @SuppressWarnings("ResourceType") - public SubtitlePainter(Context context) { + public SubtitlePainter(Context context, int horizontalPadding) { int[] viewAttr = {android.R.attr.lineSpacingExtra, android.R.attr.lineSpacingMultiplier}; TypedArray styledAttributes = context.obtainStyledAttributes(null, viewAttr, 0, 0); spacingAdd = styledAttributes.getDimensionPixelSize(0, 0); @@ -122,6 +127,8 @@ public SubtitlePainter(Context context) { bitmapPaint = new Paint(); bitmapPaint.setAntiAlias(true); bitmapPaint.setFilterBitmap(true); + + this.horizontalPadding = horizontalPadding; } /** @@ -370,6 +377,9 @@ private void setupTextLayout() { this.textLeft = textLeft; this.textTop = textTop; this.textPaddingX = textPaddingX; + + // reset spannable text background span and draw padding area + setupPaddingSpan(backgroundColor, horizontalPadding); } @RequiresNonNull("cueBitmap") @@ -470,6 +480,63 @@ private void drawTextLayout(Canvas canvas) { canvas.restoreToCount(saveCount); } + private void setupPaddingSpan(int backgroundColor, int horizontalPadding) { + if (horizontalPadding <= 0 || !(textLayout.getText() instanceof Spannable)) { + return; + } + List backgroundSpanInfos = new ArrayList<>(); + int lineBackgroundColor = backgroundColor; + Spannable spannableText = (Spannable) textLayout.getText(); + BackgroundColorSpan[] colorSpans = spannableText.getSpans( + 0, + spannableText.length(), + BackgroundColorSpan.class); + if (colorSpans != null) { + for (BackgroundColorSpan span : colorSpans) { + int color = span.getBackgroundColor(); + int spanStart = spannableText.getSpanStart(span); + int spanEnd = spannableText.getSpanEnd(span); + if (spanStart == 0 && spanEnd == spannableText.length()) { + lineBackgroundColor = color; + } else { + backgroundSpanInfos.add( + new PaddingLineBackgroundSpan.BackgroundSpanInfo( + spanStart, + spanEnd, + color)); + } + // remove original background span, background will drawn by PaddingLineBackgroundSpan. + if (color != Color.TRANSPARENT) { + spannableText.removeSpan(span); + } + } + Collections.sort(backgroundSpanInfos); // sort by start position. + } + + if (lineBackgroundColor != Color.TRANSPARENT) { + for (int line = 0; line < textLayout.getLineCount(); line++) { + final float lineWidth = textLayout.getLineMax(line); + int start = textLayout.getLineStart(line); + int end = textLayout.getLineVisibleEnd(line); + float measureScale = lineWidth / textPaint.measureText(spannableText, start, end); + spannableText.setSpan( + new PaddingLineBackgroundSpan( + lineBackgroundColor, + horizontalPadding, + textLayout.getLineLeft(line), + textLayout.getLineTop(line), + textLayout.getLineRight(line), + textLayout.getLineBottom(line), + measureScale, + backgroundSpanInfos.toArray(new PaddingLineBackgroundSpan.BackgroundSpanInfo[0]) + ), + start, + end, + Spanned.SPAN_PRIORITY); + } + } + } + @RequiresNonNull({"cueBitmap", "bitmapRect"}) private void drawBitmapLayout(Canvas canvas) { canvas.drawBitmap(cueBitmap, /* src= */ null, bitmapRect, bitmapPaint); diff --git a/libraries/ui/src/main/java/androidx/media3/ui/SubtitleView.java b/libraries/ui/src/main/java/androidx/media3/ui/SubtitleView.java index 6e7285da64..c386a9f198 100644 --- a/libraries/ui/src/main/java/androidx/media3/ui/SubtitleView.java +++ b/libraries/ui/src/main/java/androidx/media3/ui/SubtitleView.java @@ -151,6 +151,12 @@ public SubtitleView(Context context, @Nullable AttributeSet attrs) { viewType = VIEW_TYPE_CANVAS; } + public void setSubtitleHorizontalPadding(int horizontalPadding) { + if (innerSubtitleView instanceof CanvasSubtitleOutput) { + ((CanvasSubtitleOutput) innerSubtitleView).setHorizontalPadding(horizontalPadding); + } + } + /** * Sets the cues to be displayed by the view. *