Update TtmlDecoder to keep only one Span of each type

The current code relies on Android's evaluation order of spans, which
doesn't seem to be defined anywhere.

PiperOrigin-RevId: 288700011
This commit is contained in:
ibaker 2020-01-08 16:18:18 +00:00 committed by Oliver Woodman
parent 8e26505ee8
commit 14e401f53a
4 changed files with 226 additions and 39 deletions

View File

@ -0,0 +1,55 @@
/*
* Copyright (C) 2020 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;
import android.text.Spannable;
import android.text.style.ForegroundColorSpan;
/**
* Utility methods for Android <a href="https://developer.android.com/guide/topics/text/spans">span
* styling</a>.
*/
public final class SpanUtil {
/**
* Adds {@code span} to {@code spannable} between {@code start} and {@code end}, removing any
* existing spans of the same type and with the same indices and flags.
*
* <p>This is useful for types of spans that don't make sense to duplicate and where the
* evaluation order might have an unexpected impact on the final text, e.g. {@link
* ForegroundColorSpan}.
*
* @param spannable The {@link Spannable} to add {@code span} to.
* @param span The span object to be added.
* @param start The start index to add the new span at.
* @param end The end index to add the new span at.
* @param spanFlags The flags to pass to {@link Spannable#setSpan(Object, int, int, int)}.
*/
public static void addOrReplaceSpan(
Spannable spannable, Object span, int start, int end, int spanFlags) {
Object[] existingSpans = spannable.getSpans(start, end, span.getClass());
for (Object existingSpan : existingSpans) {
if (spannable.getSpanStart(existingSpan) == start
&& spannable.getSpanEnd(existingSpan) == end
&& spannable.getSpanFlags(existingSpan) == spanFlags) {
spannable.removeSpan(existingSpan);
}
}
spannable.setSpan(span, start, end, spanFlags);
}
private SpanUtil() {}
}

View File

