diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java b/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java index 3b84761c28..b816c6608b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java @@ -807,6 +807,12 @@ public final class Cue { return this; } + /** Sets {@link Cue#windowColorSet} to false. */ + public Builder clearWindowColor() { + this.windowColorSet = false; + return this; + } + /** * Returns true if the fill color of the window is set. * diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/CueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/CueTest.java index 8c66d35253..c16cb928b1 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/CueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/CueTest.java @@ -76,6 +76,14 @@ public class CueTest { assertThat(modifiedCue.verticalType).isEqualTo(cue.verticalType); } + @Test + public void clearWindowColor() { + Cue cue = + new Cue.Builder().setText(SpannedString.valueOf("text")).setWindowColor(Color.CYAN).build(); + + assertThat(cue.buildUpon().clearWindowColor().build().windowColorSet).isFalse(); + } + @Test public void buildWithNoTextOrBitmapFails() { assertThrows(RuntimeException.class, () -> new Cue.Builder().build()); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java index f0093a282c..d3ef1a1a87 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java @@ -34,7 +34,6 @@ import android.text.TextUtils; import android.text.style.AbsoluteSizeSpan; import android.text.style.BackgroundColorSpan; import android.text.style.ForegroundColorSpan; -import android.text.style.RelativeSizeSpan; import android.util.DisplayMetrics; import androidx.annotation.Nullable; import com.google.android.exoplayer2.text.CaptionStyleCompat; @@ -82,8 +81,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private int cuePositionAnchor; private float cueSize; private float cueBitmapHeight; - private boolean applyEmbeddedStyles; - private boolean applyEmbeddedFontSizes; private int foregroundColor; private int backgroundColor; private int windowColor; @@ -142,8 +139,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * which the same parameters are passed. * * @param cue The cue to draw. - * @param applyEmbeddedStyles Whether styling embedded within the cue should be applied. - * @param applyEmbeddedFontSizes If {@code applyEmbeddedStyles} is true, defines whether font * sizes embedded within the cue should be applied. Otherwise, it is ignored. * @param style The style to use when drawing the cue text. * @param defaultTextSizePx The default text size to use when drawing the text, in pixels. @@ -158,8 +153,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; */ public void draw( Cue cue, - boolean applyEmbeddedStyles, - boolean applyEmbeddedFontSizes, CaptionStyleCompat style, float defaultTextSizePx, float cueTextSizePx, @@ -176,8 +169,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; // Nothing to draw. return; } - windowColor = (cue.windowColorSet && applyEmbeddedStyles) - ? cue.windowColor : style.windowColor; + windowColor = cue.windowColorSet ? cue.windowColor : style.windowColor; } if (areCharSequencesEqual(this.cueText, cue.text) && Util.areEqual(this.cueTextAlignment, cue.textAlignment) @@ -189,8 +181,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; && Util.areEqual(this.cuePositionAnchor, cue.positionAnchor) && this.cueSize == cue.size && this.cueBitmapHeight == cue.bitmapHeight - && this.applyEmbeddedStyles == applyEmbeddedStyles - && this.applyEmbeddedFontSizes == applyEmbeddedFontSizes && this.foregroundColor == style.foregroundColor && this.backgroundColor == style.backgroundColor && this.windowColor == windowColor @@ -219,8 +209,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; this.cuePositionAnchor = cue.positionAnchor; this.cueSize = cue.size; this.cueBitmapHeight = cue.bitmapHeight; - this.applyEmbeddedStyles = applyEmbeddedStyles; - this.applyEmbeddedFontSizes = applyEmbeddedFontSizes; this.foregroundColor = style.foregroundColor; this.backgroundColor = style.backgroundColor; this.windowColor = windowColor; @@ -266,31 +254,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; return; } - // Remove embedded styling or font size if requested. - if (!applyEmbeddedStyles) { - // Remove all spans, regardless of type. - for (Object span : cueText.getSpans(0, cueText.length(), Object.class)) { - cueText.removeSpan(span); - } - } else if (!applyEmbeddedFontSizes) { - AbsoluteSizeSpan[] absSpans = cueText.getSpans(0, cueText.length(), AbsoluteSizeSpan.class); - for (AbsoluteSizeSpan absSpan : absSpans) { - cueText.removeSpan(absSpan); - } - RelativeSizeSpan[] relSpans = cueText.getSpans(0, cueText.length(), RelativeSizeSpan.class); - for (RelativeSizeSpan relSpan : relSpans) { - cueText.removeSpan(relSpan); - } - } else { - // Apply embedded styles & font size. - if (cueTextSizePx > 0) { - // Use an AbsoluteSizeSpan encompassing the whole text to apply the default cueTextSizePx. - cueText.setSpan( - new AbsoluteSizeSpan((int) cueTextSizePx), - /* start= */ 0, - /* end= */ cueText.length(), - Spanned.SPAN_PRIORITY); - } + if (cueTextSizePx > 0) { + // Use an AbsoluteSizeSpan encompassing the whole text to apply the default cueTextSizePx. + cueText.setSpan( + new AbsoluteSizeSpan((int) cueTextSizePx), + /* start= */ 0, + /* end= */ cueText.length(), + Spanned.SPAN_PRIORITY); } // Remove embedded font color to not destroy edges, otherwise it overrides edge color. diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleTextView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleTextView.java index 9d0dfb78a2..0e5b4a3ab2 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleTextView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleTextView.java @@ -40,8 +40,6 @@ import java.util.List; private List cues; @Cue.TextSizeType private int textSizeType; private float textSize; - private boolean applyEmbeddedStyles; - private boolean applyEmbeddedFontSizes; private CaptionStyleCompat style; private float bottomPaddingFraction; @@ -55,18 +53,22 @@ import java.util.List; cues = Collections.emptyList(); textSizeType = Cue.TEXT_SIZE_TYPE_FRACTIONAL; textSize = DEFAULT_TEXT_SIZE_FRACTION; - applyEmbeddedStyles = true; - applyEmbeddedFontSizes = true; style = CaptionStyleCompat.DEFAULT; bottomPaddingFraction = DEFAULT_BOTTOM_PADDING_FRACTION; } @Override - public void onCues(List cues) { - if (this.cues == cues || this.cues.isEmpty() && cues.isEmpty()) { - return; - } + public void update( + List cues, + CaptionStyleCompat style, + float textSize, + @Cue.TextSizeType int textSizeType, + float bottomPaddingFraction) { this.cues = cues; + this.style = style; + this.textSize = textSize; + this.textSizeType = textSizeType; + this.bottomPaddingFraction = bottomPaddingFraction; // Ensure we have sufficient painters. while (painters.size() < cues.size()) { painters.add(new SubtitlePainter(getContext())); @@ -75,54 +77,6 @@ import java.util.List; invalidate(); } - @Override - public void setTextSize(@Cue.TextSizeType int textSizeType, float textSize) { - if (this.textSizeType == textSizeType && this.textSize == textSize) { - return; - } - this.textSizeType = textSizeType; - this.textSize = textSize; - invalidate(); - } - - @Override - public void setApplyEmbeddedStyles(boolean applyEmbeddedStyles) { - if (this.applyEmbeddedStyles == applyEmbeddedStyles - && this.applyEmbeddedFontSizes == applyEmbeddedStyles) { - return; - } - this.applyEmbeddedStyles = applyEmbeddedStyles; - this.applyEmbeddedFontSizes = applyEmbeddedStyles; - invalidate(); - } - - @Override - public void setApplyEmbeddedFontSizes(boolean applyEmbeddedFontSizes) { - if (this.applyEmbeddedFontSizes == applyEmbeddedFontSizes) { - return; - } - this.applyEmbeddedFontSizes = applyEmbeddedFontSizes; - invalidate(); - } - - @Override - public void setStyle(CaptionStyleCompat style) { - if (this.style == style) { - return; - } - this.style = style; - invalidate(); - } - - @Override - public void setBottomPaddingFraction(float bottomPaddingFraction) { - if (this.bottomPaddingFraction == bottomPaddingFraction) { - return; - } - this.bottomPaddingFraction = bottomPaddingFraction; - invalidate(); - } - @Override public void dispatchDraw(Canvas canvas) { @Nullable List cues = this.cues; @@ -163,8 +117,6 @@ import java.util.List; SubtitlePainter painter = painters.get(i); painter.draw( cue, - applyEmbeddedStyles, - applyEmbeddedFontSizes, style, defaultViewTextSizePx, cueTextSizePx, diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java index 23a1add0fc..24f9f6b3a2 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java @@ -20,6 +20,10 @@ import static java.lang.annotation.RetentionPolicy.SOURCE; import android.content.Context; import android.content.res.Resources; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.style.AbsoluteSizeSpan; +import android.text.style.RelativeSizeSpan; import android.util.AttributeSet; import android.util.TypedValue; import android.view.View; @@ -35,6 +39,7 @@ import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; import java.lang.annotation.Retention; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -85,6 +90,14 @@ public final class SubtitleView extends FrameLayout implements TextOutput { @IntDef({VIEW_TYPE_TEXT, VIEW_TYPE_WEB}) public @interface ViewType {} + private List cues; + private CaptionStyleCompat style; + @Cue.TextSizeType private int defaultTextSizeType; + private float defaultTextSize; + private float bottomPaddingFraction; + private boolean applyEmbeddedStyles; + private boolean applyEmbeddedFontSizes; + private @ViewType int viewType; private Output output; private View innerSubtitleView; @@ -95,6 +108,14 @@ public final class SubtitleView extends FrameLayout implements TextOutput { public SubtitleView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); + cues = Collections.emptyList(); + style = CaptionStyleCompat.DEFAULT; + defaultTextSizeType = Cue.TEXT_SIZE_TYPE_FRACTIONAL; + defaultTextSize = DEFAULT_TEXT_SIZE_FRACTION; + bottomPaddingFraction = DEFAULT_BOTTOM_PADDING_FRACTION; + applyEmbeddedStyles = true; + applyEmbeddedFontSizes = true; + SubtitleTextView subtitleTextView = new SubtitleTextView(context, attrs); output = subtitleTextView; innerSubtitleView = subtitleTextView; @@ -113,7 +134,8 @@ public final class SubtitleView extends FrameLayout implements TextOutput { * @param cues The cues to display, or null to clear the cues. */ public void setCues(@Nullable List cues) { - output.onCues(cues != null ? cues : Collections.emptyList()); + this.cues = (cues != null ? cues : Collections.emptyList()); + updateOutput(); } /** @@ -149,6 +171,7 @@ public final class SubtitleView extends FrameLayout implements TextOutput { innerSubtitleView = view; output = view; addView(view); + updateOutput(); } /** @@ -211,7 +234,9 @@ public final class SubtitleView extends FrameLayout implements TextOutput { } private void setTextSize(@Cue.TextSizeType int textSizeType, float textSize) { - output.setTextSize(textSizeType, textSize); + this.defaultTextSizeType = textSizeType; + this.defaultTextSize = textSize; + updateOutput(); } /** @@ -221,7 +246,8 @@ public final class SubtitleView extends FrameLayout implements TextOutput { * @param applyEmbeddedStyles Whether styling embedded within the cues should be applied. */ public void setApplyEmbeddedStyles(boolean applyEmbeddedStyles) { - output.setApplyEmbeddedStyles(applyEmbeddedStyles); + this.applyEmbeddedStyles = applyEmbeddedStyles; + updateOutput(); } /** @@ -231,7 +257,8 @@ public final class SubtitleView extends FrameLayout implements TextOutput { * @param applyEmbeddedFontSizes Whether font sizes embedded within the cues should be applied. */ public void setApplyEmbeddedFontSizes(boolean applyEmbeddedFontSizes) { - output.setApplyEmbeddedFontSizes(applyEmbeddedFontSizes); + this.applyEmbeddedFontSizes = applyEmbeddedFontSizes; + updateOutput(); } /** @@ -251,7 +278,8 @@ public final class SubtitleView extends FrameLayout implements TextOutput { * @param style A style for the view. */ public void setStyle(CaptionStyleCompat style) { - output.setStyle(style); + this.style = style; + updateOutput(); } /** @@ -264,7 +292,8 @@ public final class SubtitleView extends FrameLayout implements TextOutput { * @param bottomPaddingFraction The bottom padding fraction. */ public void setBottomPaddingFraction(float bottomPaddingFraction) { - output.setBottomPaddingFraction(bottomPaddingFraction); + this.bottomPaddingFraction = bottomPaddingFraction; + updateOutput(); } @RequiresApi(19) @@ -288,12 +317,94 @@ public final class SubtitleView extends FrameLayout implements TextOutput { return CaptionStyleCompat.createFromCaptionStyle(captioningManager.getUserStyle()); } + private void updateOutput() { + output.update( + getCuesWithStylingPreferencesApplied(), + style, + defaultTextSize, + defaultTextSizeType, + bottomPaddingFraction); + } + + /** + * Returns {@link #cues} with {@link #applyEmbeddedStyles} and {@link #applyEmbeddedFontSizes} + * applied. + * + *

If {@link #applyEmbeddedStyles} is false then all styling spans are removed from {@link + * Cue#text}, {@link Cue#textSize} and {@link Cue#textSizeType} are set to {@link Cue#DIMEN_UNSET} + * and {@link Cue#windowColorSet} is set to false. + * + *

Otherwise if {@link #applyEmbeddedFontSizes} is false then only size-related styling spans + * are removed from {@link Cue#text} and {@link Cue#textSize} and {@link Cue#textSizeType} are set + * to {@link Cue#DIMEN_UNSET} + */ + private List getCuesWithStylingPreferencesApplied() { + if (applyEmbeddedStyles && applyEmbeddedFontSizes) { + return cues; + } + List strippedCues = new ArrayList<>(cues.size()); + for (int i = 0; i < cues.size(); i++) { + strippedCues.add(removeEmbeddedStyling(cues.get(i))); + } + return strippedCues; + } + + private Cue removeEmbeddedStyling(Cue cue) { + @Nullable CharSequence cueText = cue.text; + if (!applyEmbeddedStyles) { + Cue.Builder strippedCue = + cue.buildUpon().setTextSize(Cue.DIMEN_UNSET, Cue.TYPE_UNSET).clearWindowColor(); + if (cueText != null) { + // Remove all spans, regardless of type. + strippedCue.setText(cueText.toString()); + } + return strippedCue.build(); + } else if (!applyEmbeddedFontSizes) { + if (cueText == null) { + return cue; + } + Cue.Builder strippedCue = cue.buildUpon().setTextSize(Cue.DIMEN_UNSET, Cue.TYPE_UNSET); + if (cueText instanceof Spanned) { + SpannableString spannable = SpannableString.valueOf(cueText); + AbsoluteSizeSpan[] absSpans = + spannable.getSpans(0, spannable.length(), AbsoluteSizeSpan.class); + for (AbsoluteSizeSpan absSpan : absSpans) { + spannable.removeSpan(absSpan); + } + RelativeSizeSpan[] relSpans = + spannable.getSpans(0, spannable.length(), RelativeSizeSpan.class); + for (RelativeSizeSpan relSpan : relSpans) { + spannable.removeSpan(relSpan); + } + strippedCue.setText(spannable); + } + return strippedCue.build(); + } + return cue; + } + /* package */ interface Output { - void onCues(List cues); - void setTextSize(@Cue.TextSizeType int textSizeType, float textSize); - void setApplyEmbeddedStyles(boolean applyEmbeddedStyles); - void setApplyEmbeddedFontSizes(boolean applyEmbeddedFontSizes); - void setStyle(CaptionStyleCompat style); - void setBottomPaddingFraction(float bottomPaddingFraction); + + /** + * Updates the list of cues displayed. + * + * @param cues The cues to display. + * @param style A {@link CaptionStyleCompat} to use for styling unset properties of cues. + * @param defaultTextSize The default font size to apply when {@link Cue#textSize} is {@link + * Cue#DIMEN_UNSET}. + * @param defaultTextSizeType The type of {@code defaultTextSize}. + * @param bottomPaddingFraction The bottom padding to apply when {@link Cue#line} is {@link + * Cue#DIMEN_UNSET}, as a fraction of the view's remaining height after its top and bottom + * padding have been subtracted. + * @see #setStyle(CaptionStyleCompat) + * @see #setTextSize(int, float) + * @see #setBottomPaddingFraction(float) + */ + void update( + List cues, + CaptionStyleCompat style, + float defaultTextSize, + @Cue.TextSizeType int defaultTextSizeType, + float bottomPaddingFraction); } } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleWebView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleWebView.java index ee081f384e..8778ca996c 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleWebView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleWebView.java @@ -16,9 +16,6 @@ */ package com.google.android.exoplayer2.ui; -import static com.google.android.exoplayer2.ui.SubtitleView.DEFAULT_BOTTOM_PADDING_FRACTION; -import static com.google.android.exoplayer2.ui.SubtitleView.DEFAULT_TEXT_SIZE_FRACTION; - import android.content.Context; import android.graphics.Color; import android.text.Layout; @@ -57,14 +54,6 @@ import java.util.List; private final SubtitleTextView subtitleTextView; private final WebView webView; - private final List cues; - - @Cue.TextSizeType private int defaultTextSizeType; - private float defaultTextSize; - private boolean applyEmbeddedStyles; - private boolean applyEmbeddedFontSizes; - private CaptionStyleCompat style; - private float bottomPaddingFraction; public SubtitleWebView(Context context) { this(context, null); @@ -72,13 +61,6 @@ import java.util.List; public SubtitleWebView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); - cues = new ArrayList<>(); - defaultTextSizeType = Cue.TEXT_SIZE_TYPE_FRACTIONAL; - defaultTextSize = DEFAULT_TEXT_SIZE_FRACTION; - applyEmbeddedStyles = true; - applyEmbeddedFontSizes = true; - style = CaptionStyleCompat.DEFAULT; - bottomPaddingFraction = DEFAULT_BOTTOM_PADDING_FRACTION; subtitleTextView = new SubtitleTextView(context, attrs); webView = @@ -104,74 +86,26 @@ import java.util.List; } @Override - public void onCues(List cues) { + public void update( + List cues, + CaptionStyleCompat style, + float textSize, + @Cue.TextSizeType int textSizeType, + float bottomPaddingFraction) { List bitmapCues = new ArrayList<>(); - this.cues.clear(); + List textCues = new ArrayList<>(); for (int i = 0; i < cues.size(); i++) { Cue cue = cues.get(i); if (cue.bitmap != null) { bitmapCues.add(cue); } else { - this.cues.add(cue); + textCues.add(cue); } } - subtitleTextView.onCues(bitmapCues); + subtitleTextView.update(bitmapCues, style, textSize, textSizeType, bottomPaddingFraction); // Invalidate to trigger subtitleTextView to draw. invalidate(); - updateWebView(); - } - - @Override - public void setTextSize(@Cue.TextSizeType int textSizeType, float textSize) { - if (this.defaultTextSizeType == textSizeType && this.defaultTextSize == textSize) { - return; - } - this.defaultTextSizeType = textSizeType; - this.defaultTextSize = textSize; - invalidate(); - updateWebView(); - } - - @Override - public void setApplyEmbeddedStyles(boolean applyEmbeddedStyles) { - if (this.applyEmbeddedStyles == applyEmbeddedStyles - && this.applyEmbeddedFontSizes == applyEmbeddedStyles) { - return; - } - this.applyEmbeddedStyles = applyEmbeddedStyles; - this.applyEmbeddedFontSizes = applyEmbeddedStyles; - invalidate(); - updateWebView(); - } - - @Override - public void setApplyEmbeddedFontSizes(boolean applyEmbeddedFontSizes) { - if (this.applyEmbeddedFontSizes == applyEmbeddedFontSizes) { - return; - } - this.applyEmbeddedFontSizes = applyEmbeddedFontSizes; - invalidate(); - updateWebView(); - } - - @Override - public void setStyle(CaptionStyleCompat style) { - if (this.style == style) { - return; - } - this.style = style; - invalidate(); - updateWebView(); - } - - @Override - public void setBottomPaddingFraction(float bottomPaddingFraction) { - if (this.bottomPaddingFraction == bottomPaddingFraction) { - return; - } - this.bottomPaddingFraction = bottomPaddingFraction; - invalidate(); - updateWebView(); + updateWebView(textCues, style, textSize, textSizeType, bottomPaddingFraction); } /** @@ -181,11 +115,15 @@ import java.util.List; * other methods may be called on this view after destroy. */ public void destroy() { - cues.clear(); webView.destroy(); } - private void updateWebView() { + private void updateWebView( + List cues, + CaptionStyleCompat style, + float defaultTextSize, + @Cue.TextSizeType int defaultTextSizeType, + float bottomPaddingFraction) { StringBuilder html = new StringBuilder(); html.append( Util.formatInvariant( @@ -250,8 +188,7 @@ import java.util.List; String writingMode = convertVerticalTypeToCss(cue.verticalType); String cueTextSizeCssPx = convertTextSizeToCss(cue.textSizeType, cue.textSize); String windowCssColor = - HtmlUtils.toCssRgba( - cue.windowColorSet && applyEmbeddedStyles ? cue.windowColor : style.windowColor); + HtmlUtils.toCssRgba(cue.windowColorSet ? cue.windowColor : style.windowColor); String positionProperty; String lineProperty;