Merge pull request #8943 from dlafayet:embeddedstyle2

PiperOrigin-RevId: 375484765
This commit is contained in:
Oliver Woodman 2021-05-26 11:28:37 +01:00
parent 55f50e24eb
commit 69394e6fb5
8 changed files with 283 additions and 35 deletions

View File

@ -15,6 +15,10 @@
* DRM: * DRM:
* Don't restore offline keys before releasing them. In OEMCrypto v16+ keys * Don't restore offline keys before releasing them. In OEMCrypto v16+ keys
must be released without restoring them first. must be released without restoring them first.
* UI:
* Keep subtitle language features embedded (e.g. rubies & tate-chu-yoko)
in `Cue.text` even when `SubtitleView#setApplyEmbeddedStyles()` is
false.
### 2.14.0 (2021-05-13) ### 2.14.0 (2021-05-13)

View File

@ -29,4 +29,4 @@ package com.google.android.exoplayer2.text.span;
// NOTE: There's no Android layout support for this, so this span currently doesn't extend any // NOTE: There's no Android layout support for this, so this span currently doesn't extend any
// styling superclasses (e.g. MetricAffectingSpan). The only way to render this styling is to // styling superclasses (e.g. MetricAffectingSpan). The only way to render this styling is to
// extract the spans and do the layout manually. // extract the spans and do the layout manually.
public final class HorizontalTextInVerticalContextSpan {} public final class HorizontalTextInVerticalContextSpan implements LanguageFeatureSpan {}

View File

@ -0,0 +1,19 @@
/*
* Copyright 2021 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 com.google.android.exoplayer2.text.span;
/** Marker interface for span classes that carry language features rather than style information. */
public interface LanguageFeatureSpan {}

View File