@ -15,7 +15,6 @@
*/
package com.google.android.exoplayer2.text.ttml;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.AbsoluteSizeSpan;
@ -27,6 +26,7 @@ import android.text.style.StrikethroughSpan;
import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan;
import android.text.style.UnderlineSpan;
import com.google.android.exoplayer2.text.SpanUtil;
import java.util.Map;
/**
@ -77,32 +77,60 @@ import java.util.Map;
builder.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (style.hasFontColor()) {
builder.setSpan(new ForegroundColorSpan(style.getFontColor()), start, end,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
SpanUtil.addOrReplaceSpan(
builder,
new ForegroundColorSpan(style.getFontColor()),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (style.hasBackgroundColor()) {
builder.setSpan(new BackgroundColorSpan(style.getBackgroundColor()), start, end,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
SpanUtil.addOrReplaceSpan(
builder,
new BackgroundColorSpan(style.getBackgroundColor()),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (style.getFontFamily() != null) {
builder.setSpan(new TypefaceSpan(style.getFontFamily()), start, end,
SpanUtil.addOrReplaceSpan(
builder,
new TypefaceSpan(style.getFontFamily()),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (style.getTextAlign() != null) {
builder.setSpan(new AlignmentSpan.Standard(style.getTextAlign()), start, end,
SpanUtil.addOrReplaceSpan(
builder,
new AlignmentSpan.Standard(style.getTextAlign()),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
switch (style.getFontSizeUnit()) {
case TtmlStyle.FONT_SIZE_UNIT_PIXEL:
builder.setSpan(new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end,
SpanUtil.addOrReplaceSpan(
builder,
new AbsoluteSizeSpan((int) style.getFontSize(), true),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
break;
case TtmlStyle.FONT_SIZE_UNIT_EM:
builder.setSpan(new RelativeSizeSpan(style.getFontSize()), start, end,
SpanUtil.addOrReplaceSpan(
builder,
new RelativeSizeSpan(style.getFontSize()),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
break;
case TtmlStyle.FONT_SIZE_UNIT_PERCENT:
builder.setSpan(new RelativeSizeSpan(style.getFontSize() / 100), start, end,
SpanUtil.addOrReplaceSpan(
builder,
new RelativeSizeSpan(style.getFontSize() / 100),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
break;
case TtmlStyle.UNSPECIFIED:

View File

@ -15,11 +15,11 @@
*/
package com.google.android.exoplayer2.text.webvtt;
import static com.google.android.exoplayer2.text.SpanUtil.addOrReplaceSpan;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.graphics.Typeface;
import android.text.Layout;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.SpannedString;
@ -535,7 +535,12 @@ public final class WebvttCueParser {
return;
}
if (style.getStyle() != WebvttCssStyle.UNSPECIFIED) {
addOrReplaceSpan(spannedText, new StyleSpan(style.getStyle()), start, end);
addOrReplaceSpan(
spannedText,
new StyleSpan(style.getStyle()),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (style.isLinethrough()) {
spannedText.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
@ -544,29 +549,62 @@ public final class WebvttCueParser {
spannedText.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (style.hasFontColor()) {
addOrReplaceSpan(spannedText, new ForegroundColorSpan(style.getFontColor()), start, end);
addOrReplaceSpan(
spannedText,
new ForegroundColorSpan(style.getFontColor()),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (style.hasBackgroundColor()) {
addOrReplaceSpan(
spannedText, new BackgroundColorSpan(style.getBackgroundColor()), start, end);
spannedText,
new BackgroundColorSpan(style.getBackgroundColor()),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (style.getFontFamily() != null) {
addOrReplaceSpan(spannedText, new TypefaceSpan(style.getFontFamily()), start, end);
addOrReplaceSpan(
spannedText,
new TypefaceSpan(style.getFontFamily()),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
Layout.Alignment textAlign = style.getTextAlign();
if (textAlign != null) {
addOrReplaceSpan(spannedText, new AlignmentSpan.Standard(textAlign), start, end);
addOrReplaceSpan(
spannedText,
new AlignmentSpan.Standard(textAlign),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
switch (style.getFontSizeUnit()) {
case WebvttCssStyle.FONT_SIZE_UNIT_PIXEL:
addOrReplaceSpan(
spannedText, new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end);
spannedText,
new AbsoluteSizeSpan((int) style.getFontSize(), true),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
break;
case WebvttCssStyle.FONT_SIZE_UNIT_EM:
addOrReplaceSpan(spannedText, new RelativeSizeSpan(style.getFontSize()), start, end);
addOrReplaceSpan(
spannedText,
new RelativeSizeSpan(style.getFontSize()),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
break;
case WebvttCssStyle.FONT_SIZE_UNIT_PERCENT:
addOrReplaceSpan(spannedText, new RelativeSizeSpan(style.getFontSize() / 100), start, end);
addOrReplaceSpan(
spannedText,
new RelativeSizeSpan(style.getFontSize() / 100),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
break;
case WebvttCssStyle.UNSPECIFIED:
// Do nothing.
@ -578,26 +616,6 @@ public final class WebvttCueParser {
}
}
/**
* Adds {@code span} to {@code spannedText} between {@code start} and {@code end}, removing any
* existing spans of the same type and with the same indices.
*
* <p>This is useful for types of spans that don't make sense to duplicate and where the
* evaluation order might have an unexpected impact on the final text, e.g. {@link
* ForegroundColorSpan}.
*/
private static void addOrReplaceSpan(
SpannableStringBuilder spannedText, Object span, int start, int end) {
Object[] existingSpans = spannedText.getSpans(start, end, span.getClass());
for (Object existingSpan : existingSpans) {
if (spannedText.getSpanStart(existingSpan) == start
&& spannedText.getSpanEnd(existingSpan) == end) {
spannedText.removeSpan(existingSpan);
}
}
spannedText.setSpan(span, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
/**
* Returns the tag name for the given tag contents.
*

View File

@ -0,0 +1,86 @@
/*
* Copyright (C) 2020 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;
import static com.google.common.truth.Truth.assertThat;
import android.graphics.Color;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Tests for {@link SpanUtil}. */
@RunWith(AndroidJUnit4.class)
public class SpanUtilTest {
@Test
public void addOrReplaceSpan_replacesSameTypeAndIndexes() {
Spannable spannable = SpannableString.valueOf("test text");
spannable.setSpan(
new ForegroundColorSpan(Color.CYAN),
/* start= */ 2,
/* end= */ 5,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
ForegroundColorSpan newSpan = new ForegroundColorSpan(Color.BLUE);
SpanUtil.addOrReplaceSpan(
spannable, newSpan, /* start= */ 2, /* end= */ 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Object[] spans = spannable.getSpans(0, spannable.length(), Object.class);
assertThat(spans).asList().containsExactly(newSpan);
}
@Test
public void addOrReplaceSpan_ignoresDifferentType() {
Spannable spannable = SpannableString.valueOf("test text");
ForegroundColorSpan originalSpan = new ForegroundColorSpan(Color.CYAN);
spannable.setSpan(originalSpan, /* start= */ 2, /* end= */ 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
BackgroundColorSpan newSpan = new BackgroundColorSpan(Color.BLUE);
SpanUtil.addOrReplaceSpan(spannable, newSpan, 2, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Object[] spans = spannable.getSpans(0, spannable.length(), Object.class);
assertThat(spans).asList().containsExactly(originalSpan, newSpan).inOrder();
}
@Test
public void addOrReplaceSpan_ignoresDifferentStartEndAndFlags() {
Spannable spannable = SpannableString.valueOf("test text");
ForegroundColorSpan originalSpan = new ForegroundColorSpan(Color.CYAN);
spannable.setSpan(originalSpan, /* start= */ 2, /* end= */ 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
ForegroundColorSpan differentStart = new ForegroundColorSpan(Color.GREEN);
SpanUtil.addOrReplaceSpan(
spannable, differentStart, /* start= */ 3, /* end= */ 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
ForegroundColorSpan differentEnd = new ForegroundColorSpan(Color.BLUE);
SpanUtil.addOrReplaceSpan(
spannable, differentEnd, /* start= */ 2, /* end= */ 6, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
ForegroundColorSpan differentFlags = new ForegroundColorSpan(Color.GREEN);
SpanUtil.addOrReplaceSpan(
spannable, differentFlags, /* start= */ 2, /* end= */ 5, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
Object[] spans = spannable.getSpans(0, spannable.length(), Object.class);
assertThat(spans)
.asList()
.containsExactly(originalSpan, differentStart, differentEnd, differentFlags)
.inOrder();
}
}