@ -30,7 +30,7 @@ package com.google.android.exoplayer2.text.span;
// extract the spans and do the layout manually. // extract the spans and do the layout manually.
// TODO: Consider adding support for parenthetical text to be used when rendering doesn't support // TODO: Consider adding support for parenthetical text to be used when rendering doesn't support
// rubies (e.g. HTML <rp> tag). // rubies (e.g. HTML <rp> tag).
public final class RubySpan { public final class RubySpan implements LanguageFeatureSpan {
/** The ruby text, i.e. the smaller explanatory characters. */ /** The ruby text, i.e. the smaller explanatory characters. */
public final String rubyText; public final String rubyText;

View File

@ -32,7 +32,7 @@ import java.lang.annotation.Retention;
// NOTE: There's no Android layout support for text emphasis, so this span currently doesn't extend // NOTE: There's no Android layout support for text emphasis, so this span currently doesn't extend
// any styling superclasses (e.g. MetricAffectingSpan). The only way to render this emphasis is to // any styling superclasses (e.g. MetricAffectingSpan). The only way to render this emphasis is to
// extract the spans and do the layout manually. // extract the spans and do the layout manually.
public final class TextEmphasisSpan { public final class TextEmphasisSpan implements LanguageFeatureSpan {
/** /**
* The possible mark shapes that can be used. * The possible mark shapes that can be used.

View File

@ -21,10 +21,6 @@ import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.content.Context; import android.content.Context;
import android.content.res.Resources; import android.content.res.Resources;
import android.graphics.Canvas; import android.graphics.Canvas;
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.AttributeSet;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.View; import android.view.View;
@ -380,37 +376,13 @@ public final class SubtitleView extends FrameLayout implements TextOutput {
} }
private Cue removeEmbeddedStyling(Cue cue) { private Cue removeEmbeddedStyling(Cue cue) {
@Nullable CharSequence cueText = cue.text; Cue.Builder strippedCue = cue.buildUpon();
if (!applyEmbeddedStyles) { if (!applyEmbeddedStyles) {
Cue.Builder strippedCue = SubtitleViewUtils.removeAllEmbeddedStyling(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) { } else if (!applyEmbeddedFontSizes) {
if (cueText == null) { SubtitleViewUtils.removeEmbeddedFontSizes(strippedCue);
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; return strippedCue.build();
} }
} }

View File

@ -16,7 +16,16 @@
*/ */
package com.google.android.exoplayer2.ui; package com.google.android.exoplayer2.ui;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.RelativeSizeSpan;
import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.span.LanguageFeatureSpan;
import com.google.common.base.Predicate;
/** Utility class for subtitle layout logic. */ /** Utility class for subtitle layout logic. */
/* package */ final class SubtitleViewUtils { /* package */ final class SubtitleViewUtils {
@ -48,5 +57,50 @@ import com.google.android.exoplayer2.text.Cue;
} }
} }
/** Removes all styling information from {@code cue}. */
public static void removeAllEmbeddedStyling(Cue.Builder cue) {
cue.clearWindowColor();
if (cue.getText() instanceof Spanned) {
if (!(cue.getText() instanceof Spannable)) {
cue.setText(SpannableString.valueOf(cue.getText()));
}
removeSpansIf(
(Spannable) checkNotNull(cue.getText()), span -> !(span instanceof LanguageFeatureSpan));
}
removeEmbeddedFontSizes(cue);
}
/**
* Removes all font size information from {@code cue}.
*
* <p>This involves:
*
* <ul>
* <li>Clearing {@link Cue.Builder#setTextSize(float, int)}.
* <li>Removing all {@link AbsoluteSizeSpan} and {@link RelativeSizeSpan} spans from {@link
* Cue#text}.
* </ul>
*/
public static void removeEmbeddedFontSizes(Cue.Builder cue) {
cue.setTextSize(Cue.DIMEN_UNSET, Cue.TYPE_UNSET);
if (cue.getText() instanceof Spanned) {
if (!(cue.getText() instanceof Spannable)) {
cue.setText(SpannableString.valueOf(cue.getText()));
}
removeSpansIf(
(Spannable) checkNotNull(cue.getText()),
span -> span instanceof AbsoluteSizeSpan || span instanceof RelativeSizeSpan);
}
}
private static void removeSpansIf(Spannable spannable, Predicate<Object> removeFilter) {
Object[] spans = spannable.getSpans(0, spannable.length(), Object.class);
for (Object span : spans) {
if (removeFilter.apply(span)) {
spannable.removeSpan(span);
}
}
}
private SubtitleViewUtils() {} private SubtitleViewUtils() {}
} }

View File

@ -0,0 +1,199 @@
/*
* Copyright 2021 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 com.google.android.exoplayer2.ui;
import static com.google.android.exoplayer2.testutil.truth.SpannedSubject.assertThat;
import static com.google.common.truth.Truth.assertThat;
import android.graphics.Color;
import android.text.Layout;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.UnderlineSpan;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan;
import com.google.android.exoplayer2.text.span.RubySpan;
import com.google.android.exoplayer2.text.span.TextAnnotation;
import com.google.android.exoplayer2.text.span.TextEmphasisSpan;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Tests for {@link SubtitleView}. */
@RunWith(AndroidJUnit4.class)
public class SubtitleViewUtilsTest {
private static final Cue CUE = buildCue();
@Test
public void testRemoveAllEmbeddedStyling() {
Cue.Builder cueBuilder = CUE.buildUpon();
SubtitleViewUtils.removeAllEmbeddedStyling(cueBuilder);
Cue strippedCue = cueBuilder.build();
Spanned originalText = (Spanned) CUE.text;
Spanned strippedText = (Spanned) strippedCue.text;
// Assert all non styling properties and spans are kept
assertThat(strippedCue.textAlignment).isEqualTo(CUE.textAlignment);
assertThat(strippedCue.multiRowAlignment).isEqualTo(CUE.multiRowAlignment);
assertThat(strippedCue.line).isEqualTo(CUE.line);
assertThat(strippedCue.lineType).isEqualTo(CUE.lineType);
assertThat(strippedCue.position).isEqualTo(CUE.position);
assertThat(strippedCue.positionAnchor).isEqualTo(CUE.positionAnchor);
assertThat(strippedCue.textSize).isEqualTo(Cue.DIMEN_UNSET);
assertThat(strippedCue.textSizeType).isEqualTo(Cue.TYPE_UNSET);
assertThat(strippedCue.size).isEqualTo(CUE.size);
assertThat(strippedCue.verticalType).isEqualTo(CUE.verticalType);
assertThat(strippedCue.shearDegrees).isEqualTo(CUE.shearDegrees);
TextEmphasisSpan expectedTextEmphasisSpan =
originalText.getSpans(0, originalText.length(), TextEmphasisSpan.class)[0];
assertThat(strippedText)
.hasTextEmphasisSpanBetween(
originalText.getSpanStart(expectedTextEmphasisSpan),
originalText.getSpanEnd(expectedTextEmphasisSpan));
RubySpan expectedRubySpan = originalText.getSpans(0, originalText.length(), RubySpan.class)[0];
assertThat(strippedText)
.hasRubySpanBetween(
originalText.getSpanStart(expectedRubySpan), originalText.getSpanEnd(expectedRubySpan));
HorizontalTextInVerticalContextSpan expectedHorizontalTextInVerticalContextSpan =
originalText
.getSpans(0, originalText.length(), HorizontalTextInVerticalContextSpan.class)[0];
assertThat(strippedText)
.hasHorizontalTextInVerticalContextSpanBetween(
originalText.getSpanStart(expectedHorizontalTextInVerticalContextSpan),
originalText.getSpanEnd(expectedHorizontalTextInVerticalContextSpan));
// Assert all styling properties and spans are removed
assertThat(strippedCue.windowColorSet).isFalse();
assertThat(strippedText).hasNoUnderlineSpanBetween(0, strippedText.length());
assertThat(strippedText).hasNoRelativeSizeSpanBetween(0, strippedText.length());
assertThat(strippedText).hasNoAbsoluteSizeSpanBetween(0, strippedText.length());
}
@Test
public void testRemoveEmbeddedFontSizes() {
Cue.Builder cueBuilder = CUE.buildUpon();
SubtitleViewUtils.removeEmbeddedFontSizes(cueBuilder);
Cue strippedCue = cueBuilder.build();
Spanned originalText = (Spanned) CUE.text;
Spanned strippedText = (Spanned) strippedCue.text;
// Assert all non text-size properties and spans are kept
assertThat(strippedCue.textAlignment).isEqualTo(CUE.textAlignment);
assertThat(strippedCue.multiRowAlignment).isEqualTo(CUE.multiRowAlignment);
assertThat(strippedCue.line).isEqualTo(CUE.line);
assertThat(strippedCue.lineType).isEqualTo(CUE.lineType);
assertThat(strippedCue.position).isEqualTo(CUE.position);
assertThat(strippedCue.positionAnchor).isEqualTo(CUE.positionAnchor);
assertThat(strippedCue.size).isEqualTo(CUE.size);
assertThat(strippedCue.windowColor).isEqualTo(CUE.windowColor);
assertThat(strippedCue.windowColorSet).isEqualTo(CUE.windowColorSet);
assertThat(strippedCue.verticalType).isEqualTo(CUE.verticalType);
assertThat(strippedCue.shearDegrees).isEqualTo(CUE.shearDegrees);
TextEmphasisSpan expectedTextEmphasisSpan =
originalText.getSpans(0, originalText.length(), TextEmphasisSpan.class)[0];
assertThat(strippedText)
.hasTextEmphasisSpanBetween(
originalText.getSpanStart(expectedTextEmphasisSpan),
originalText.getSpanEnd(expectedTextEmphasisSpan));
RubySpan expectedRubySpan = originalText.getSpans(0, originalText.length(), RubySpan.class)[0];
assertThat(strippedText)
.hasRubySpanBetween(
originalText.getSpanStart(expectedRubySpan), originalText.getSpanEnd(expectedRubySpan));
HorizontalTextInVerticalContextSpan expectedHorizontalTextInVerticalContextSpan =
originalText
.getSpans(0, originalText.length(), HorizontalTextInVerticalContextSpan.class)[0];
assertThat(strippedText)
.hasHorizontalTextInVerticalContextSpanBetween(
originalText.getSpanStart(expectedHorizontalTextInVerticalContextSpan),
originalText.getSpanEnd(expectedHorizontalTextInVerticalContextSpan));
UnderlineSpan expectedUnderlineSpan =
originalText.getSpans(0, originalText.length(), UnderlineSpan.class)[0];
assertThat(strippedText)
.hasUnderlineSpanBetween(
originalText.getSpanStart(expectedUnderlineSpan),
originalText.getSpanEnd(expectedUnderlineSpan));
// Assert the text-size properties and spans are removed
assertThat(strippedCue.textSize).isEqualTo(Cue.DIMEN_UNSET);
assertThat(strippedCue.textSizeType).isEqualTo(Cue.TYPE_UNSET);
assertThat(strippedText).hasNoRelativeSizeSpanBetween(0, strippedText.length());
assertThat(strippedText).hasNoAbsoluteSizeSpanBetween(0, strippedText.length());
}
private static Cue buildCue() {
SpannableString spanned =
new SpannableString("TextEmphasis おはよ Ruby ございます 123 Underline RelativeSize AbsoluteSize");
spanned.setSpan(
new TextEmphasisSpan(
TextEmphasisSpan.MARK_SHAPE_CIRCLE,
TextEmphasisSpan.MARK_FILL_FILLED,
TextAnnotation.POSITION_BEFORE),
"Text emphasis ".length(),
"Text emphasis おはよ".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spanned.setSpan(
new RubySpan("おはよ", TextAnnotation.POSITION_BEFORE),
"TextEmphasis おはよ Ruby ".length(),
"TextEmphasis おはよ Ruby ございます".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spanned.setSpan(
new HorizontalTextInVerticalContextSpan(),
"TextEmphasis おはよ Ruby ございます ".length(),
"TextEmphasis おはよ Ruby ございます 123".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spanned.setSpan(
new UnderlineSpan(),
"TextEmphasis おはよ Ruby ございます 123 ".length(),
"TextEmphasis おはよ Ruby ございます 123 Underline".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spanned.setSpan(
new RelativeSizeSpan(1f),
"TextEmphasis おはよ Ruby ございます 123 Underline ".length(),
"TextEmphasis おはよ Ruby ございます 123 Underline RelativeSize".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spanned.setSpan(
new AbsoluteSizeSpan(10),
"TextEmphasis おはよ Ruby ございます 123 Underline RelativeSize ".length(),
"TextEmphasis おはよ Ruby ございます 123 Underline RelativeSize AbsoluteSize".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
return new Cue.Builder()
.setText(spanned)
.setTextAlignment(Layout.Alignment.ALIGN_CENTER)
.setMultiRowAlignment(Layout.Alignment.ALIGN_NORMAL)
.setLine(5, Cue.LINE_TYPE_NUMBER)
.setLineAnchor(Cue.ANCHOR_TYPE_END)
.setPosition(0.4f)
.setPositionAnchor(Cue.ANCHOR_TYPE_MIDDLE)
.setTextSize(0.2f, Cue.TEXT_SIZE_TYPE_FRACTIONAL)
.setSize(0.8f)
.setWindowColor(Color.CYAN)
.setVerticalType(Cue.VERTICAL_TYPE_RL)
.setShearDegrees(-15f)
.build();
}
}